ThinkPHP多语言漏洞可导致RCE:因lang参数未校验直接拼接进include路径,攻击者通过目录穿越(如lang=../../../../usr/local/lib/php/pearcmd)包含pearcmd.php等系统脚本,结合config-create命令写入并执行恶意PHP文件。

ThinkPHP多语言功能本身不执行代码,但语言参数若未经校验直接拼接进 include 路径,就会触发文件包含型命令注入——这是真实发生过的高危漏洞(如 pearcmd.php 利用链)。
language参数为什么能导致任意代码执行
问题出在动态加载语言包时的路径拼接逻辑:$langFile = $this->app->getLangPath() . $langSet . '.php'; include $langFile;。如果 $langSet 来自用户输入(如 GET?lang=zh-cn),攻击者可传入 lang=../../../../public/pearcmd,最终执行服务器上已存在的 PHP 工具脚本。
- 该漏洞在 ThinkPHP 5.x 早期版本中被广泛利用,核心是未对
lang参数做白名单限制 - 不是所有多语言调用都危险:只有手动拼接路径 +
include/require的写法才触发;框架内置的Lang::get()是安全的 - 常见错误现象:
Warning: include(...): failed to open stream日志里出现大量带../的路径,或访问/index.php?lang=../../../../etc/passwd%00返回系统文件内容
input('lang') 必须加正则白名单,不能只靠后缀过滤
仅检查是否以 .php 结尾或用 basename() 去掉路径分隔符远远不够——basename('a/../../etc/passwd') 仍返回 passwd,而 lang=en_US 和 lang=en%5fUS(URL 编码下划线)可能绕过简单字符串匹配。
- 推荐正则:
/^[a-z]{2}(_[A-Z]{2})?$/,严格匹配en、zh_CN、pt_BR等标准语言标识 - 必须在中间件或控制器入口处拦截,不能等进到 Lang 类再处理——因为
include前就该把非法值拒之门外 - 最大长度限制为 10 字符,防止超长 payload 触发其他解析异常
- 禁止任何点号(
.)、斜杠(/)、空格、Unicode 控制字符
绝对路径校验比 basename() 更可靠
即使参数通过了正则,也要在实际加载前做路径合法性验证。ThinkPHP 的 getLangPath() 返回的是相对路径,拼接后可能脱离预期目录。
立即学习“PHP免费学习笔记(深入)”;
- 用
realpath()获取真实路径,再用str_starts_with()判断是否落在lang/目录内:if (!str_starts_with(realpath($langFile), realpath($this->app->getLangPath()))) { throw new HttpException(403); } - 不要用
dirname(__FILE__)拼接,避免因工作目录变化导致校验失效 - 生产环境应禁用
allow_url_include,防止lang=php://filter/...类协议攻击
删掉 pearcmd.php 不等于漏洞修复
很多团队发现被攻击后第一反应是删 pearcmd.php,但这只是堵住一个利用点。只要语言参数校验缺失,攻击者仍可上传恶意 PHP 文件,或利用其他已存在工具(如 phpunit、composer 自带的调试脚本)达成 RCE。
真正要做的,是切断「用户可控输入 → 动态文件包含」这条链。路径白名单 + 绝对路径校验 + 生产环境清理工具文件,三者缺一不可。最容易被忽略的是:路由变量(如 lang/:set)走的是 Route 解析路径,不经过 Request 过滤,必须单独加校验逻辑。



















