2026-07-01 · Team Fenrir
How to spot a web shell in a WordPress site
A web shell is the shortest answer to the question “how did they get back in?”. It is not the initial attack: it is what the attacker leaves behind afterwards, to return at will. On WordPress it almost always takes the form of a PHP file hidden among yours. Spotting it takes three things: knowing where to look, with which commands, and what to look for in the logs. This guide covers all three.
What exactly is a web shell?
It is a script uploaded to the server that exposes a command interface over HTTP: the attacker opens a URL (or sends a POST) and the server executes whatever it is told — read files, write more code, spawn processes. OWASP classes it among the direct consequences of unrestricted file upload: if an application lets you upload a file and that file can be executed server-side, the result is arbitrary code execution. MITRE ATT&CK catalogues it as T1505.003 (Server Software Component: Web Shell), a persistence technique: its purpose is to survive the discovery of the initial attack.
The practical difference from other malware: a web shell does nothing on its own. It sits still, invisible, until the attacker calls it. That is why the classic symptoms (slow site, odd redirects, spam) are often entirely absent.
Where does it hide in WordPress?
Web shells end up where code should not be, or where nobody looks:
wp-content/uploads/— the media directory. It should contain images and documents, never PHP. It is the most common hiding place because it is always writable by the web server.- Inside a legitimate plugin or theme — an added file (
helper.php,class-cache.php) or a few lines injected at the end of a real file. The plugin keeps working normally, so nobody notices. wp-content/mu-plugins/— “must-use plugins” load automatically and never show up in the activatable plugins list. Few people know this directory exists: perfect for a backdoor.- Names that mimic the core —
wp-conf.php,wp-sett.php,wp-cache.phpin the root or inwp-includes/. At a glance they look like WordPress files; they are not.
Which signs give it away?
Four concrete indicators, in order of reliability:
- PHP files where none should exist. A
.phpinuploads/is suspicious by definition. - Obfuscated code.
eval(,base64_decode(,gzinflate(,str_rot13(chained together are the classic signature: the payload is encoded to evade scanners and is decoded and executed on the fly. - Core or plugin files that don’t match the official checksums. This is the most reliable check: it doesn’t look at how the code is written, but at whether it is what WordPress.org distributed.
- Inconsistent timestamps. A “core” file modified after the last update is an anomaly. Beware though: attackers know how to forge dates (
touch), so a clean timestamp proves nothing — a dirty one is just one more clue.
How do I search for it from the command line?
Four commands, from the simplest to the most reliable:
# 1. PHP in the media directory: there should be nothing
find wp-content/uploads -name '*.php'
# 2. Obfuscation patterns in the site files
grep -rEl "eval\(|base64_decode\(|gzinflate\(" wp-content/
# 3. Core integrity against the official WordPress.org checksums
wp core verify-checksums
# 4. Plugin integrity (official repository plugins only)
wp plugin verify-checksums --all
Command 3 is the most valuable: wp core verify-checksums compares every core file against the checksums published by WordPress.org and flags both modified files and unexpected ones that should not exist. Command 2 produces false positives (some legitimate plugins use base64_decode): treat its output as a list of candidates to examine, not as a verdict.
What should I look for in the web server logs?
A web shell is used over HTTP, so it leaves traces in the access logs. MITRE lists log and file monitoring among the detection methods for this technique. The typical patterns:
- Repeated POSTs to a single PHP file that is not a known endpoint (
admin-ajax.phpandwp-cron.phpreceive legitimate POSTs;wp-content/uploads/2026/01/img.phpdoes not). - Requests with long, unreadable parameters — often the command to execute, base64-encoded.
- A single IP always calling the same file, at odd hours, with no referrer and never touching the rest of the site: a real visitor browses, a backdoor does not.
- 200 responses on URLs you don’t recognise. If a PHP file you never installed answers 200, the problem is confirmed.
A useful grep as a starting point:
grep 'POST' access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
If the top of the list shows a file you don’t recognise, you have found the thread to pull.
How do I remove it without destroying the evidence?
Instinct says “delete it now”. Resist: the web shell is the re-entry point, not the cause. If you remove it without understanding how the attacker got in, you will find it again tomorrow.
In order:
- Preserve the evidence — copy the suspicious file and the log lines that mention it off the server (you need them to reconstruct the entry point, and for any notification duties).
- Reconstruct the entry — the first request to that file in the logs, and what happened right before it: an upload? a login? a plugin exploit?
- Close the hole — update or remove the vulnerable component, rotate all credentials (WordPress admin, database, FTP/SSH), invalidate sessions.
- Only now remove it — delete the web shell and restore modified files from clean sources (the checksums from the previous step tell you exactly which ones).
- Re-verify — a clean
wp core verify-checksums, logs under watch for the following days: if the attacker had a second backdoor, they will use it.
How do I keep it from coming back?
The official WordPress hardening guide converges on a few high-yield points:
- Block PHP execution in
uploads/— the single most effective countermeasure: even if a PHP file gets uploaded, the web server refuses to execute it. On nginx:location ~* /uploads/.*\.php$ { return 403; }. - Update core, plugins and themes — most WordPress compromises come through known, already-patched vulnerable components.
- Fewer components, less surface — every deactivated-but-installed plugin is attackable code you don’t need: remove it.
- Minimal permissions — files must not be writable by the web server where it isn’t needed; no
777. - Recurring integrity checks —
wp core verify-checksumsin cron is a nearly free web shell detector.
Why does continuous monitoring matter?
Everything above has a structural limit: it is manual and point-in-time. A web shell found with Saturday morning’s grep is a web shell that had a week’s head start. The point is not the single command, but having something that watches files and logs continuously and alerts you when a PHP file appears where it shouldn’t, when a core file changes checksum, when an IP starts hammering an endpoint that doesn’t exist.
That is exactly what Fenrir SOC does on servers and Fenrir for WordPress does on sites: file integrity, log correlation and a response that fires before you open the terminal. You sleep, the site defends itself — and you read about the web shell in the morning report, already blocked.
Frequently asked questions
What is a web shell?▾
A script (on WordPress almost always PHP) uploaded by the attacker to run commands on the server remotely, typically over HTTP requests. It is a persistent backdoor: MITRE ATT&CK catalogues it as persistence technique T1505.003.
Is an antivirus enough to find it?▾
Not always: obfuscated web shells evade signatures. You also need behavioural checks on files (integrity against official checksums, timestamps) and analysis of the web server access logs.
How do I check whether a WordPress core file was modified?▾
With WP-CLI: `wp core verify-checksums` compares every core file against the official WordPress.org checksums and lists modified or unexpected files. For plugins from the official repository there is `wp plugin verify-checksums`.
Is deleting the web shell file enough?▾
No. The web shell is the re-entry point, not the cause: if you don't find and close the way in (vulnerable plugin, stolen credentials, unfiltered upload), the attacker will recreate it. Before deleting, preserve the file and the logs as evidence.