How to find PHP webshells in wp-content/uploads
Updated October 2026
The uploads directory is where WordPress stores images and documents, and it is writable by the web server, which makes it the most common place for an attacker to drop a webshell. Legitimate sites have no reason to run PHP from there, so every PHP file you find is a lead.
List every PHP file in uploads
Start with a plain listing. Include files that only look like images, because webshells are often named image.php, .cache.php or hidden in a dot-directory:
$ find /var/www/html/wp-content/uploads -type f \
\( -name '*.php' -o -name '*.php?' -o -name '*.phtml' -o -name '*.phar' \) -ls
On a clean site this prints nothing. Also look for files that are PHP regardless of extension, which catches shell.jpg tricks paired with a malicious .htaccess:
$ grep -rl --include='*' -m1 '<?php' /var/www/html/wp-content/uploads
$ find /var/www/html/wp-content/uploads -name .htaccess -exec sh -c 'echo "== $1"; cat "$1"' _ {} \;
A .htaccess in uploads that adds a PHP handler for .jpg or .txt files is itself part of the attack.
Scan uploads with YARA rules
A listing finds files; YARA rules tell you what they are. Feed the whole uploads tree to WPSniffer. It is fast because it skips nothing and reads each file whole, and it credits the rule so you can read the author's description of the malware family:
$ wpsniffer malware-scan --match-all \
--output-columns filename,signature_id,signature_description,rule_reference,matched_text \
/var/www/html/wp-content/uploads
Because this directory should contain no code at all, it is safe to use the broader extended package here even if you keep core for the rest of the site:
$ wpsniffer malware-scan --yara-forge extended /var/www/html/wp-content/uploads
Install instructions are in the docs; the whole setup takes two minutes.
Inspect what you found
Before removing a file, learn what it does and when it arrived. That tells you whether other files are involved and how the attacker got in.
$ stat /var/www/html/wp-content/uploads/2026/10/cache.php
$ head -c 600 /var/www/html/wp-content/uploads/2026/10/cache.php
$ grep -h 'uploads/2026/10/cache.php' /var/log/apache2/*access* /var/log/nginx/*access* 2>/dev/null | head
Typical signs in the file itself are eval, base64_decode, gzinflate, str_rot13, $_POST or $_COOKIE fed into a function call, long encoded strings, or a password check at the top. The access log shows the IP addresses that called it, and the first request to it often reveals the vulnerable plugin or form that uploaded it.
Keep a copy before you remove anything. Move suspicious files to a quarantine directory outside the web root with no permissions rather than deleting them:
$ mkdir -p -m 700 ~/quarantine
$ mv /var/www/html/wp-content/uploads/2026/10/cache.php ~/quarantine/
$ chmod 000 ~/quarantine/cache.php
Block PHP execution in uploads
Once clean, make the directory unable to execute PHP even if a file is dropped again. This single change defuses most upload-based attacks.
Apache (place in wp-content/uploads/.htaccess, requires AllowOverride):
<FilesMatch "\.(php|php[0-9]|phtml|phar)$">
Require all denied
</FilesMatch>
Nginx (inside the server block, before the generic PHP location):
location ~* ^/wp-content/uploads/.*\.(php|php[0-9]|phtml|phar)$ {
deny all;
}
Reload the web server and confirm by requesting a harmless test file you create, then remove the test file. Also set define('DISALLOW_FILE_EDIT', true); in wp-config.php so the admin theme and plugin editors cannot be used to write PHP.
Re-scan and schedule
Webshells rarely travel alone. Scan the whole site, not just uploads, and look at recently modified files for the loader that dropped the shell:
$ wpsniffer malware-scan --yara-forge extended --match-all /var/www/html
Then add a nightly scan. A one-line cron job that scans uploads every hour costs nothing and catches the next drop within the hour:
0 * * * * find /var/www/html/wp-content/uploads -type f -name '*.ph*' -print0 | /home/deploy/wpsniffer/bin/wpsniffer malware-scan