Researchers have reported a deserialization vulnerability in Apache OFBiz. This vulnerability is caused by multiple Java deserialization issues and may be triggered when the code handles requests sent to /webtools/control/xmlrpc. An unauthenticated, remote attacker could trigger and exploit this vulnerability and achieve arbitrary code execution by sending a crafted malicious request.
Apache OFBiz is an open source enterprise resource planning (ERP) system that provides a series of enterprise applications to help enterprises automate many business processes. It includes a framework that provides a common data model and business process that all applications within the enterprise need to use to use common data, logic, and business processing components. In addition to the framework itself, Apache OFBiz also provides services including accounting (contract agreements, invoices, supplier management, general ledger), asset maintenance, item classification, product management, equipment management, warehouse management system (WMS), manufacturing execution/manufacturing operations Management (MES/MOM) and order processing functions, in addition to inventory management, automatic inventory replenishment, content management system (CMS), human resources (HR), people and team management, project management, sales force Various features such as automation, workload management, electronic point of sale (ePOS), e-commerce (e-commerce) and scrum (development).
Apache OFBiz uses a range of open source technologies and standards, such as Java, JavaEE, XML and SOAP.
Hypertext Transfer Protocol is a request/response protocol that is described in detail in RFC 7230-7237. A request is sent from the client device to the server, and after the server receives and processes the request, it sends a response back to the client. An HTTP request consists of request content, various headers, blank lines and optional message bodies:
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 represents a new line sequence carriage return (CR), followed by a line feed (LF), and SP represents a space character. Parameters will be passed from the client to the server in the form of key-value pairs through the Request-URI or message-body, depending on the parameters defined in the Method and Content-Type headers. For example, in the following HTTP request sample, there is a parameter named "param" with a value of "1" and the POST method is used:
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 Supports serialization operations on objects so that they can be represented as a compact and portable byte stream. This byte stream can then be transmitted over the network and deserialized for use by the receiving servlet or applet. The following example demonstrates how to serialize a class and subsequently extract data:
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(); }
All Java objects need to be serialized through the Serializable or Externalizable interface, which implements writeObject()/writeExternal () and readObject()/readExternal() methods, which will be called when the object is serialized or deserialized. These methods enable custom behavior by modifying the code during serialization and deserialization.
XML-RPC is a remote procedure call (RPC) protocol that uses XML to encode its calls and HTTP as the transport mechanism. It is a standard specification and provides ready-made implementations that allow running on different operating systems and environments. In XML-RPC, a client performs RPC by sending an HTTP request to a server that implements XML-RPC and receives an HTTP response.
Each XML-RPC request begins with the XML element "
As shown in the following example, common data types can be converted into corresponding XML types:
1404 Something here 1
Encoding examples of various primitives are as follows:
1 -12.53 42
String The coding example of Implemented in the Java class org.apache.xmlrpc.parser.SerializableParser.
However, there is an insecure deserialization vulnerability in Apache OFBiz. This vulnerability is due to OFBiz being configured to use XML-RPC to intercept and transform HTTP when sent to the "/webtools/control/xmlrpc" URL. Caused by XML data in the body. Requests sent to this endpoint are initially handled by the org.apache.ofbiz.webapp.control.RequestHandler Java class, which determines how the URL is mapped. Next, the org.apache.ofbiz.webapp.event.XmlRpcEventHandler class will call the execute() method. XML parsing first needs to call the parse() method through the XMLReader class, and this method needs to be in org.apache.ofbiz.webapp.event. Called in the getRequest() method of the .XmlRpcEventHandler class.
Elements in XML-RPC requests will be parsed in the following classes:
org.apache.xmlrpc.parser.XmlRpcRequestParser org.apache.xmlrpc.parser.RecursiveTypeParserImpl org.apache.xmlrpc.parser.MapParser
不安全的序列化问题存在于org.apache.xmlrpc.parser.SerializableParser类的getResult()方法之中。一个未经身份验证的远程攻击者可以利用该漏洞来发送包含了定制XML Payload的恶意HTTP请求。由于OFBiz使用了存在漏洞的Apache Commons BeanUtils库和Apache ROME库,攻击者将能够使用ysoserial工具以XML格式来构建恶意Payload。该漏洞的成功利用将导致攻击者在目标应用程序中实现任意代码执行。
下列代码段取自Apache OFBiz v17.12.03版本,并添加了相应的注释。
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 injection "); } boolean throwRequestHandlerExceptionOnMissingLocalRequest = EntityUtilProperties.propertyValueEqualsIgnoreCase("requestHandler", "throwRequestHandlerExceptionOnMissingLocalRequest", "Y", delegator); long startTime = System.currentTimeMillis(); HttpSession session = request.getSession(); ConfigXMLReader.ControllerConfig controllerConfig = getControllerConfig(); MaprequestMapMap = 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");
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 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"); } } } protected XmlRpcRequest getRequest(final XmlRpcStreamRequestConfig pConfig, InputStream pStream) throws XmlRpcException { 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 } }
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; } }
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数据时,便会触发该漏洞。
The above is the detailed content of APACHE OFBIZ XMLRPC Remote Code Execution Vulnerability Example Analysis. For more information, please follow other related articles on the PHP Chinese website!