写点什么

浅析安全反序列化漏洞

  • 2022 年 1 月 20 日
  • 本文字数:4277 字

    阅读完需:约 14 分钟

一、概述

对于这个漏洞的学习,有几个大体的思路,一是向大佬学习;二是找到可以利用的点,再不断构造合理的对象向这个点靠近;三是把几个子链分别构造好再连起来。


另外,查到的资料说这个漏洞并不是适用于所有的 TP5.0.X 版本,这里为了不产生歧义,只记成 TP5.0.24 版本。

二、分析

(一)环境搭建

Windows、PHPStudy(PHP5.6)、ThinkPHP5.0.24;


首先安装此版本的 ThinkPHP,


composer create-project topthink/think tp 5.0.24
复制代码


index controller 改为


class Index{    public function index()    {        @unserialize($_GET['k']);    }}
复制代码


调用栈如下,


File.php:160, think\cache\driver\File->set()Memcache.php:94, think\session\driver\Memcache->write()Output.php:154, think\console\Output->write()Output.php:143, think\console\Output->writeln()Output.php:124, think\console\Output->block()Output.php:212, call_user_func_array()Output.php:212, think\console\Output->__call()Model.php:912, think\console\Output->getAttr()Model.php:912, think\Model->toArray()Model.php:936, think\Model->toJson()Model.php:2267, think\Model->__toString()Windows.php:163, file_exists()Windows.php:163, think\process\pipes\Windows->removeFiles()Windows.php:59, think\process\pipes\Windows->__destruct()App.php:8, app\index\controller\Index->index()
复制代码

(二)分析与调试

分为几个部分进行分析。


1.从 Windows.removeFiles()到 Model.toArray()


从 Windows 的__destruct 开始看,



【一>所有资源获取<一】1、电子书籍(白帽子)2、安全大厂内部视频 3、100 份 src 文档 4、常见安全面试题 5、ctf 大赛经典题目解析 6、全套工具包 7、应急响应笔记 8、网络安全学习路线


close()中没有可以直接使用的点,removeFiless()中有 file_exists()的判断,如果 file_exists()的参数是一个对象,则会调用其对应类的__toString()方法,



think\Model 中有很好的__toString()方法,其中调用了 toJson,toJson 中调用了 toArray。



这里需要注意,Model 是个抽象类,没法直接从抽象类创建对象,它的意义在于被扩展,



经过搜索,可以选择 Pivot 与 Merge 两个类作为具体实现,选择哪一个对主干不会产生太大影响,但会对 PoC 的写法产生细微差别(个别字段的 public 与 private 属性),这里先选择 Merge。


进入 Model.toArray 之后,就该考虑如何进一步调用到Output.__call()


2.从 Model.toArray 到 Output.__call()


从 Model 的 toArray 中,我们可以看到有若干个疑似可以利用的点,选择不同的触发点会对整个流程产生一定影响。此处选择$item[$key] = $value ? $value->getAttr($attr) : null;这一点。



接下来遇到一个很现实的问题,就是找到了这个点之后,怎么在 $value 有意义的前提下,保证程序可以在前面的若干行代码中不出错,进而顺利正常抵达这里。


(1)进入 else


首先最高级的 if 和内部的大 else 前面的 if 和 elseif 较为直接,要求如下:name 不可为数组,this->append 数组,而这个数组是我们可控的,则对应的name 也都可控,故这里可以较为容易的走过。这一部分可以简单走过,重要的是 else 之中的隐藏的阻碍。


(2)parseName 与进入 if (method_exists(relation))


首先跟进看 Loader::parseName($name, 1, false);



功能是字符串命名风格转换,应该来讲没什么影响,name 直接划等号。


接下来看如何使 method_exists(relation)这一条件为 true,这要求我们找一个 Model 或其子类中存在的方法名,由此可见,上一小步中的 $this->append 不能随意构造,应该放入一个合适的方法名。


(3)getRelationData



可以看到,在判断relation 函数后,便会调用这个函数,赋值给this->getRelationData(value,$value 将在$item[$key] = $value ? $value->getAttr($attr) : null;这关键一行中发挥作用,这就要求我们找到一个具有优良性质的函数来串起这一切。


Model 类中的 getError 方法具有逻辑简单、返回值直接可控的性质,没有比这个函数还好用的函数了,也许选别的也可以,但选这个准没错。



我们将this->error 的值可根据需要再行设定。


跟进看下一行的$value = $this->getRelationData($modelRelation);



有一个比较严苛的条件if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent))


首先modelRelation->isSelfRelation()的返回值不为空,最后还有一个类型比较。


若想研究 isSelfRelation()和 getModel()这两个方法,得先保证modelRelation 实质上是this->error 的值便依此而定。



经过搜索发现,Relation 类中有这两个方法的定义,但是和 Model 一样,Relation 是个抽象类,我们需要寻找它的可用的子类。


看看另外两个函数。



其一返回 Model 的 selfRelation,由于要加上逻辑否,这里设置为 false 即可;其二最终返回了this->query->model 和this->error 要是 Relation 的子类,this->parent 决定这两点。


(4)method_exists($modelRelation, ‘getBindAttr’)


这算得上是这一子链的较为关键的一部分,



要想进入红色的触发点,必须保证bindAttr 要具有一定的性质。


上面提到this->error,是我们可控的,且要是 Relation 的子类,



我们搜索发现,只有 OnetoOne 类中定义了 getBindAttr()方法,



且查看后发现,此类是继承了 Relation 的抽象类,可以满足此一步和上一步的要求。


接下来要做的是找一个合适的 OneToOne 的子类。全局搜索,找到如下两个,



经过查看,这两个的相关属性和方法几乎一模一样,选择哪个应该都可以,只是 PoC 写的时候有所区分,这里选择 HasOne。


由刚才的分析,我们知道,这一段中有两个举足轻重的变量:modelRelation)和this->error 上,至于触发点处的this->value的取值,要考虑到后续的利用,根据大佬的指点,我们后面要使用Output.__call(),所以this->value 必须为 Output 对象,



且由 getRelationData()知,$this->parent 应为 Output 对象。


因为还要过get_class($modelRelation->getModel()) == get_class($this->parent)这一道障碍,this->parent 一样,是 Output 对象。class Query 中天然带着this->query,其 $model 属性应为 Output 对象。



(5)$modelRelation->getBindAttr()


接下来要研究的是modelRelation->getBindAttr();这一句。



跟进可见,这一函数功能也是非常简单直接,我们可以直接控制 $this->bindAttr 为我们想要的对象。



首先this->data 直接为空即可保证能够进入 else 即可,这里重要的是attr 是何值,只要能够触发 Output->getAttr($attr)就会去调用__call 方法,就能进入下面的环节了。


3.从 Output.__call()到 Memcached.write()


跟进 Output.__call(),



在简单构造 $this->styles 后,就能进入call_user_func_array([Output, 'block'], $args),调用 block 方法,



接下来能否成功利用,就看 Output 的 $this->handle->write 了。


首先 Output.handle 是可控的,我们要找一个带有合适的 write()方法的类。


搜索一下,发现了数个定义有 write 的类,



从中有没有比较合适的呢,经过学习可知,我们可以选择 think\session\driver\Memcached 类。Memcached 的 write() 中调用了 this->handler 可控,再加上 think\cache\driver\File 中的 set() 方法可以写文件,故而选择 Memcached。


$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;$result = file_put_contents($filename, $data);
复制代码


4.Memcached.write()->File.set()


但当仔细观察时,我们会发现这个思路其实是存疑的:此处可以写入文件不假,但是否可以写入 webshell 有待考证,因为这里的 $data 未必是可控的。



向上找data 的值为定值 true,这样一来,如果没有进一步方案,写入 shell 的想法就会失败。


这时如果我们接着往下看,会看到一句$this->setTagItem($filename)的代码,其将文件名name),



可以看到此处将value,又调用了一次 $this->set()方法,也就是说,如果文件名构造得当,还是有机会写入 webshell 的。


而 $filename 的值是由 getCacheKey 产生的,跟进之。



可以看到,这个函数相对友好,有一个 md5 哈希操作和一个与$this->options[‘path’]的拼接操作,在一定条件下,最终的$filename基本上是可控可知的。但是要注意到$data是由$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;这样一句产生的,应想办法解决掉这个exit(),另外如果是在 Windows 环境下写文件还需要将一些特殊符号转化掉,这里就涉及到一个较为深入的知识点了。令$this->options['path']'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/../a.php',



如此,在第一次进入 set()时,我们生成了一个文件名为


php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/../a.phpc9a7cef7c410e3ea21c4287f392fd663.php


内容为


<?php //000000000000 exit();?> b:1;的文件,


第二次由 setTagItem($filename)进入 set(),才是生成 webshell 的关键一步。


文件名为$this->options['path'].md5('tag_' . md5($this->tag)).'.php'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/../a.php3b58a9545013e88c7186db11bb158c44.php的文件,其内容为<?php //000000000000 exit();?> s:154:"php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/../a.phpc9a7cef7c410e3ea21c4287f392fd663.php";


在过滤器的作用下,将写出 webshell。



最后发送个想要的 POST 包即可。



用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
浅析安全反序列化漏洞