← All posts

How to Detect a Webshell on a Linux Server

Aswad Gul · 2026-07-23 · 8 min read

A webshell is a small script an attacker drops into your web root that gives them command execution through a normal HTTP request. It is quiet, it survives reboots, and it usually outlives the vulnerability that let it in.

That last part is what people underestimate. You patch the plugin that was exploited, congratulate yourself, and the attacker still has a shell sitting in your uploads directory because patching the entry point does nothing about what came through it.

Key Takeaways

  • A webshell is not a process and not a port. It only exists as a file, and it only runs when requested, which is why process and network monitoring miss it entirely.
  • Manual detection works for what is already there, using recently-modified-file searches plus grep for the function calls webshells depend on.
  • The reliable signal is a combination: a recently changed file, in a directory that should only contain uploads, containing an obfuscated call.
  • Manual scans do not catch the next one. File integrity monitoring on the web root does.
  • Finding a webshell means the entry point is still open until you find it separately. Removing the file is not remediation.

Table of Contents

Why webshells are hard to spot

Most webshells are a few lines long and sit in a directory full of legitimate files with similar names. They do not open a new port, they do not run as a separate process, and they do not appear in your auth logs, the traffic looks like ordinary web requests.

That combination defeats most of what people have running. Process monitoring sees php-fpm or apache2, which are supposed to be there. Network monitoring sees inbound HTTPS on 443, which is supposed to be there. Authentication logging sees nothing at all, because no login happened. The request that triggers a webshell is indistinguishable in shape from the request that loads your homepage.

Why they are usually the second stage

A webshell is rarely the initial compromise. It is what gets installed once something else has already succeeded, most often a file upload that does not validate what it accepts, a vulnerable dependency in a CMS or its plugins, or a path traversal that allows a write outside the intended directory.

The attacker's reasoning is simple: whatever they exploited to get in might be patched tomorrow. A file they control in your web root persists across that. It is the cheapest, most durable form of access available on a web server, which is why it shows up so consistently.

Why obfuscation makes grep unreliable on its own

The naive search looks for eval(. Anyone writing a webshell in the last decade knows that, so the payload arrives base64-encoded, or split across string concatenations, or assembled from an array of character codes, or hidden in what looks like a legitimate configuration array in the middle of a plausible file.

This does not make grep useless. It means grep is one input rather than the answer, and that the more valuable signal is structural: this file did not exist last week.

Manual detection

Start with recently modified files in your web root:

find /var/www -type f -name "*.php" -mtime -7 -ls

Then look for the function calls webshells rely on:

grep -rniE "eval\(|base64_decode\(|shell_exec\(|passthru\(|system\(" /var/www --include=*.php

Legitimate code does use some of these, so expect false positives. The useful signal is the combination: a recently changed file, in an upload directory, containing an obfuscated call.

Two further checks worth running, because they catch what the first two miss:

# PHP anywhere it has no business being
find /var/www -type f -name "*.php" -path "*upload*" -o -name "*.php" -path "*cache*" 2>/dev/null

# files owned by the web server user rather than your deploy user
find /var/www -type f -user www-data -name "*.php" -ls 2>/dev/null

That second one is often the highest-yield check on the list. Your application files should be owned by whatever account deploys them. A PHP file owned by www-data usually means the web server process wrote it, and the web server process has no legitimate reason to be writing PHP.

Reading the web server logs

The file tells you a webshell exists. The access log tells you whether it has been used, and often how it arrived.

# successful requests to PHP files in directories that should be static
grep -E "GET|POST" /var/log/nginx/access.log | grep -E "upload|cache|tmp|assets" | grep "\.php"

# POST requests returning 200 to unusual paths
awk '$6 ~ /POST/ && $9 == 200 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

What you are looking for is a path that receives requests from one or two addresses and nothing else, often with POST bodies, often at odd intervals. Legitimate application endpoints get traffic from many sources; a webshell gets traffic from its owner.

If you find the path, the first request to it in the log is approximately when the file was planted, and the requests immediately before that often show the exploitation attempt that put it there.

What actually works long-term

Manual scans catch what is already there. They do not catch the next one. Three things do:

The web root is close to an ideal target for integrity monitoring, for a reason worth stating: it should only change when you deploy. That gives it a naturally low change rate, which means alerts on it are rare and worth reading. Contrast that with monitoring /var wholesale, which produces constant noise and gets muted within a fortnight.

The practical configuration is to watch the web root and exclude the specific paths your application genuinely writes to at runtime, rather than the reverse. If you cannot enumerate those paths, that is worth knowing on its own.

After you find one

Removing the file is the easiest step and the least important one.

Preserve it first. Copy it somewhere off the server before deleting. It tells you what the attacker was doing, and if this turns into a disclosure conversation you will want the artefact rather than a memory of it.

Find the entry point. A webshell did not appear by itself. Check upload handling, check the versions of everything in your dependency tree against known vulnerabilities using the approach in how to track CVEs for the packages installed on your Linux servers, and check whether anything else in the web root was written at the same timestamp.

Assume it was used. Look for what the attacker did next: new users, added SSH keys, changed cron entries, new systemd units. The specifics are in detecting privilege escalation on a Linux server, and the broader sweep is how to tell if your Linux server has been compromised.

Check for the payload. On small business servers the most common thing to find next is a cryptominer, since that is how stolen compute gets monetised, see how to detect a cryptominer on a Linux server.

Rotate credentials. Anything readable from the web root, which usually means database passwords and API keys in your application's configuration or environment files.

That is the loop SecAI runs continuously: hash, compare, flag, and let an AI pipeline validate before a human is paged. Where this fits among the other controls worth running is set out in the guide to Linux server security software.

Frequently Asked Questions

Will antivirus find a webshell? Sometimes, for known samples. Webshells are trivially modified and frequently custom, so signature scanning alone is unreliable. A new or changed file in a directory that should be static is the stronger signal.

Can a webshell exist without a compromised application? It needs some write path into the web root. Usually that is an application flaw, but it can also be leaked FTP or deploy credentials, or a shared hosting account compromised elsewhere.

Does a webshell run as root? Normally it runs as the web server user, which is intentionally limited. That is still enough to read your configuration files and application data, and it is a starting point for escalation rather than the end state.

How often should I scan the web root? Scanning on a schedule is weaker than monitoring for change continuously. If you are scanning manually, weekly is a reasonable floor, but the gap between scans is the attacker's working window.

My web root has hundreds of PHP files. Where do I start? Sort by modification time and start with anything changed since your last deploy. Then check files owned by the web server user. Those two filters remove almost everything legitimate.

On a server hosting WordPress this is the most common foothold, and server-level WordPress security explains why a plugin cannot reliably detect it from inside the application.

The automated detection, including the AI screening that keeps legitimate plugin code out of the alert stream, is described under Linux malware detection.

SecAI monitors Linux servers for exactly these threats automatically.