Web Shells: What They Are and How to Find Them

By · Updated · 4 min read

Of everything that turns up during a cleanup, a web shell is the one I least like to find. It is not a piece of automated malware that does one thing on a schedule — it is a live control panel someone else can log into, sitting on your server, with whatever permissions your web server runs as. If there is a shell on the site, assume a human has been walking around in it.

What a web shell is

A web shell is a script — almost always PHP on a WordPress site — that accepts commands over HTTP and executes them on your server. The well-known families have been around for years and get reused constantly. Open one in a browser and you typically get a functioning interface: a file browser for reading, editing, uploading and deleting anything the web server can reach, a form for running shell commands, database access, and often tools for mailing out spam or scanning other machines from your server.

The important distinction from ordinary malware is intent. An injected redirect monetizes traffic automatically. A shell is an access method — somebody comes back and uses it. That changes what you should assume about what was seen and touched.

How they get there

The usual route is a file upload flaw in a plugin or theme, which is why the uploads directory is such a common location for them. Anything that accepts a file and does not properly validate what it actually is can end up storing executable PHP under a name that looks harmless. My guide on PHP files in the uploads folder covers that specific case, and Contact Form 7 is a well-documented example of the pattern.

The other routes are a stolen administrator password used to install a plugin or edit a theme file, a vulnerable plugin that permits arbitrary file writes, and a compromise elsewhere on a shared hosting account that reaches across into your site. Each leaves different traces, and your access logs are the fastest way to tell which one you are dealing with.

Finding one

Start with location, because shells are usually somewhere WordPress has no business having PHP: inside wp-content/uploads, in a directory of images or documents, in the web root under a name that mimics a core file, or inside a plugin folder whose name looks real but whose contents do not match anything you installed.

Then use timestamps. On a site that has not been updated recently, a handful of PHP files modified last Tuesday stand out sharply against thousands that have not changed in months. Sort the whole tree by modification time and read the top of the list. Finally, compare against known-good copies: WordPress core, and every plugin and theme from the repository, can be compared file-for-file against a fresh download, and anything present in your install but absent from the clean copy deserves an explanation.

Why they are hard to spot by eye

Modern shells rarely look like readable code. The payload is commonly encoded and reassembled at runtime, split across several innocuous-looking variables, or hidden inside what appears to be a legitimate file with thousands of lines of normal code and one modified function. Some are tiny — a few dozen characters that take a parameter from the request and execute it — which makes them easy to tuck into an existing file rather than a new one.

Because of that, a visual skim is not a search strategy. Look for the behavior instead: functions that execute strings or system commands, request parameters flowing straight into execution, and decoding chains that end in an evaluation. My guide on reading obfuscated code covers how to unwrap one of these safely to see what it really does.

What to assume once you find one

Assume everything the web server user can read was readable, which on most setups means your entire site, your wp-config.php and therefore your database credentials, and potentially other sites sharing that account. Assume the database was accessible, including user records and any customer data in it. Assume other files were modified, because a shell is rarely the only thing left behind.

Deleting the shell is the easy part and the smallest part. Rotate every credential the file could have exposed — database password, WordPress salts, hosting and FTP passwords, and any API keys or SMTP credentials stored in the site — and force a password reset for all users. My guide on rotating credentials after a hack lists them in order.

Cleaning up around it

Do the full pass rather than stopping at the file you found: replace core, plugins and theme from clean sources, check the database for injected content and rogue administrator accounts, and check for persistence in scheduled tasks and must-use plugins. Then close the entry route — patch what let the upload happen, and stop PHP from executing in your uploads directory so the same trick does not work twice.

If the site handles customer data or takes payments, a confirmed shell is the point at which this becomes a business decision rather than a technical chore, and telling customers after a hack is worth reading before you decide how to handle it. If you would rather have the whole thing gone through properly by someone who does this for a living, that is my malware removal service.

Common questions

Can I just delete the file and move on?

You can delete it in seconds, but that only removes one access method. Shells rarely travel alone, the credentials in your wp-config.php should be treated as exposed, and the flaw that allowed the upload is still there unless you fixed it. Deleting the file and stopping is the single most common reason a site gets hit again within the week.

How do I know if anyone actually used it?

Your access logs. Requests to the shell's path — especially POST requests, from a small number of addresses, at odd hours — show it being driven. Compare the first such request against the file's creation time to work out how long the access window was open. That timeline matters if you need to assess what data was reachable.

Will a security plugin detect a web shell?

Often, but far from always. Well-known shells match published signatures and get caught. Custom or heavily obfuscated ones, and small snippets appended to otherwise legitimate files, frequently do not. A clean scan is encouraging; it is not proof, particularly on a site where you already have other evidence of compromise.

Does this mean customer data was stolen?

It means it was reachable, which is the assumption you should work from. Whether it was actually taken is usually impossible to prove either way from a typical shared hosting log retention window. If you hold personal or payment data, treat reachable as the working standard when deciding on disclosure rather than waiting for proof that will never come.

Why is the uploads folder such a common location?

Because it is the one directory WordPress is designed to let outside input write into, and because plugins that accept file uploads do not always validate properly what they are given. It is also served directly over the web, so a file placed there is immediately reachable by URL. Blocking PHP execution in that directory removes most of the value of putting a shell there.