thinkphp v5.1.37 反序列化利用链分析

0x00 前言

最近看到一篇代码审计的文章中 ,里面多次提到用thinkphp 的 反序列化利用链 来写shell 。由于之前没有对thinkphp 反序列化利用链做过系统的分析,所以决定最近对thinkphp 反序列化利用链 亲自动手来复现 分析以下。

0x01 环境搭建

我是直接在github 上下载源码来搭建环境的,看到网上也可以用composer来安装。我这里就介绍从github上拉源码来安装的方法:

需要下载 thinkphp 两部分:
https://github.com/top-think/framework/releases/tag/v5.1.37
https://github.com/top-think/think/releases/tag/v5.1.37
然后把framework 改名为 thinkphp 放到 think-5.1.37 就行了
在这里插入图片描述

0x02 漏洞分析
0x 2.0 提前说明
(1)关于 trait

利用链中的 Conversion , Relationship , Attribute 都是 trait ,而 Model 正好都 use 了他们

在这里插入图片描述
但是Model 是一个抽象类,不能直接 new ,所以需要找 继承了Model 类的子类,Pivot 类
在这里插入图片描述
所以对于 Conversion ,Relationship , Attribute 里面的 $this -> [属性名] ,最后写 poc 的时候 只要在Pivot 对象中 赋值这些属性就行了

(2)关于 think\Request 里面的input()函数利用

在这里插入图片描述
对 thinkphp 历史 rce 利用链有了解的同学对 input() 应该不陌生,都知道 think\Request 类中的 input($data = [], $name = ‘’, $default = null, $filter = ‘’) 方法 是一个不错的利用点,相当于 call_user_func($filter,$data) 。其中$filter 如果为’’ 可以通过 $this->filter 来自定义
下面先来分析以下这个input() 函数:

如果$name 为’'的话就跳过这个if:
在这里插入图片描述

getFilter() 函数,跟进:
在这里插入图片描述
filterValue() 函数,跟进:
在这里插入图片描述
看到了直接call_user_func() ,参数是$filter和$value ,而$value 正是input() 函数中的$data 参数。

所以综上分析,只要我们构造input(‘whoami’,’’,null,‘system’)就可以顺利执行命令。(特别要注意第二个参数要为 ‘’ )

这里就有个坑点
在这里插入图片描述
在还没反序列化之前就会调用一次Request的构造函数,而里面又调用了init()函数,init() 函数中会初始化config 中的一些值,其中就包含后面用到的 $this->config[‘var_ajax’] ,所以在写poc 的时候要把$this->config[‘var_ajax’]赋值为 ‘’(对应上面input() 函数中的$name 参数)。 这一步不能省去。

0x 2.1入口点

首先全局搜索 __destruct() 函数,在think\process\pipes\Windows 类中发现一个有个removeFiles() 操作:
在这里插入图片描述
跟进:
在这里插入图片描述

发现file_exists() 函数,而file_exists() 函数需要传入的参数为string 类型,而这里的$filename 又是可控的 ,那么当$filename 是一个对象的时候,就会触发他的 __tostring() 方法
(这里还存在任意文件删除漏洞)

然后我们全局搜索__tostring() 方法,
在这里插入图片描述
进入 think\model\concern\Conversion trait 中的__tostring()

调用了 toJson() 函数,跟进
在这里插入图片描述
再跟进 toArray() 函数,里面存在这么几行代码:
在这里插入图片描述

0x 2.2 关于 $relation 可控

$name 很明显是可控的,由$this->append 数组的键值,$key 是$this->append 中的 键名 ,那我们现在跟进$relation 赋值的过程,看怎$relation 是不是可控:
跟进 getRelation() 函数:

在这里插入图片描述
这里我们可以让 $key 不在 $this->relation (比如$this->relation 为空 )中,就会返回 null ,这样就进入了下面的if 中。

再看看getAttr() 函数: think\model\concern\Attribute triat
在这里插入图片描述
476行 跟进getData()函数 think\model\concern\Attribute triat
在这里插入图片描述
那么在getData() 中,我们只需要让$this->data 中 有 $key 这个键,然后让getAttr() 函数486行下面的if 判断条件都不满足(显然这个很容易实现,后面那些属性值都为空),就可以直接使 $relation = $this->data[$key] ;
那么$this->data 可控,$key 也是可控的($this->append 中的 键名),所以 $relation 也是可控的。

所以到这一步分析后,我们可以写出这一块的poc

namespace think;
abstract class Model{

	protected $append = [];
	private $data = [];

	function __construct(){
		$this->append = ["ke"=>[args]];
		$this->data = [ "ke" => Obj];
	}
}

namespace think\model;
use think\Model;
class Pavot extends Model
{
}

namespace think\process\pipes;
use think\model\Pavot;
class Windows extends Pipes{
	private $file = [] ;
	function __construct(){
		$this->file = [new Pavot()] 
	}
}
0x 2.3 \think\Requests 利用点

到上一步结束,我们可以构造 $relation 为任意 对象,$name 也可以任意构造。
在这里插入图片描述
那么我们全局搜索function __call()
在\think\Requests 类中:
在这里插入图片描述
发现,我们可以完全控制call_user_func_array 的 第一个参数,这就相当于 call_user_func_array(array(任意类,任意方法),$args) ,只不过有一点,这里把 本类对象的$this 添加到了 $args 中的第一个

对thinkphp 之前的一些rce 链有了解的话,都知道 think\Request 类中的 input 方法 是一个不错的利用点,相当于 call_user_func($filter,$data) 。其中$filter 如果为’’ 可以通过 $this->filter 来自定义
在这里插入图片描述

但是这里,我们不能直接调用Input 方法,因为 前面把 $this 放到了$args 中的第一个,就导致$args 数组第一个值是一个直接写死的对象。所以这里不能直接调用 input() 方法。我们要去寻找间接调用input() 方法的函数

0x 2.4 寻找间接调用 \think\Requests :: input() 的函数

找input() 函数的usage:
在这里插入图片描述
发现七处调用input() 函数的地方,看了看这几处调用Input的函数定义,发现第一个参数都是$name,比如下面的param() 函数,而$name 作为一个对象传入input() 函数的时候,在上图 1354行会进行一个强制转化为string ,而Requests类中又不存在 __tostring 魔术方法,会产生一个 fatal error 导致程序退出。 所以还要继续找 调用这些函数 的函数。
在这里插入图片描述
这里找 param() 函数的usage() ,直接 用 find usage 太多了,有8百多个,所以这里先看Requests.php 中使用param() 的地方,搜索 " param( " 关键字

发现在 isAjax() 函数中调用了 param() 函数
在这里插入图片描述
而且这里传入 param() 的函数是又可控的,isAjax() 第一个参数为对象的时候也不会强制转换报错。

这部分 poc 如下:

namespace think;
class Request
{
	protected $config = [
        'var_ajax'         => [''],
    ];
	protected $filter = 'system';
	protected $param = ['whoami'];
	protected $hook = ['visible'=>[$this,'isAjax']];
	
}
0x03 poc

最后将上面两部分poc 结合起来:


0x04 poc 验证

首先在index.php 中添加反序列化利用的代码
在这里插入图片描述
然后,启动thinkphp

 php think run

在这里插入图片描述
生成payload:
在这里插入图片描述

浏览器传入 payload ,成功执行命令:
在这里插入图片描述

0x05 总结

参考网上的一些 poc 来打,本地测试失败,估计是小版本的原因,所以干脆自己写了一个poc, 亲自调试过程中也遇到了一些坑点。比如一开始没注意trait,以及后来的 没有设置 protected $config = [‘var_ajax’ => ‘’,]; 等等。

利用file_exists() 函数作为跳板执行 __toString() 函数确实很妙,那么相应的要是其他函数接收的参数是String 的话,我们也可以直接传入对象,来触发__toString() 函数

在trait 中,直接use下来之后trait 中原来的属性值和属性可访问性,也都copy了下来,trait 中的$this ,就变成了 use 的 类的$this,class 也是一样(除非是访问private 属性时候的$this)。还有一点就是,trait 中 的属性不可以在use 的 类中重写,这点和class 不同。

还发现一个有趣的现象,class A 继承 class B 之后,反序列化A的对象,也会反序列化继承来自B 的属性,不过如果覆盖了B的属性,就不会反序列化父类中被覆盖的属性了。

假如A 是 trait , B use 了 A , 那么 序列化 B 对象的时候,A 中的私有属性变成了B 中的私有属性,这点class 和 trait 不同。
在这里插入图片描述
在这里插入图片描述

不说了,不说了,抬起头,发现天都黑了,告辞告辞。

参考链接:

ThinkPHP5.1.X反序列化利用链
挖掘暗藏thinkphp中的反序列利用链

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值