What a webshell is, and how to catch one in your own code
Every few weeks a new mass-exploitation campaign makes the news: a flaw in some widely deployed web platform gets weaponized, and thousands of servers are compromised in days. The 2025 SharePoint "ToolShell" wave was a textbook case, tens of thousands of internet-facing servers hit through a single vulnerability chain. In almost all of these stories, the very first thing the attacker does after getting in is drop a webshell. Understand what that file is and you understand both how the breach persists and how a defender finds it.
The one idea
A webshell is a file placed in a directory the web server will execute, that takes an attacker's input from the request and runs it as code or shell commands on the server. That is the whole thing. It is not malware in the virus sense. It is often a handful of lines. Its power comes entirely from one pattern: a value from the HTTP request flows into something that executes code. Legitimate application code almost never does that on purpose. So the pattern itself is the signature you hunt for.
Here is the shape, defanged, so you can recognize it. Do not deploy this; it exists to be detected.
import os
from flask import request
def render():
cmd = request.args.get("x") # attacker controls this
return os.popen(cmd).read() # server runs it, returns the output
A request to /render?x=whoami now runs whoami on the server and returns the answer. Swap in any command and the attacker has a remote shell through a normal-looking web page. That is why it is called a webshell.
The detector
You cannot block webshells by looking for a fixed string, attackers rename and reshuffle them endlessly. You look for the behavior: a source of untrusted input reaching an execution sink. A simple scanner encodes exactly that, plus the obfuscation trick of running a decoded blob.
import re, pathlib
SINKS = r"(?:eval|exec|os\.system|os\.popen|subprocess\.\w+|compile|__import__)"
SOURCES = r"(?:request\.(?:args|form|values|data|cookies)|\$_(?:GET|POST|REQUEST))"
OBFUSCATION = r"(?:base64\.b64decode|codecs\.decode|bytes\.fromhex)\s*\(.{0,80}?\)"
def scan(path):
src = pathlib.Path(path).read_text()
hits = []
has_sink, has_source = re.search(SINKS, src), re.search(SOURCES, src)
if has_sink and has_source:
hits.append("request input reaches a code-execution sink")
if re.search(OBFUSCATION, src) and has_sink:
hits.append("decoded blob passed to an execution sink")
return hits
Three details that matter:
- The signal is the combination, not either half. Plenty of honest code calls
subprocess. Plenty readsrequest.args. It is the two meeting in one file that is rare and worth an alert. - Obfuscation flips from innocent to suspicious in context.
base64.b64decodeis everywhere in normal code.base64.b64decodefeeding straight intoexecis a stager hiding its payload. - This is a heuristic, not a proof. It will miss a cleverly split shell and it will occasionally flag a plugin system that legitimately runs user code. It is a triage tool that turns "scan 40,000 files by hand" into "look hard at these three".
Proof: benign passes, both shells trip
Point it at a directory holding one honest image helper and two planted samples:
clean gallery.py
SUSPECT invoice_helper.py:
- request input reaches a code-execution sink
SUSPECT b64_stager.py:
- decoded blob passed to an execution sink
The image resizer is clean because nothing from the request ever reaches an execution call. The two planted files trip for exactly the reason they are dangerous, and the detector even tells you why, which is what an analyst needs to decide fast.
Where this shows up
This is the core of what commercial webshell scanners and endpoint tools do, dressed up with taint tracking that follows a request value through variables and function calls instead of matching it in one line, plus behavioral monitoring that watches for a web process suddenly spawning a shell. The same source-to-sink idea is how static analysis finds SQL injection and command injection before they ship. Learn to see the pattern and you read code the way an attacker and a defender both do.
If you want to build the taint tracker, the vulnerable server, and the exploit that drops the shell, then close the hole, that is the loop the cybersecurity track on IWTLP walks you through, offense and defense on the same code.
Sources
- Mass exploitation and webshell deployment in the 2025 SharePoint "ToolShell" campaign (CVE-2025-53770): CISA advisory on Microsoft SharePoint vulnerabilities
- Webshell detection guidance and indicators: NSA/ASD joint report, Detect and Prevent Web Shell Malware