How to Check a WordPress or PHP Site for Web Shells
Spot the signs of a web shell or backdoor, run safe checks, compare your files to a clean copy, and follow a careful recovery order if you find one.
A web shell is a script an attacker plants on your server so they can run commands, upload files, read your database or send spam through ordinary web requests. This guide is for small-business owners and the person who looks after their WordPress or PHP site. It explains the warning signs, the checks you can run safely, and the order to work in if you find something.
A backdoor is anything that gives unauthorised access while bypassing your normal controls. It can be a standalone file, or a few lines slipped into a real file such as wp-config.php or a theme's functions.php. A web shell is usually a backdoor too.
In short: Look for PHP files where none belong, search for suspicious code, and compare your files with a clean copy of the same software versions. If you confirm something, take the site offline at server level, keep evidence, find the entry point, rebuild from trusted downloads and change every credential. No single search or scan can prove a site is clean.
Signs that something is wrong
You rarely spot a shell by browsing your own site. The clues usually come from elsewhere:
- Your host warns you about spam, unusual outbound traffic or CPU spikes.
- Google Search Console reports a security issue.
- Visitors are sent to another site, often only when they arrive from search results or on mobile.
- Search results show pages you never made, such as foreign-language text or pharmacy keywords.
- Your emails start bouncing or landing in spam.
- Administrator accounts appear that you did not create.
- Files were changed at times when nobody was working on the site.
One sign alone does not prove a shell exists, but it is enough reason to investigate.
Before you start: don't make it worse
- Talk to your host. A maintenance-page plugin does not stop attackers reaching a malicious script directly. Ask how to take the site offline at server level or restrict access to your own IP address.
- Preserve evidence. Take a full backup of files and database, and copy the access and error logs. Store the copy away from the web root, label it "possibly infected" and never run code from it.
- Never open a suspicious
.phpfile in a browser to see what it does. That is exactly how the attacker uses it. Read it in a plain text editor on a trusted computer.
Watch out: If you cannot take a safe backup or inspect files confidently, stop and ask your host or a security professional. A half-finished clean-up is worse than none, because it gives false confidence.
Check 1: files that don't belong
Shells are often PHP files hiding in folders that should only hold images or cached data. On WordPress, look at:
wp-content/uploads/and its subfolderswp-content/cache/images,assetsorjsfolders inside themes and plugins- the site root, beside
wp-config.php wp-content/mu-plugins/, which loads automatically
Connect by SFTP or your host's file manager. With SSH on Linux, this lists PHP-type files in uploads:
find /path/to/site/wp-content/uploads -type f \( -iname "*.php" -o -iname "*.phtml" -o -iname "*.phar" \)
Any result deserves a look. No result does not prove the folder is clean, because server rules in .htaccess or .user.ini can make other file types run as PHP. Also watch for lookalike names such as wp-conflg.php and files beginning with a dot, which some file managers hide. A strange name proves nothing; it means "open it in a text editor and read it".
Check 2: suspicious code patterns
Malicious scripts must run attacker-supplied input, so they lean on a small set of PHP features. This search builds a shortlist of files to read:
grep -R --include="*.php" -nE "eval\s*\(|base64_decode\s*\(|gzinflate\s*\(|str_rot13\s*\(|shell_exec\s*\(|passthru\s*\(|system\s*\(|FilesMan" /path/to/site
Treat this as a filter, not a verdict. Attackers can hide calls behind variable names, string joins and encoding, and legitimate plugins sometimes use these functions too.
| Pattern | What it usually means |
|---|---|
eval( | Runs a string as PHP; rare in well-written code, so check every hit |
base64_decode + eval or gzinflate | A common way to hide a payload |
system, shell_exec, passthru | Runs operating-system commands |
A flagged file that is one long line of unreadable characters, or looks like this, is highly suspicious:
<?php eval(base64_decode("ZXZhbChnemluZmxhdGUoYmFzZTY0X2RlY29kZS..."));
Do not run it to find out. Compare it with a trusted source instead.
Check 3: compare against a clean copy
Comparing with an untouched copy of the same software is the best way to find backdoors hidden inside genuine files.
- Find your WordPress version in the admin dashboard, or in
$wp_versioninsidewp-includes/version.php. That file can be tampered with, so cross-check against an older backup if you have one. - Download the same version from the WordPress releases page and unzip it into a separate, empty folder.
- Download each installed plugin and theme at the version you run. The WordPress download does not include them.
- Compare each clean copy with the matching folder on your site.
On Linux or macOS:
diff -r /tmp/wordpress-clean/ /path/to/site/
Some differences are normal: wp-content/uploads (your media), wp-config.php (your settings), plus any custom code you or your developer added. Anything else is worth explaining, such as an extra file in core or a changed line in a plugin or theme file.
WordPress hiding places
| Where | What to look for |
|---|---|
wp-config.php | Code added at the bottom, or an include of an odd file |
.htaccess (root and uploads) | Redirect rules that apply only to certain visitors, or rules running images as PHP |
.user.ini | auto_prepend_file, which makes every PHP request load a chosen file |
mu-plugins | Unfamiliar files that load without being activated |
| Users screen | Administrators you do not recognise |
Tip: Do not delete a suspicious administrator straight away. Confirm who owns the account, keep a record of it, then remove access while the site is contained.
Check 4: look in the database
Some compromises live partly in the database. Compare the siteurl and home options with what you intend, and look for injected <script> tags pointing at unfamiliar domains in posts, widgets and theme options. Review users together with their roles. Do not hand-edit serialised option values; use a tool that re-serialises correctly, after taking a database backup.
The free Safe Scan checks your site from the outside, the way a visitor sees it. It cannot read your server files or database, so treat it as a second opinion alongside the checks above, never as proof that the inside is clean.
If you find a shell or backdoor
Assume there may be more than one way back in, and work in this order:
- Contain. Take the site offline at server or hosting level. If other sites share the same hosting account, check them too.
- Preserve evidence. Keep the infected backup, logs, file timestamps and a database copy outside the web root.
- Investigate from a trusted device. Do not run tools downloaded from the compromised site. Also review scheduled server jobs, SSH keys and server-level PHP settings, not just site files.
- Find the entry point. Typical causes are outdated plugins or themes, weak or reused passwords, stolen SFTP or SSH access and vulnerable custom code. Fix the cause before reopening, or the attacker simply returns.
- Rebuild from trusted sources. Reinstall core, plugins and themes from their original downloads at current versions. Review any content you carry over (uploads, custom code, database).
- Change every credential from a clean device: database password, WordPress admins, hosting panel, SFTP/SSH, API keys and the salts in
wp-config.php. Revoke active sessions. - Validate and monitor. Test the site, then watch logs for signs of reinfection over the following weeks.
- Consider personal data. If customer data may have been accessed, get legal advice on any duty to notify in your country.
Make reinfection harder
- Keep PHP, core, plugins and themes updated, and delete anything unused.
- Block PHP execution in
wp-content/uploads/through server configuration (Apache rules, or alocationblock on Nginx). - Use strong, unique passwords and two-factor authentication on every admin account.
- Add security headers. They reduce some browser-side risks but do not remove server-side backdoors.
- Keep off-server backups with enough history to restore from a compromise noticed late.
Key takeaways
- Look for PHP files in folders that should not contain any, especially uploads and cache.
- Use code searches to build a shortlist, then read the files. Never run them.
- Compare core, plugins and themes with clean copies of the same versions.
- Contain first, preserve evidence, then find the entry point before you reopen.
- Treat any "all clear" from a search or scan as one data point, not proof.
FAQ
Can a security plugin find every web shell?
No. Scanners catch known patterns and can miss custom or well-hidden code. Pair them with file comparison and good hygiene.
Is deleting the suspicious file enough?
Rarely. The file is a symptom. If you do not find how it got there, it usually returns.
Should I restore last month's backup?
Only if you are confident it predates the compromise, and you still need to fix the entry point and change credentials.