0x1 前言
周五考完试,正在准备复习的时候,无聊的时候跑去水群,然后看到有师傅丢了个payload和文档,说是thinkphp5.0.x的远程rce,于是来分析了一波。
0x2 漏洞分析
本文以thinkphp5.0.11为分析:
0x2.1 执行流程
thinkphp如何接收参数,如何获取数据这些,你可以从入口开始读
/Users/xq17/www/test2/thinkphp/thinkphp/start.php
namespace think;
// ThinkPHP 引导文件
// 加载基础文件
require __DIR__ . '/base.php';
// 执行应用
App::run()->send();//跟进APP类下run方法
/Users/xq17/www/test2/thinkphp/thinkphp/library/think/App.php
107line 跟进
public static function run(Request $request = null)
{
is_null($request) && $request = Request::instance();
......................//加载配置,初始化代码省略
if (empty($dispatch)) {
// 进行URL路由检测
$dispatch = self::routeCheck($request, $config);//跟进当前类的routeCheck
}
is_null($request) && $request = Request::instance();
(1)包含了request文件,然后Request::instance()
->$request (Request类实例)
继续向下分析:
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false;
.....................................................
// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
if ($must && false === $result) {
// 路由无效
throw new RouteNotFoundException();
}
}
跟进$result = Route::check($request , $path, $depr, $config['url_domain_deploy']);
这里传入的参数$request
对应上面说的Request
实例
public static function check($request, $url, $depr = '/', $checkDomain = false)
{
// 分隔符替换 确保路由定义使用统一的分隔符
$url = str_replace($depr, '|', $url);
...............................................
$method = strtolower($request->method());//跟进这里
// 获取当前请求类型的路由规则
$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
$request->method()
调用了Request
类的method方法,跟进
0x2.2 进入漏洞点
/Users/xq17/www/test2/thinkphp/thinkphp/library/think/Request.php
lines 503
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
} 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 = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
}
}
return $this->method;
}
参数为空,进入elseif
流程
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 = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
isset($_POST[Config::get('var_method')])
Config::get('var_method')
获取请求中的参数的值(跟进get
函数就行了) //从简分析大概理解流程就行了
直接全局搜索(省事哈)
得到Config::get('var_method')
=> _method
的值
所以说我们$_POST
_method=__construct
的值就可以继续执行下去
$this->method = strtoupper($_POST[Config::get('var_method')]); //大写赋值给$this->method
此时$this->method
= __construct
继续跟进下一条语句:
$this->{$this->method}($_POST);
这个时候就等价:
$this->__construct($_POST);
把$_POST数组作为参数传回了构造函数,跟进分析下
Lines 130
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) { //遍历判断$_POST的键值是否存在类的属性中
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}
// 保存 php://input
$this->input = file_get_contents('php://input');
}
这里我们假设重新post _method=__construct&filter=assert
到这里的流程
foreach ($options as $name => $item) {
if (property_exists($this, $name)) { //遍历判断$_POST的键值是否存在类的属性中
$this->$name = $item;
}
首先_method
属性当前类不存在 pass
但是filter
属性当前类存在进入$this->$name = $item;
//被覆盖为我们传入assert
我们可以回到Request
类定义看看filter属性是干嘛用的
全局过滤规则,熟悉tp框架应该就知道会调用这个全局规律规则去过滤pathinfo的参数值,这里我从简分析下流程
回到上面那个APP
文件
if (empty($dispatch)) {
// 进行URL路由检测
$dispatch = self::routeCheck($request, $config);//上文分析到这里
}
// 记录当前调度信息
............................
$data = self::exec($dispatch, $config);//跟进这里
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); //跟进module模块
public static function module($result, $config, $convert = null)
{
.......................
return self::invokeMethod($call, $vars);//跟进这里
}
public static function invokeMethod($method, $vars = [])
{
........................
$args = self::bindParams($reflect, $vars);//跟进这里
self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}
private static function bindParams($reflect, $vars = [])
{
if (empty($vars)) {
// 自动获取请求变量
if (Config::get('url_param_type')) {
$vars = Request::instance()->route();
} else {
$vars = Request::instance()->param();//跟进这里
}
}
...............
}
前面一连串调用其实就是工作流程而已,大概理解就行了
Request::instance()->param();
//这里就是payload执行地方了,跟进仔细分析
调用了Request类方法生成Request实例->param()
//ps这里就回到了我们之前设置全局$filter规则地方
public function param($name = '', $default = null, $filter = '')
{
..................
return $this->input($this->param, $name, $default, $filter); //$filter=assert
}
跟进input
函数
public function input($data = [], $name = '', $default = null, $filter = '')
{
...................
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);//跟进这里
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
array_walk_recursive($data, [$this, 'filterValue'], $filter);
调用了当前类下的filterValue
跟进这个函数:
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);//这里执行payload
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
$value = call_user_func($filter, $value);//这里调用了assert执行了payload
到了这里,一个payload的完整利用流程已经出来轮廓了。
0x2.3 漏洞演示
0x2.4 谈谈漏洞版本影响
这个分析和payload我测试过了5.0.10,5.0,11都是可以的,理论来说是5.0.1x通杀,只要代码是这样走的,而且这个也不要求开启debug模式,5.0.23其中一个利用payload是要求开启debug的。
这里讲下为啥5.0.23的payload没办法用到5.0.1x上,其实主要的代码区别是在于: 5.0.23版的method有server函数处理,才会有了那篇满天飞的文档接下来nb的分析(tql)
而5.0.1版本是:
很明显就没有进入$this->server()
函数的过程,对比清楚哈,别看花了。
那问题又来了,那为啥5.0.1x不能用在5.0.23上呢,其实文档作者也说了
这样到了那个调用$filter
就为空了。(因为作者文档写的很清楚这个利用我就不再进行仔细分析了)
对比差异,多了句设置$filter
的语句
说到这里,我只想说这个漏洞作者Tql。
0x3 感想
两个小时的分析,总算完成分析任务,这篇文章实在粗糙,诸多纰漏,但还是尽量从简出发,让大家对漏洞的流程有这种执行过程的观念,才能知其payload,在利用其payload,时间匆忙,明天还要考试,其实我还想找下通杀全版本的payload,从那个任意调用函数出发吧,如果有幸,坐等我的第二篇文章,最后打波广告,跟我一样的萌新想学习代码审计,可以关注我在安全客写的ECTOUCH2.0分析代码审计流程
本文始发于微信公众号(米斯特安全团队):ThinkPhp5.0.x 远程RCE简单分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论