核心在于Request类__construct函数遍历$_POST赋值导致filter、method等属性被覆盖,进而通过param→input→call_user_func链触发RCE;需POST请求、_method=__construct、filter=system及数组型参数(如get[]=whoami),版本差异影响触发条件(如5.0.13+需s=captcha或debug开启)。

ThinkPHP 5.0.x 中 __construct 触发的变量覆盖
漏洞核心在 think\Request 类的构造函数:它直接遍历 $options(即用户可控的 $_POST)并用 property_exists 检查后赋值。只要属性名存在,就能覆盖 $this->filter、$this->method、$this->get 等关键字段。
常见错误现象:500 Internal Server Error 或无响应,往往是因为没触发 _method=__construct,或目标版本不匹配(如 5.0.13+ 需要 s=captcha 或开启 debug)。
实操要点:
- 必须确保请求方法为 POST,且含
Content-Type: application/x-www-form-urlencoded -
_method=__construct是绕过 method 检查的入口,缺一不可 -
filter=system覆盖的是Request实例的filter属性,不是全局配置 - 后续 RCE 依赖
param()→input()→call_user_func链,所以get[]=whoami中的get[]必须是数组形式,否则call_user_func('system', 'whoami')不会执行
assign() + extract() 引发的模板层文件包含
当控制器中写 $this->assign(request()->get()),再调用 $this->fetch(),就可能触发模板引擎对 $this->data 的 extract() 调用。而 extract($vars, EXTR_OVERWRITE) 会把数组键名转为变量名,直接覆盖已有变量 —— 其中 $cacheFile 就是高危目标。
立即学习“PHP免费学习笔记(深入)”;
典型利用场景:攻击者传入 cacheFile=php://filter/read=convert.base64-encode/resource=/etc/passwd,在 file.php::read() 中被 include $cacheFile 加载,造成任意文件读取甚至代码执行(若配合日志文件包含)。
注意点:
- 该路径在 ThinkPHP 5.0.0–5.0.18 和 5.1.0–5.1.10 影响明确,5.1.15 仍可复现
-
assign()接收数组时不做键名校验,extract()默认行为就是EXTR_OVERWRITE,无需额外参数 - 模板路径未硬编码时,
$cacheFile可能来自template_cache配置,但攻击者可通过覆盖$this->config['cache_path']进一步控制
ThinkPHP 3.2.x 中 assign() 第一个参数可控导致的 RCE
不同于 5.x 的构造函数覆盖,3.2.x 的 assign() 方法允许第一个参数为数组且直接用于 array_merge($this->tVar, $name)。若传入 value[_filename]=./Application/Runtime/Logs/Home/21_06_30.log,后续在 ParseTemplateBehavior::run() 中经 extract() 后,$_filename 就会覆盖模板引擎内部的 $filename 变量,最终 include $_filename。
这个漏洞依赖本地文件包含(LFI),所以必须先让日志中写入 PHP 代码(例如通过报错触发 index.php?c=<?php phpinfo();?>),再用 _filename 指向该日志路径。
容易踩的坑:
- 日志路径中的日期必须与服务器当前时间严格匹配(如
21_06_30对应 2021 年 6 月 30 日),否则include失败 - TP3.2 默认关闭
display_errors,需手动开启或通过其他方式触发错误写入 -
assign()若被二次封装(如加了htmlspecialchars过滤),该链就中断,必须确认原始调用位置
为什么 parse_str() 和 import_request_variables() 在审计中不能忽略
虽然 ThinkPHP 自身不直接使用 parse_str(),但在自定义路由解析、第三方插件或开发者手写的工具函数中极常见。例如某扩展中写 parse_str($_GET['data'], $out); extract($out);,就等同于开放了一条变量覆盖通道。
import_request_variables() 已在 PHP 5.4+ 废弃,但老旧项目或兼容层代码里仍有残留,它会将 $_GET/$_POST 直接导入符号表,风险等同于 register_globals=On。
审计建议:
- 全局搜索
parse_str(、mb_parse_str(、extract(、import_request_variables(,重点关注参数是否来自用户输入 - 检查
extract()调用是否显式指定EXTR_SKIP或EXTR_IF_EXISTS;默认无参数即为EXTR_OVERWRITE - 发现
$$动态变量赋值时,必须回溯左侧变量是否可控,例如$$key = $val中$key来自$_GET即构成覆盖
变量覆盖本身不直接执行代码,但它改写了对象状态,让后续看似安全的函数调用(比如 call_user_func、include、file_get_contents)接收恶意参数。最危险的不是某个函数,而是“可控输入 → 属性覆盖 → 危险函数调用”这条链路被连通的位置 —— 它往往藏在框架默认行为和开发者习惯写法的交界处。



















