/hunting-web-backdoors
Hunt planted webshells and backdoors across a web source tree — PHP first (also JSP, ASP, Node) — triaging a directory at scale, statically decoding obfuscation layers without ever executing the payload, finding append-infections and fake plugins, and treating known shell
$ npx -y skills add trilwu/secskills --skill hunting-web-backdoors --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/hunting-web-backdoors
Context preview
The summary Claude sees to decide when to auto-load this skill.
Hunt planted webshells and backdoors across a web source tree — PHP first (also JSP, ASP, Node) — triaging a directory at scale, statically decoding obfuscation layers without ever executing the payload, finding append-infections and fake plugins, and treating known shell
SKILL.md
hunting-web-backdoors.SKILL.mdname: hunting-web-backdoors
description: Hunt planted webshells and backdoors across a web source tree — PHP first (also JSP, ASP, Node) — triaging a directory at scale, statically decoding obfuscation layers without ever executing the payload, finding append-infections and fake plugins, and treating known shell families as leads rather than verdicts. Use when a web server is suspected compromised, cleaning a hacked WordPress/Magento/CMS site, vetting a downloaded PHP codebase or plugin for hidden malicious code, or when a file contains eval on decoded input.
verified: 2026-07-27
Hunting Web Backdoors
This is not a vulnerability audit. You are looking for code an attacker **already planted** — a webshell, a one-line eval backdoor, a fake plugin, a malicious line appended to a legitimate file. The weakness that let them in is `auditing-php-applications`' job; this skill finds what they left behind.
Two things make it hard. The malicious code is deliberately hidden — obfuscated, split, or buried in a tree of thousands of legitimate files — so a naive grep misses it. And the tells (`eval`, `base64_decode`, dynamic calls) appear in plenty of benign code, so a naive grep also drowns you in false positives. The work is the discrimination.
When to Use
- A web server is suspected or confirmed compromised and you need every shell
- Cleaning a hacked WordPress, Magento, Joomla, or custom PHP site
- Vetting a downloaded plugin, theme, or codebase before deploying it
- A file contains `eval`, `assert`, or `system` on decoded or request-derived input
- Triaging a web root full of unfamiliar PHP for planted malice
When NOT to Use
- **Finding the vulnerability that allowed the upload** — use
`auditing-php-applications`
- **Deep single-sample analysis** (a specific shell you want to fully reverse,
its C2, its capabilities) — use `analyzing-malware`
- **The broader incident** (scoping, timeline, containment) — use
`responding-to-incidents`; this skill is the source-tree sweep within it
- **Malicious dependencies / package install scripts** — use
`auditing-supply-chain`
- **Host-level persistence** (cron, systemd, LD_PRELOAD) — use
`analyzing-linux-persistence`
Never Execute to Deobfuscate
The single rule that must not bend: **the obfuscated code is the payload.** Do not run it, do not "just let PHP decode it," do not replace `eval(` with `echo(` and execute the file, do not paste it into an online sandbox that runs it. Deobfuscation is a **static** transformation:
- Decode the layers by hand or with a decode-only tool (`base64_decode`,
`gzinflate`, `gzuncompress`, `str_rot13`, hex/`\xNN`, `strrev`) applied as string operations, not as executed PHP.
- If you must automate, use a de-obfuscator that decodes without executing
(UnPHP's decoder, a Python reimplementation of the decode chain) — never the PHP interpreter on the sample.
- Replacing the final `eval(` with `file_put_contents('out.php', ...)` and
running it *is* executing the payload if any prior layer has side effects. Decode statically, then read.
"Most webshells are three or four layers of encoding around ten lines of logic" — peel to the logic, do not detonate the wrapper.
Triage a Directory at Scale
You cannot read every file. Rank candidates by cheap signals, then read the top of the list:
# 1. PHP where PHP should not be — uploads, cache, images, media dirs
find wp-content/uploads -name '*.php' -o -name '*.php[0-9]' -o -name '*.phtml' 2>/dev/null
# 2. Recently modified, clustered in time (attackers touch many files at once)
find . -name '*.php' -newermt '2026-07-01' -printf '%T+ %p\n' | sort
# 3. High entropy / long single lines — the shape of packed obfuscation
find . -name '*.php' -exec awk 'length>1000{print FILENAME": "length; nextfile}' {} \;
# 4. The dangerous-sink grep — leads, NOT verdicts (see the FP problem below)
grep -rnE '\b(eval|assert|system|passthru|shell_exec|proc_open|popen|create_function)\s*\(' --include='*.php' .
grep -rnE '(base64_decode|gzinflate|gzuncompress|str_rot13|hex2bin)\s*\(' --include='*.php' .
# 5. Request-driven code execution — the webshell core
grep -rnE '\$_(GET|POST|REQUEST|COOKIE|SERVER)\s*\[[^]]*\]\s*\(' --include='*.php' .
grep -rnE '(eval|assert|system|passthru)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)' --include='*.php' .The most reliable single lead is **variable execution of request data** — `$_POST['x']()`, `ev[a]l($_GET[...])`, `ass[e]rt($_REQUEST[...])`, `call_user_func($_GET['f'], ...)`. Legitimate code almost never dispatches a function name straight from a superglobal.
The Grep Is Not Clean Just Because It Is Empty
A source tree that returns nothing for `grep eval` can still be backdoored. Attackers defeat the obvious grep deliberately:
- **Concatenation:** `$a='ev'.'al'; $a($code);` or `('e'.'v'.'a'.'l')(...)`.
- **Variable variables / indirection:** `$f='system'; $f($_GET['c']);`,
`${'_'.'GET'}`, `$$x`.
- **Char/hex/octal escapes:** `"\x65\x76\x61\x6c"` is `eval`;
`chr(101).chr(118)...`.
- **Callback sinks:** `array_map`, `array_filter`, `usort`,
`preg_replace_callback`, `register_shutdown_function`, `ob_start` with a user-controlled callback.
- **String-eval that is not `eval`:** `assert($str)` (pre-8.0),
`preg_replace('/./e', $str, ...)` (pre-7.0), `create_function('', $str)` (pre-8.0), `mb_ereg_replace` with `e`.
- **Comment and whitespace insertion** between token pieces to break naive
regex.
So grep is a *lead generator*, not a coverage guarantee. When grep is empty and you still suspect infection, fall back to: entropy/long-line ranking, mtime clustering, and diffing against a known-good copy (below).
Append-Infections Are the Ones You Miss
The hardest infection is a backdoor **appended to or inserted into a legitimate file** — `index.php`, `wp-config.php`, a theme's `functions.php`, `vendor/` autoloaders. The file is 99% real, so it does not stand out by name, location, or entropy overall
Read more
name: hunting-web-backdoors description: Hunt planted webshells and backdoors across a web source tree — PHP first (also JSP, ASP, Node) — triaging a directory at scale, statically decoding obfuscation layers without ever executing the payload, finding append-infections and fake plugins, and treating known shell families as leads rather than verdicts. Use when a web server is suspected compromised, cleaning a hacked WordPress/Magento/CMS site, vetting a downloaded PHP codebase or plugin for hidden malicious code, or when a file contains eval on decoded input. verified: 2026-07-27
Hunting Web Backdoors
This is not a vulnerability audit. You are looking for code an attacker **already planted** — a webshell, a one-line eval backdoor, a fake plugin, a malicious line appended to a legitimate file. The weakness that let them in is `auditing-php-applications`' job; this skill finds what they left behind.
Two things make it hard. The malicious code is deliberately hidden — obfuscated, split, or buried in a tree of thousands of legitimate files — so a naive grep misses it. And the tells (`eval`, `base64_decode`, dynamic calls) appear in plenty of benign code, so a naive grep also drowns you in false positives. The work is the discrimination.
When to Use
- A web server is suspected or confirmed compromised and you need every shell
- Cleaning a hacked WordPress, Magento, Joomla, or custom PHP site
- Vetting a downloaded plugin, theme, or codebase before deploying it
- A file contains `eval`, `assert`, or `system` on decoded or request-derived input
- Triaging a web root full of unfamiliar PHP for planted malice
When NOT to Use
- **Finding the vulnerability that allowed the upload** — use
`auditing-php-applications`
- **Deep single-sample analysis** (a specific shell you want to fully reverse,
its C2, its capabilities) — use `analyzing-malware`
- **The broader incident** (scoping, timeline, containment) — use
`responding-to-incidents`; this skill is the source-tree sweep within it
- **Malicious dependencies / package install scripts** — use
`auditing-supply-chain`
- **Host-level persistence** (cron, systemd, LD_PRELOAD) — use
`analyzing-linux-persistence`
Never Execute to Deobfuscate
The single rule that must not bend: **the obfuscated code is the payload.** Do not run it, do not "just let PHP decode it," do not replace `eval(` with `echo(` and execute the file, do not paste it into an online sandbox that runs it. Deobfuscation is a **static** transformation:
- Decode the layers by hand or with a decode-only tool (`base64_decode`,
`gzinflate`, `gzuncompress`, `str_rot13`, hex/`\xNN`, `strrev`) applied as string operations, not as executed PHP.
- If you must automate, use a de-obfuscator that decodes without executing
(UnPHP's decoder, a Python reimplementation of the decode chain) — never the PHP interpreter on the sample.
- Replacing the final `eval(` with `file_put_contents('out.php', ...)` and
running it *is* executing the payload if any prior layer has side effects. Decode statically, then read.
"Most webshells are three or four layers of encoding around ten lines of logic" — peel to the logic, do not detonate the wrapper.
Triage a Directory at Scale
You cannot read every file. Rank candidates by cheap signals, then read the top of the list:
# 1. PHP where PHP should not be — uploads, cache, images, media dirs
find wp-content/uploads -name '*.php' -o -name '*.php[0-9]' -o -name '*.phtml' 2>/dev/null
# 2. Recently modified, clustered in time (attackers touch many files at once)
find . -name '*.php' -newermt '2026-07-01' -printf '%T+ %p\n' | sort
# 3. High entropy / long single lines — the shape of packed obfuscation
find . -name '*.php' -exec awk 'length>1000{print FILENAME": "length; nextfile}' {} \;
# 4. The dangerous-sink grep — leads, NOT verdicts (see the FP problem below)
grep -rnE '\b(eval|assert|system|passthru|shell_exec|proc_open|popen|create_function)\s*\(' --include='*.php' .
grep -rnE '(base64_decode|gzinflate|gzuncompress|str_rot13|hex2bin)\s*\(' --include='*.php' .
# 5. Request-driven code execution — the webshell core
grep -rnE '\$_(GET|POST|REQUEST|COOKIE|SERVER)\s*\[[^]]*\]\s*\(' --include='*.php' .
grep -rnE '(eval|assert|system|passthru)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)' --include='*.php' .The most reliable single lead is **variable execution of request data** — `$_POST['x']()`, `ev[a]l($_GET[...])`, `ass[e]rt($_REQUEST[...])`, `call_user_func($_GET['f'], ...)`. Legitimate code almost never dispatches a function name straight from a superglobal.
The Grep Is Not Clean Just Because It Is Empty
A source tree that returns nothing for `grep eval` can still be backdoored. Attackers defeat the obvious grep deliberately:
- **Concatenation:** `$a='ev'.'al'; $a($code);` or `('e'.'v'.'a'.'l')(...)`.
- **Variable variables / indirection:** `$f='system'; $f($_GET['c']);`,
`${'_'.'GET'}`, `$$x`.
- **Char/hex/octal escapes:** `"\x65\x76\x61\x6c"` is `eval`;
`chr(101).chr(118)...`.
- **Callback sinks:** `array_map`, `array_filter`, `usort`,
`preg_replace_callback`, `register_shutdown_function`, `ob_start` with a user-controlled callback.
- **String-eval that is not `eval`:** `assert($str)` (pre-8.0),
`preg_replace('/./e', $str, ...)` (pre-7.0), `create_function('', $str)` (pre-8.0), `mb_ereg_replace` with `e`.
- **Comment and whitespace insertion** between token pieces to break naive
regex.
So grep is a *lead generator*, not a coverage guarantee. When grep is empty and you still suspect infection, fall back to: entropy/long-line ranking, mtime clustering, and diffing against a known-good copy (below).
Append-Infections Are the Ones You Miss
The hardest infection is a backdoor **appended to or inserted into a legitimate file** — `index.php`, `wp-config.php`, a theme's `functions.php`, `vendor/` autoloaders. The file is 99% real, so it does not stand out by name, location, or entropy overall
**Security skills for Claude Code that encode the judgment professionals bring, not another wrapper around a scanner.** Offense, defense, and the reverse-engineering and code-audit core both sides share. Every skill is fact-checked against primary sources.

