mcpField
H
hunting-web-backdoors

hunting-web-backdoors

github.com/trilwu安全17760 次安装12 周前更新
代码仓库

关于 hunting-web-backdoors

面向事件响应人员和网站维护者的安全技能,用于查找 PHP 网站目录中被植入的后门。它结合静态分析、分层排查和可信文件对比,而非依赖特征匹配或简单 grep。

主要功能

  1. 01按目录位置和修改时间筛选 PHP 文件
  2. 02识别混淆代码和请求驱动执行
  3. 03与可信版本对比文件
  4. 04静态解码可疑代码,不执行载荷
  5. 05命中后保全证据并继续排查

适用场景

  1. 01排查遭入侵的 WordPress 或自定义 PHP 网站
  2. 02部署前检查插件或主题
  3. 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 很少与第一个属于同一家族。

确认一个后,就假定还有更多

  1. 删除前先保留证据。 已确认的 Web shell 是证据——请依照 responding-to-incidents,先将其复制(并计算哈希)到保留位置,再将其移除。 仅删除会丢失时间线;如果漏掉入口点,攻击者可能在几小时内再次植入。
  2. 查找入口点。 Web shell 一定是通过某种方式进入的——可能是易受攻击的上传功能、 已知 CVE 的插件或被盗凭据。将此项工作交给 auditing-php-applications(源代码)或 responding-to-incidents (日志);漏洞仍然敞开的网站,即使清理过,也会在第二天早上再次感染。
  3. 继续排查。 找到一个 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 — 将已确认的家族转化为可重复使用的特征

评论

支持 Markdown