Interactive
Webshell Signal Explorer
Detection is not magic and it is not a single clever rule. It is several weak signals that only mean something together. Here are six cases, including one where the right answer is to do nothing.
Webshell Signal Explorer
Six things a scanner sees on a real host. Pick one and read why it does — or deliberately does not — trigger an action.
The classic first stage. A short file, an encoded string, one call that turns data into code.
<?php
/* thumbnail cache — do not edit */
$k = 'ZXhhbXBsZQ==';
$p = base64_decode($k);
eval($p);Signals
- heuristics · php.dynamic_eval +45
Code is built at runtime and executed —
eval()receives a variable, not a literal. Legitimate application code almost never needs this; loaders always do. - heuristics +25
Decoder immediately before the sink —
base64_decode()feeds the executing call directly. The distance between decoding and execution is the tell — not the function itself. - heuristics +20
PHP inside an upload directory —
wp-content/uploads/receives files from the internet. A.phpfile there has no legitimate reason to exist, whatever it contains. - heuristics +8
Comment mimics platform vocabulary — “do not edit” headers are cheap camouflage against a human reviewer. On its own this proves nothing; it only raises the score once other signals fire.
Three transformations in one expression. The point of the nesting is to defeat a grep for any single function name.
<?php
$s = 'nSjXpH5NmH2Wn2E=';
$x = gzinflate(base64_decode(str_rot13($s)));
$h = "\x73\x79\x73\x74\x65\x6d";
echo strlen($x);Signals
- heuristics +35
Three decoders in one expression —
str_rot13→base64_decode→gzinflateis a transformation chain, not data handling. Each function is harmless; the composition is the signal. - heuristics +24
Hex-escaped string literal — A long
\x..sequence hides a plain identifier from anything that searches for words. PHP resolves it at parse time; a reader does not. - heuristics · php.escape_density +12
Unusual density of escape sequences — Measured against the rest of the file. On its own this fires on legitimate library code too — which is exactly why it never decides alone.
- heuristics +15
File name imitates core, path does not match — The name mimics WordPress core, but no such file exists in the distribution, and its hash is not in the known-good allowlist.
A single strong-looking signal that on its own must not trigger an action. This is what report-only mode is for.
<?php
$a = 'php';
$b = 'info';
$f = $a . $b;
$f();Signals
- heuristics · php.variable_call +28
Call target assembled at runtime — The callee never appears as a literal, so no static rule can name it. Dispatchers, plugin loaders and DI containers do this legitimately every day.
- heuristics +10
Identifier split across concatenation — Splitting a name into fragments is a search-evasion pattern. In framework code it is often just readability.
Magic bytes say GIF, the body says PHP. Upload validators that check the header are satisfied; the web server is not asked.
GIF89a;
<?php echo 1; ?>Signals
- heuristics +40
PHP open tag with code inside a media file — A
<?byte pair alone is not enough — that produced false positives on ordinary JPEGs and was fixed. An explicit tag with executable code after it is. - heuristics +26
Declared type and content disagree — Type detection by magic bytes only steers priority. A file whose extension, magic bytes and content tell three different stories is inspected, never skipped as “just an image”.
- heuristics +18
Writable upload path — The file arrived where anonymous visitors can write. Combined with the tag, that answers “how did it get here” before anyone has to ask.
No code at all. Two configuration lines that make the server execute pictures and load an attacker file before every request.
# .htaccess
AddType application/x-httpd-php .png
AddHandler application/x-httpd-php .ico
; .user.ini
auto_prepend_file = /home/example/public_html/wp-content/uploads/.cache.phpSignals
- heuristics +42
Extension remapped to the PHP handler —
AddType/AddHandlerunder a document root turns inert uploads into executables. Almost no legitimate site needs this belowuploads/. - heuristics +38
`auto_prepend_file` set from inside a site — This is the same mechanism Shelltrap’s own upload gate uses, which is exactly why the product treats a site-level override as a finding rather than trusting it.
- heuristics +11
Dot-file in a public directory — The prepended file is hidden from a casual
ls. Persistence lives in the files nobody lists.
.htaccess and .user.ini writes as well as content writes, because the event mask includes attribute changes. That is why a configuration-only attack is caught by the same pipeline as a code drop.Real WordPress core code that trips a heuristic. If a scanner quarantines this, it takes the site down. The honest answer is a known-good allowlist, not a louder alarm.
<?php
// wp-includes/kses.php — unmodified WordPress core
$string = str_replace( array( '&', '<', '>' ), array( '&', '<', '>' ), $string );
$string = wp_kses_normalize_entities( $string, $context );
$string = preg_replace( '/&([^&\s;]+)(?![0-9a-z;])/', '&$1', $string );Signals
- heuristics · php.escape_density +12
High density of escaped characters — An HTML sanitiser is nothing but escape sequences. PHPMailer, SimplePie and the named-character-reference tables trip the same signal.
- hash +0
SHA-256 matches the known-good allowlist — The checksum comes from the published WordPress distribution. The allowlist answers the heuristic — but it never overrides a path-context hit such as PHP inside
uploads/.
clean. One signal, a matching known-good hash, no upload path. Nothing is reported, nothing is moved, and nobody is woken up at 03:00.Illustration, not malware: every snippet here is inert. Scores and identifiers depend on the active rule generation and are shown to explain the reasoning, not to specify the product.
Why a score and not a rule
A single signal is almost never enough. base64_decode appears in half the plugins on a busy
WordPress host. A variable function call is what every dispatcher and dependency-injection
container in the PHP world does for a living. High escape density is what an HTML sanitiser
is.
Any of those, used alone as a rule, produces a scanner that either misses obvious loaders or quarantines WordPress core. Both failure modes end with the same phone call.
So heuristics in Shelltrap score. Each signal contributes a weight, the weights add up, and the verdict compares the total against a threshold you can move per domain. Every signal that contributed is stored with the finding and shown in the panel, so a verdict is an argument you can read rather than a label you have to trust.
The signals that are not code
Two of the six cases above contain no PHP at all. A rewritten handler in .htaccess and an
auto_prepend_file line in .user.ini are configuration, and they are among the most effective
persistence mechanisms on shared hosting: they need no shell of their own and survive the
cleanup of the file everybody was looking at.
This is exactly why the watcher’s event mask includes attribute changes and why .user.ini and
.htaccess always go through the full pipeline, whatever their size. A scanner that only looks
at .php files is looking at the wrong half of the problem.
The one that must stay quiet
The last case is the important one. wp-includes/kses.php is WordPress core, unmodified, and it
trips a heuristic every time because sanitisers are made of escape sequences. So do PHPMailer,
SimplePie and the named-character-reference tables.
The honest answer to that is a known-good allowlist built from published distribution checksums — not a louder alarm and not a quieter heuristic. And the allowlist has a limit of its own: it may not override a path-context hit. A file whose hash is in WordPress core but which now sits in an uploads directory is still a finding, because how it got there is the question.
Try it against your own thinking
- How real-time detection works — where these files are seen in the first place
- How the upload gate works — how a file like the first one is refused before your application runs
- The product page — the whole pipeline, verdicts and policy model