Thinkphp 多语言 RCE
七月份的时候挖到这个洞,卖给了国内一个项目,如今距离修复的 commit 已经过去了将近三个月,在这里公开漏洞细节
影响范围
Thinkphp,v6.0.1~v6.0.13,v5.0.x,v5.1.x
fofa指纹
header="think_lang"
简单描述
如果 Thinkphp 程序开启了多语言功能,那就可以通过 get、header、cookie 等位置传入参数,实现目录穿越+文件包含,通过 pearcmd 文件包含这个 trick 即可实现 RCE。
攻击条件
- 开启多语言功能
thinkphp6 ,打开多语言功能
https://www.kancloud.cn/manual/thinkphp6_0/1037637
app/middleware.php
:
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
\think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];
thinkphp5 ,打开多语言功能
https://static.kancloud.cn/manual/thinkphp5/118132
config/app.php
application/config.php
'lang_switch_on' => true
测试环境搭建
这里以 thinkphp 6.0.12 为例
官方下载代码:
https://github.com/top-think/think
root@ubuntu:/var/www/#git clone https://github.com/top-think/think.git think_git
root@ubuntu:/var/www/#cd think_git
root@ubuntu:/var/www/think_git#git checkout v6.0.12
更改 composer.json
,安装 v6.0.12
:
"require": {
"php": ">=7.2.5",
"topthink/framework": "6.0.12",
"topthink/think-orm": "^2.0"
},
root@ubuntu:/var/www/think_git#composer install
然后打开多语言功能:
app/middleware.php
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
\think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];
启动 docker compose :
version: "3.3" # optional since v1.27.0
services:
web:
image: php:7.4-apache
ports:
- "8888:80"
volumes:
- /var/www/think_git:/var/www/html
进行攻击
header 、cookie 、query string 都可以作为 payload 的传入点,进行目录穿越 + pearcmd 文件包含,可以写 webshell :
# exp
pearcmd 文件包含这个 trick ,可以参考 p 牛的文章:https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html#0x06-pearcmdphp
漏洞分析、调试
thinkphp 6
调试环境:windows ,php7.3,thinkphp6.0.12
打开多语言中间件:
访问:
http://127.0.0.1:82/?lang=../../../../../public/index
每个 middleware 的 handle()
函数都会被调用,这里断在 LoadLangPack.php
的 handle()
,直接在最开头调用 $langset = $this->detect($request);
:
跟进这个 detect()
,可以看到依次排查了 GET["lang"]
、HEADER["think-lang"]
、COOKIE["think_lang"]
,并且将其不做任何过滤,直接赋值给了 $langSet
:
然后默认情况下,即 allow_lang_list
这个配置为空,$langSet
被赋值给 $range
,而 $range
被返回:
回到 handle()
,如果返回的 $langset
不等于默认的 langset ,即 zh-cn
,那么就会调用 $this->lang->switchLangSet($langset)
,正是在这里面实现了 文件包含:
跟进 switchLangSet()
,可以看到调用了 $this->load()
,而传入的参数直接拼接而成,本例中传入的最终结果是 D:\var\www\think6\vendor\topthink\framework\src\lang\../../../../../index.php
:
跟进这个 load()
,可以看到直接将传入的参数作为文件名,先判断文件在不在,如果在就传入 parse()
中,进行文件包含:
跟进 parse()
,可以看到进行了文件包含:
既然可以通过目录穿越实现任意 php 文件的包含,那么用 pearcmd 文件包含这个 trick ,就能 RCE 了
thinkphp 5
调试环境:windows ,php7.3,thinkphp5.1.41
打开多语言中间件:
访问
http://127.0.0.1:81/?lang=../../../../../public/index
调用了 Lang.php
中的 detect()
,包含的文件名可以来自 get 、cookie:
然后进行文件包含:
修复漏洞
官方已完成修复:
https://github.com/top-think/framework/commit/c4acb8b4001b98a0078eda25840d33e295a7f099