关于 hunting-web-backdoors
面向事件响应人员和网站维护者的安全技能,用于查找 PHP 网站目录中被植入的后门。它结合静态分析、分层排查和可信文件对比,而非依赖特征匹配或简单 grep。
主要功能
- 01按目录位置和修改时间筛选 PHP 文件
- 02识别混淆代码和请求驱动执行
- 03与可信版本对比文件
- 04静态解码可疑代码,不执行载荷
- 05命中后保全证据并继续排查
适用场景
- 01排查遭入侵的 WordPress 或自定义 PHP 网站
- 02部署前检查插件或主题
- 03调查陌生网站目录中的可疑代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trilwu/ secskills --skill hunting-web-backdoors安装后在 Claude Code 中输入/hunting-web-backdoors确认已加载。
详细介绍
搜寻 Web 后门
这不是漏洞审计。你要寻找的是攻击者已经植入的代码——Webshell、一行式 eval 后门、伪造插件,或追加到合法文件中的恶意代码。让攻击者得以入侵的弱点属于 auditing-php-applications 的工作;这项技能用于发现他们留下的东西。
有两点让这项工作很难。恶意代码会被刻意隐藏——经过混淆、拆分,或埋在成千上万个合法文件组成的目录树中——所以简单的 grep 会漏掉它。另一方面,eval、base64_decode、动态调用等线索也会出现在大量无害代码中,因此简单的 grep 还会带来大量误报。关键在于甄别。
适用场景
- 怀疑或确认 Web 服务器已遭入侵,需要找出所有 Webshell
- 清理遭入侵的 WordPress、Magento、Joomla 或自定义 PHP 网站
- 部署前检查下载的插件、主题或代码库
- 文件中包含
eval、assert或system,且输入来自解码内容或请求数据 - 排查充斥着陌生 PHP 文件、疑似被植入恶意代码的 Web 根目录
不适用场景
- 查找导致文件上传的漏洞——使用
auditing-php-applications - 深入分析单个样本(想要完整逆向某个特定 Webshell,分析其 C2 和功能)——使用
analyzing-malware - 更广泛的事件响应(范围界定、时间线、遏制)——使用
responding-to-incidents;这项技能用于其中的源代码树排查 - 恶意依赖项或软件包安装脚本——使用
auditing-supply-chain - 主机级持久化(cron、systemd、LD_PRELOAD)——使用
analyzing-linux-persistence
切勿通过执行代码来反混淆
有一条绝不可违背的规则:混淆代码本身就是载荷。不要运行它,不要“让 PHP 解码一下就好”,不要把 eval( 替换为 echo( 后执行文件,也不要把代码粘贴到会运行它的在线沙箱中。反混淆应当是静态转换:
- 手动剥离各层,或使用仅解码工具(
base64_decode、gzinflate、gzuncompress、str_rot13、十六进制/\xNN、strrev),将其作为字符串操作处理,而不是作为 PHP 执行。 - 如果必须自动化,请使用不会执行代码的反混淆工具(UnPHP 的解码器、用 Python 重新实现解码链)——绝不能用 PHP 解释器运行样本。
- 将最终的
eval(替换为file_put_contents('out.php', ...)后再运行,仍然是在执行载荷,因为前面的任何一层都可能产生副作用。应当静态解码,然后阅读。
“大多数 Webshell 都是用三四层编码包裹着十来行逻辑”——剥开外层直到看到逻辑,切勿引爆外壳。
大规模排查目录
你无法逐个阅读所有文件。先按低成本信号对候选文件排序,再阅读列表靠前的文件:
# 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' .
最可靠的单一线索是对请求数据进行变量执行——$_POST['x']()、ev[a]l($_GET[...])、
ass[e]rt($_REQUEST[...])、call_user_func($_GET['f'], ...)。合法代码几乎不会直接从超全局变量中分派函数名。
Grep 没有结果,不代表代码干净
即使对源代码树运行 grep eval 一无所获,也仍可能存在后门。
攻击者会刻意绕过显而易见的 grep:
- 拼接:
$a='ev'.'al'; $a($code);或('e'.'v'.'a'.'l')(...)。 - 可变变量/间接调用:
$f='system'; $f($_GET['c']);、${'_'.'GET'}、$$x。 - 字符/十六进制/八进制转义:
"\x65\x76\x61\x6c"就是eval;chr(101).chr(118)...。 - 回调执行点:
array_map、array_filter、usort、preg_replace_callback、register_shutdown_function、 使用用户可控回调的ob_start。 - 并非
eval的字符串求值:assert($str)(PHP 8.0 之前)、preg_replace('/./e', $str, ...)(PHP 7.0 之前)、create_function('', $str)(PHP 8.0 之前)、带e的mb_ereg_replace。 - 在代码片段之间插入注释和空白,以绕过简单的正则表达式。
因此,grep 是线索生成器,不能保证覆盖全部情况。若 grep 没有结果, 但你仍怀疑存在感染,请改用:熵值/长行排序、mtime 聚类,以及与已知干净副本 进行差异比较(见下文)。
追加式感染最容易漏掉
最难发现的感染,是将后门追加到或插入到合法文件中,例如 index.php、
wp-config.php、主题的 functions.php、vendor/ 中的自动加载器。文件有 99%
都是真实代码,因此从文件名、位置或整体熵值来看都不显眼。应通过比较而非检查来查找:
# Diff the tree against a pristine copy of the same CMS/plugin version
# (download the exact version from wordpress.org / the vendor)
diff -rq ./site /known-good/wordpress-6.x/
# WordPress core/plugin integrity via checksums
wp core verify-checksums; wp plugin verify-checksums --all
# git/composer/npm: what changed vs the committed/locked state
git status --porcelain; git diff
如果没有已知干净的基准副本,以下是可疑迹象:原本正常的文件在闭合
?> 之后还有代码;配置文件顶部夹着一行孤立的 eval/base64_decode;
或者 <?php ... ?> 代码块的风格与周围文件格格不入。
版本限制的迹象能为代码断代
某个已移除语言特性的存在能说明一些情况:
- 带
/e修饰符的preg_replace——已在 PHP 7.0 中移除。它的存在说明 后门针对 PHP 5.x,或早于该版本。 - 将
create_function()和字符串参数形式的assert()用于代码执行—— 已在 PHP 8.0 中移除。旧式 Web shell 中很常见;在 PHP 8 服务上它们会报错, 而这本身就能提示代码是在何时植入的。
已知家族只能提供线索,不能作为定论
特征码能加快初步筛查,但不能取代代码审查:
本节中的 IOC 字符串已用方括号拆开——
ev[a]l是eval,ass[e]rt是assert。如果按原样写出,它们会匹配Backdoor:PHP/Chopper, 这份文件作为一种防御性排查技能,在下载时会被隔离。 上面的 grep 模式保持原样,可以安全复制。请参阅仓库 README 中的 “Antivirus false positives”。
- China Chopper——一个单行代码:
<?php @ev[a]l($_POST['pass']);?>(或assert),通常不到 30 字节。它非常短,容易漏掉,也容易被追加。 - WSO、b374k、c99、r57、indoxploit——功能齐全的面板;可将其横幅字符串、 特征函数名或身份验证 Cookie 名称作为线索进行搜索。
- Weevely——会生成经过混淆、由密码保护且每次构建都不同的 Web shell; 特征匹配效果有限,因此应依靠请求驱动执行和代码混淆等迹象。
命中特征码只能确认存在某个 Web shell,不能确认已经全部找到——攻击者可能植入多个, 而第二个 Web shell 很少与第一个属于同一家族。
确认一个后,就假定还有更多
- 删除前先保留证据。 已确认的 Web shell 是证据——请依照
responding-to-incidents,先将其复制(并计算哈希)到保留位置,再将其移除。 仅删除会丢失时间线;如果漏掉入口点,攻击者可能在几小时内再次植入。 - 查找入口点。 Web shell 一定是通过某种方式进入的——可能是易受攻击的上传功能、
已知 CVE 的插件或被盗凭据。将此项工作交给
auditing-php-applications(源代码)或responding-to-incidents(日志);漏洞仍然敞开的网站,即使清理过,也会在第二天早上再次感染。 - 继续排查。 找到一个 Web shell,意味着应提高对整个代码树的警惕,而不是结案。 移除明显的 Web shell 后,再次运行全面排查。
应当驳斥的自我辩解
- “搜索
eval没有结果,所以整个目录都干净。” 拼接、十六进制转义和回调执行点都能绕过这种检查。搜索结果为空不代表覆盖全面——应改用熵值、修改时间和与已知良好版本的差异进行检查。 - “我直接运行一下,看看它会解码出什么。” 这就是在执行载荷。始终进行静态解码。
- “它用了
eval,所以是后门。” 模板引擎、缓存和框架都会正当地使用eval。关键在于是否对已解码或源自请求的输入使用eval,而不是eval本身。 - “它被混淆得很厉害,所以肯定是恶意的。” 商业插件也会发布混淆后的版本(ionCube、Zend Guard、许可机制)。混淆会增加疑点,但不足以定罪。应解码并阅读逻辑。
- “我找到了一个 Webshell,也删掉了,搞定。” 你找到的只是一个 Webshell。先保留它,再找到入口点并重新扫描——攻击者会设置冗余。
- “这个文件是 CMS 的核心文件,肯定安全。” 恰恰是因为你会这么想,攻击者才会在这些文件中追加感染。应通过校验和进行验证。
- “没有匹配到已知 Webshell 特征。” Weevely 和自定义 Webshell 不会匹配任何特征。特征只能提供线索;由请求驱动的执行、混淆等行为才是证据。
阅读外部资料
以 Markdown 格式获取公开的安全公告、规范和供应商报告:
curl -sL "https://defuddle.md/<url>" # scheme in the path is optional
这会剔除页面上的冗余内容——对于以文字为主的页面,令牌数量大约减少 78%——并返回全文而非摘要,因此你可以搜索内容,并相信没有搜索结果这一结论。
它有三种不适用的情况。应原样获取 JSON 和 API 响应,因为可读性提取会破坏结构化数据。应直接获取经过身份验证的页面或由 JavaScript 渲染的页面,因为该方式会以匿名身份获取内容。切勿通过它访问对手的基础设施(钓鱼链接、C2、恶意软件托管)、客户所有的主机或交战 URL——请求会离开你的计算机并发送给第三方;对于正在运行的对手基础设施,这还会提醒操作者。
有些网站会拦截提取器并返回错误内容,而不是页面内容——例如 freedesktop.org 返回的 {"error":"Failed to fetch: 418 I'm a teapot"}。这表示获取请求遭到拒绝,而不是源内容不存在。在据此得出任何结论之前,应直接重新获取该 URL。
参考资料
auditing-php-applications— 允许 Webshell 入侵的漏洞analyzing-malware— 对单个已恢复的 Webshell、其 C2 和功能进行深入逆向分析responding-to-incidents— 证据处理、范围界定,以及本次扫描所属的更广泛事件响应工作analyzing-linux-persistence— Web 根目录之外的主机级持久化writing-yara-rules— 将已确认的家族转化为可重复使用的特征