Thinkphp5_x(反序列化)
版权申明:本文为原创文章,转载请注明原文出处
Thinkphp5_x(反序列化)
Thinkphp5_x(反序列化)
5.0.x反序列化
参考链接:ThinkPHP v5.0.24 反序列化 - seizer-zyx - 博客园 (cnblogs.com)
环境
PHP5.6+Linux+ThinkPHP5.0.24
thinkphp5.0.24下载
1 | composer create-project --prefer-dist topthink/think=5.1.24 tpdemo |
修改composer.json文件
1 | "require": { |
同目录下执行composer update
测试代码
1 |
|
代码分析
入口:thinkphp/library/think/process/pipes/Windows.php中的__destruct()函数

此处我们要利用的函数是 removeFiles(),查看该函数

此处的 $this->files 是可控的,也代表
$filename 是可控的,而此处的 file_exists()
方法是判断 $filename 所指定的文件是否存在,如果
$filename是一个类,就会调用该类的
__tostring()方法,此处的思路就是找
__tostring() 方法有漏洞的类
此处找的类就是model类,位于thinkphp/library/think/Model.php,但是此处的model是抽象类,因此需要找到其子类
抽象类是一种不能直接实例化的类,它主要用于定义接口和共享代码。在面向对象编程中,抽象类一般用作其他类的基类,其目的是让子类继承它,并且必须实现抽象类中定义的抽象方法。因此,抽象类不能直接被实例化,而是需要子类去继承并实现它的抽象方法后才能被实例化。

找子类的方式就是全局搜索 extends model 的类,此处选择
pivot 类,到此为止,我们的exp初步构造如下

到此为止,我们的exp初步构造如下
1 | namespace think\process\pipes; |
接下来进入 model.php 文件,在model抽象类中寻找
__tostring() 方法,如下所示

调用了 toJson() 方法

调用了 toArray() 方法,此处的 toArray()
方法是可见的长,重点是那些变量是我们可控的,特别是调用变量的函数的时候,此处选择的漏洞入口就是912行的
$value ,选择它的原因是 $value
本身是可控的,可以将 $value 构造成一个没有
getAttr() 方法且在 __call()
方法中存在漏洞的类
1 | public function toArray() |
第一步是想办法能够执行到912行,简单写这个if函数就是如下
1 | if (!empty($this->append)) { |
第一个是 $this->append 不为空,由于
$this->append 可控,该条件可实现
接着是 $this->append 里单个值不能是数组,不能有
. ,由于 $this->append
可控,该条件可实现
然后是 $relation
变量的获取,此处使用了parseName函数,此处的type为1,也就是将C风格转换为Java风格,对正常字符串没有影响,因此此处的
$relation 就是 $name ,也就是
$this->append 变量
具体来说,当
type参数为 1 时,函数会执行以下操作:
- 使用正则表达式
'/_([a-zA-Z])/'将下划线后的字母转换为大写字母。- 使用
preg_replace_callback函数将匹配到的字符转换为大写字母。- 最后,根据
$ucfirst参数决定是否将结果的首字母大写或小写,并返回结果。

接着是判断本类是否存在 $relation
函数,这个比较好办,因为 $this->append
变量本身就是可控的,因此 $relation 变量也是可控的
继续往下分析, $modelRelation 是执行
$relation
函数的结果,要想能进入下面的if语句,就必须保证返回的结果也是一个类,并且这个类有getBindAttr方法,由于此处并没有使用
$value 变量,因此先不对 $value
变量进行分析

通过全局搜索可以找到定义该函数的地方,位于
thinkphp/library/think/model/relation/OneToOne.php

该函数定义在一个抽象类中,因此用同样的方法寻找其子类

这里选择HasOne类

因此前面的 $modelRelation
我们就知道要返回什么了,就是返回HasOne类,因此我们需要找到Model类中的可控返回变量的函数
此处选择的是 getError() 函数,因为此处的
$this->error 是可控的,因此只需构造
$this->error 为HasOne类就行了, $relation
变量就是 getError() 函数,也就是
$this->append() 的值应该是
['getError()']

继续分析,接着就执行了getBindAttr方法,进入OneToOne类查看该方法

如下所示,此处的 $this->bindAttr 也是可控的

我们要执行到912的条件是 !isset($this->data[$key])
,这里的 $this->data 也是可控的,因此此处
$bindAttr 的值不需要急着定,只要保持
$this->data 为空就行了
然后就是 $value
的值不能是False,并且根据前面的分析可知, $value
应该是一个没有 getAttr() 函数并且 __call()
函数存在漏洞的类,这里选择构造的类就是Output类,位于thinkphp/library/think/console/Output.php
$value 的值通过Model类的getRelationData函数返回

此处必须要满足第一个if语句,因为 $modelRelation
我们已经确定了,而OneToOne类是没有getRelation方法的

此处有三个条件需要同时满足:
$this->parent该变量本身可控,易满足
!$modelRelation->isSelfRelation()进入该函数,该方法属于抽象Relation类,位于
thinkphp/library/think/model/Relation.php
因为确保上面的
$this->selfRelation为false就行了,该变量同样是可控的get_class($modelRelation->getModel()) == get_class($this->parent)这个等于的含义是getModel返回值类型和$this->parent相同
先看getModel函数,还是位于抽象Relation类中

进入此处Query类的getModel函数

此处的
$this->model函数也是可控的而
$this->parent也是可控的,因此这个很好满足要求
综合以上,构造的exp应该如下
1 | namespace think\model; |
接着查看Output类的__call方法,这里的 $method 参数就是
getAttr , $args 是Model类中
$bindAttr 变量。也就是上面exp所写的
[studentYang] ,接着检查 $this->styles
数组中是否存在变量 $method 的值,而
$this->styles 是可控的,因此假设能够成功进入该循环,接着
array_unshift($args, $method) 将变量 $method
插入到数组 $args 的开头,那 $args 就变成了
[getAttr,studentYang]
,然后调用block方法,将args参数传递到该方法

如下所示,block方法会写入后面的messages,后面的messages通过前面的变量传递应该是
<getAttr>studentYang</getAttr>

然后查看writeln方法,这里的 $messages 就是
<getAttr>studentYang</getAttr>

type应该为0

接着调用了handle类指定的write方法,这里的参数:
$messages 是
<getAttr>studentYang</getAttr> ,
$newline 是true, $type 是0,而
$this->handle
是可控的,这里选择Memcache.php中的write方法,位于
thinkphp/library/think/session/driver/Memcache.php

由此的exp应该增加如下
1 | namespace think\console; |
接着查看Memcache类的write方法,这里的 $sessId 应该就是
$messages 的内容,$sessData 就是
$newline 的内容,这里的 $this->handle
是可控的,因此需要查找set函数有漏洞的类,这里选择了File.php的set方法进行利用,位于
thinkphp/library/think/cache/driver/File.php

至此exp增加以下内容
1 | namespace think\session\driver; |
如下所示,这里有file_put_contents方法,说不定有漏洞

一步步进行分析,首先确定传入的变量是什么, $name 是
<getAttr>studentYang</getAttr>
,$value
是true,而后面的内容中filename和name相关,而data只和value相关,因此此处的file_put_contents函数不好利用,但是下面的setTagItem函数可利用,该函数位于driver类
这里的 $name 是变量 $filename ,这里的
this->tag
是可控的,想办法让value等于name就可以成功执行我们的文件生成函数了

现在分析filename会变成什么,在set函数中,主要进行了以下操作

而这个函数如下,这里的 this->options
也是可控的,我们想办法不对name进行额外修改,让返回的$filename = $this->options['path'] . $name . '.php';,故$filename前部分内容可控,最后返回的filename就是
path+md5(name)+.php

接着在setTagItem函数中,让最后的set方法指向File.php的set方法
最后增加的exp如下
1 | namespace think\cache\driver; |
其中,path的构造原理如下
1 | $code = '<?php echo shell_exec("calc");?>'; |
综上所述,整条链子逻辑如下
1 | Windows类的__destruct()-->removeFiles()-->Model类的__tostring()-->toJson()-->toArray()-->Output类的__call()-->block()-->writeln()-->write()-->Memcache类的write()-->File类的set()-->Driver类的setTagItem()-->File类的set()-->file_put_contents写入shell |
最后执行效果如图所示

Thinkphp5_x(反序列化)
install_url to use ShareThis. Please set it in _config.yml.



