首页 >> 网络安全 >>漏洞分析 >> CVE-2020-9496:通过对不可信数据进行反序列化在APACHE OFBIZ XMLRPC中进行RCE
详细内容

CVE-2020-9496:通过对不可信数据进行反序列化在APACHE OFBIZ XMLRPC中进行RCE

时间:2020-09-27     作者:趋势科技研究团队【转载】   阅读

在趋势科技漏洞研究服务漏洞报告的摘录中,趋势科技研究团队的John Simpson和Dusan Stevanovic详细介绍了Apache OFBiz套件中的最新代码执行漏洞。该漏洞最初是由GitHub Security Lab团队的Alvaro Munoz发现并报告的。以下是其写作的一部分,涵盖了CVE-2020-9496,并做了一些最小的修改。


Apache OFBiz中报告了一个不安全的反序列化漏洞。此漏洞是由于处理发送到的请求时的Java序列化问题引起的/webtools/control/xmlrpc。远程未经身份验证的攻击者可以通过发送精心设计的请求来利用此漏洞。成功的利用将导致任意代码执行。


漏洞

Apache OFBiz是一个开源企业资源计划(ERP)系统。它提供了一套企业应用程序,可以集成和自动化企业的许多业务流程。它包括一个提供通用数据模型和一组业务流程的框架。所有应用程序都使用通用数据,逻辑和流程组件在此框架上构建。除了框架本身之外,Apache OFBiz还提供功能,包括会计(协议,发票,供应商管理,总帐),资产维护,目录和产品管理,设施和仓库管理系统(WMS),制造执行/制造运营管理(MES / MOM),订单处理,库存管理,自动补货等,内容管理系统(CMS),人力资源(HR),人员和小组管理,


Apache OFBiz使用一组开源技术和标准,例如Java,Java EE,XML和SOAP。


超文本传输协议是RFC 7230-7237等中描述的请求/响应协议。客户端计算机将请求发送到服务器,服务器再将响应发送回客户端。HTTP请求由一个请求行,各种标头,一个空行和一个可选的消息正文组成:

Request = Request-Line headers CRLF [message-body]    
Request-Line = Method SP Request-URI SP HTTP-Version CRLF    
Headers = *[Header]    
Header = Field-Name “:” Field-Value CRLF

CRLF 表示新的行序列回车(CR),后跟换行(LF)。 SP 代表空格字符。可以根据指定的Method 和  Content-Type 标头将参数  作为名称/值对的形式从客户端传递到服务器,方法可以是  Request - URI 或  message-body。例如,一个简单的HTTP请求使用POST 方法传递一个名为“ param”且值为“ 1”的参数,  可能类似于: 

POST /my_webapp/mypage.htm HTTP/1.1    
Host: www.myhost.com    
Content-Type: application/x-www-form-urlencoded    
Content-Length: 7    
param=1

Java序列化 

Java允许对象的序列化,使它们可以表示为紧凑且可移植的字节流。然后,可以通过网络传输此字节流,并将其反序列化以供接收servlet或applet使用。以下示例说明了如何对类进行序列化,然后再将其提取: 

public static void main(String args[]) throws Exception{    
//This is the object we're going to serialize.     
MyObject1 myObj = new MyObject1();    
MyObject2 myObj2 = new MyObject2();    
myObj2.name = "calc";    
myObj.test = myObj2;    
//We'll write the serialized data to a file "object.ser"     
FileOutputStream fos = new FileOutputStream("object.ser");    
ObjectOutputStream os = new ObjectOutputStream(fos);    
os.writeObject(myObj);    
os.close();    
//Read the serialized data back in from the file "object.ser"    
FileInputStream fis = new FileInputStream("object.ser");    
ObjectInputStream ois = new ObjectInputStream(fis);    
//Read the object from the data stream, and convert it back to a String    
MyObject1 objectFromDisk = (MyObject1)ois.readObject();    
ois.close();    
}

所有可序列化的Java对象都实现  Serializable 或  Externalizable 接口。此接口强制执行writeObject() / writeExternal() 和  readObject() / readExternal() 方法,分别在序列化和反序列化对象时调用这些方法。可以修改这些方法以在Java对象的序列化和反序列化期间实现自定义行为。 


XML-RPC 

XML-RPC是一种远程过程调用(RPC)协议,该协议使用XML对其调用进行编码,并使用HTTP作为传输机制。它是一个规范和一组实现,允许在不同操作系统上运行,在不同环境中运行的软件通过Internet进行过程调用。在XML-RPC中,客户端通过将HTTP请求发送到实现XML-RPC并接收HTTP响应的服务器来执行RPC。 


每个XML-RPC请求都以XML元素“ <methodCall> </ methodCall>”开头。该元素包含单个子元素“ <methodName> something </ methodName>”。元素“ <methodName>”包含子元素“ <params>”,该子元素可以包含一个或多个“ <param>”元素。param XML元素可以包含许多数据类型。 


常见数据类型将转换为与XML等效的示例值,如下所示。数组编码的  示例如下: 

<array>    
<data>    
<value><i4>1404</i4></value>    
<value><string>Something here</string></value>    
<value><i4>1</i4></value>    
</data>    
</array>

编码各种原语的示例如下: 

<boolean>1</boolean>    
<double>-12.53</double>    
<int>42</int>

字符串编码的示例如下: 

<string>Hello world!</string>

结构编码的示例如下: 

<struct>    
<member>    
<name>foo</name>    
<value><i4>1</i4></value>    
</member>    
<member>    
<name>bar</name>    
<value><i4>2</i4></value>    
</member>    
</struct>

序列化的数据通过使用““和”XML元素。在Apache OFBiz中,代码在Java类中反序列化org.apache.xmlrpc.parser.SerializableParser。


Apache OFBiz中存在一个不安全的反序列化漏洞。此漏洞是由于OFBiz被配置为在发送到“ / webtools / control / xmlrpc” URL时使用XML-RPC来拦截和转换HTTP正文中的XML数据。发送给该端点的请求最初由Java类处理,该类org.apache.ofbiz.webapp.control.RequestHandler确定URL的映射。接下来,execute()在org.apache.ofbiz.webapp.event.XmlRpcEventHandler类中调用该方法。XML解析通过parse()从getRequest()方法org.apache.ofbiz.webapp.event.XmlRpcEventHandler类调用XMLReader类中的方法开始。


XML-RPC请求中的元素在以下类中进行解析:           

org.apache.xmlrpc.parser.XmlRpcRequestParser

          org.apache.xmlrpc.parser.RecursiveTypeParserImpl

          org.apache.xmlrpc.parser.MapParser


内容的不安全反序列化 XML元素发生在类的getResult()方法中org.apache.xmlrpc.parser.SerializableParser。


远程未经身份验证的攻击者可以通过在HTTP请求的正文中发送包含精心制作的XML有效负载的恶意HTTP请求来利用此漏洞。由于OFBiz使用易受攻击的Apache Commons BeanUtils库和Apache ROME库版本,因此攻击者可以使用ysererial小工具以XML格式制作恶意有效载荷。成功利用此漏洞可能导致用户在运行应用程序的上下文中执行任意代码。


源代码演练

以下代码段摘自Apache OFBiz版本17.12.03。趋势科技已添加了一些注释。

在org.apache.ofbiz.webapp.control.RequestHandler中:

public void doRequest(HttpServletRequest request, HttpServletResponse response, String chain,    
GenericValue userLogin, Delegator delegator) throws RequestHandlerException,    
RequestHandlerExceptionAllowExternalRequests {    
ConfigXMLReader.RequestResponse eventReturnBasedRequestResponse;    
if (!this.hostHeadersAllowed.contains(request.getServerName())) {    
Debug.logError("Domain " + request.getServerName() + " not accepted to prevent host header injection ", module);    
throw new RequestHandlerException("Domain " + request.getServerName() + " not accepted to prevent host header in
jection ");    
}    
boolean throwRequestHandlerExceptionOnMissingLocalRequest = EntityUtilProperties.propertyValueEqualsIgnoreCase("
requestHandler", "throwRequestHandlerExceptionOnMissingLocalRequest", "Y", delegator);    
long startTime = System.currentTimeMillis();    
HttpSession session = request.getSession();    
ConfigXMLReader.ControllerConfig controllerConfig = getControllerConfig();    
Map<String, ConfigXMLReader.RequestMap> requestMapMap = null;    
String statusCodeString = null;    
try {    
requestMapMap = controllerConfig.getRequestMapMap();    
statusCodeString = controllerConfig.getStatusCode();    
} catch (WebAppConfigurationException e) {    
Debug.logError((Throwable)e, "Exception thrown while parsing controller.xml file: ", module);    
throw new RequestHandlerException(e);    
}    
if (UtilValidate.isEmpty(statusCodeString))    
statusCodeString = this.defaultStatusCodeString;    
String cname = UtilHttp.getApplicationName(request);    
String defaultRequestUri = getRequestUri(request.getPathInfo());    
if (request.getAttribute("targetRequestUri") == null)    
if (request.getSession().getAttribute("_PREVIOUS_REQUEST_") != null) {    
request.setAttribute("targetRequestUri", request.getSession().getAttribute("_PREVIOUS_REQUEST_"));    
} else {    
request.setAttribute("targetRequestUri", "/" + defaultRequestUri);    
}    
String overrideViewUri = getOverrideViewUri(request.getPathInfo());    
String requestMissingErrorMessage = "Unknown request [" + defaultRequestUri + "]; this request does not exist or 
cannot be called directly.";    
ConfigXMLReader.RequestMap requestMap = null;    
if (defaultRequestUri != null)    
//get the mapping for the URI     
requestMap = requestMapMap.get(defaultRequestUri);    
if (requestMap == null) {    
String defaultRequest;    
//[...truncated for readability.....]     
ConfigXMLReader.RequestResponse nextRequestResponse = null;    
if (eventReturn == null && requestMap.event != null    
&& requestMap.event.type != null    
&& requestMap.event.path != null    
&& requestMap.event.invoke != null)    
try {    
long eventStartTime = System.currentTimeMillis();    
//call XmlRpcEventHandler     
eventReturn = runEvent(request, response, requestMap.event, requestMap, "request");

在  org.apache.ofbiz.webapp.event.XmlRpcEventHandler中: 

public void execute(XmlRpcStreamRequestConfig pConfig, ServerStreamConnection pConnection) throws XmlRpcException {    
try {    
ByteArrayOutputStream baos;    
OutputStream initialStream;    
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;    
}    
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 informatio
n"));    
}    
}    
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");    
}    
}    
}    
protected XmlRpcRequest getRequest(final XmlRpcStreamRequestConfig pConfig, InputStream pStream) throws XmlRpcExc
eption {    
final XmlRpcRequestParser parser =     
new XmlRpcRequestParser((XmlRpcStreamConfig)pConfig, getTypeFactory());    
XMLReader xr = SAXParsers.newXMLReader();    
xr.setContentHandler((ContentHandler)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);    
//the parsing of XML in the HTTP body starts in this function     
xr.parse(new InputSource(pStream));    
//truncated    
}    
}

在  org.apache.xmlrpc.parser.XmlRpcRequestParser中: 

public void endElement(String pURI, String pLocalName, String pQName) throws SAXException {    
//XML-RPC parsing happens here     
switch(--level) {    
case 0:     
break;    
case 1:     
if (inMethodName) {    
if ("".equals(pURI) && "methodName".equals(pLocalName)) {    
if (methodName == null) {    
methodName = "";    
}    
} else {    
throw new SAXParseException("Expected /methodName, got " + new QName(pURI, pLocalName), getDocumentLocator());    
}    
inMethodName = false;    
} else if (!"".equals(pURI) || !"params".equals(pLocalName)) {    
throw new SAXParseException("Expected /params, got " + new QName(pURI, pLocalName), getDocumentLocator());    
}    
break;    
case 2:     
if (!"".equals(pURI) || !"param".equals(pLocalName)) {    
throw new SAXParseException("Expected /param, got " + new QName(pURI, pLocalName), getDocumentLocator());    
}    
break;    
case 3:     
if (!"".equals(pURI) || !"value".equals(pLocalName)) {    
throw new SAXParseException("Expected /value, got " + new QName(pURI, pLocalName), getDocumentLocator());    
}    
endValueTag();    
break;    
default:     
super.endElement(pURI, pLocalName, pQName);    
break;    
}    
}

在  org.apache.xmlrpc.parser.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);    
//insecure deserialization happens here     
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);    
}    
}    
}

要触发此漏洞,攻击者会将包含XML格式的精心设计序列化对象的HTTP请求发送到受影响的目标。服务器反序列化XML数据时触发此漏洞。 


结论

Apache在   今年初修复了该错误。根据他们的文章,该漏洞的根本原因是“ webtools(xmlrpc和ping)中有2个与xmlrpc相关的命令(sic)不使用身份验证,因为它们容易受到不安全的反序列化的攻击”。Apache OFBiz的用户如果还没有安装,肯定应该应用补丁。


特别感谢趋势科技研究团队的John Simpson和Dusan Stevanovic提供了对此漏洞的详尽分析。有关趋势科技研究服务的概述,请访问  http://go.trendmicro.com/tis/。


威胁研究团队将来还会提供其他重要的漏洞分析报告。在此之前,请跟随ZDI  团队  获取最新的利用技术和安全补丁。

原文地址:https://www.thezdi.com/blog/2020/9/14/cve-2020-9496-rce-in-apache-ofbiz-xmlrpc-via-deserialization-of-untrusted-data

点击图片直接加群
更多
ots网络logo

OTS网络安全门户主要提供网络信息安全教程、文章、工具,让更多的小伙伴加入我们的社区一起学习。

技术支持: 建站ABC | 管理登录