概述
研究人員報告了一個存在于apache ofbiz中的反序列化漏洞。這個漏洞是由多個java反序列化問題所導致的,當代碼在處理發送至/webtools/control/xmlrpc的請求時,便有可能觸發該漏洞。未經認證的遠程攻擊者將能夠通過發送精心構造的惡意請求來觸發并利用該漏洞,并實現任意代碼執行。
漏洞分析
Apache OFBiz是一個開源的企業資源規劃(ERP)系統,它提供了一系列企業應用程序來幫助企業自動化實現很多業務流程。它包含了一個能提供常見數據模型和業務進程的框架,企業內所有的應用程序都需要采用這個框架來使用常見數據、邏輯和業務處理組件。除了框架本身之外,Apache OFBiz還提供了包括會計(合同協議、票據、供應商管理、總賬)、資產維護、項目分類、產品管理、設備管理、倉庫管理系統(WMS)、制造執行/制造運營管理(MES/MOM)和訂單處理等功能,除此之外,還實現了庫存管理、自動庫存補充、內容管理系統(CMS)、人力資源(HR)、人員和團隊管理、項目管理、銷售人員自動化、工作量管理、電子銷售點(ePOS)、電子商務(電子商務)和scrum(開發)等多種功能。
Apache OFBiz使用了一系列開源技術和標準,比如Java、JavaEE、XML和SOAP。
超文本傳輸協議是一種請求/響應協議,該協議在 RFC 7230-7237中有詳細描述。請求由客戶端設備發送至服務器,服務器接收并處理請求后,會將響應發送回客戶端。一個HTTP請求由請求內容、各種Header、空行和可選消息體組成:
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表示空格字符。參數將以鍵值對的形式通過Request- URI或message-body由客戶端傳遞給服務器,具體將取決于Method和Content-Type頭中定義的參數。比如說在下面的HTTP請求樣本中,有一個名為“param”的參數,其值為“1”,使用的是POST方法:
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()方法,它們會在對象序列化或反序列化時被調用。這些方法能夠在序列化和反序列化過程中通過修改代碼來實現自定義行為。
XML-RPC
XML-RPC是一個遠程過程調用(RPC)協議,它使用XML對其調用進行編碼,并使用HTTP作為傳輸機制。它是一種標準規范,并提供了現成的實現方式,允許運行在不同的操作系統和環境中。在在XML-RPC中,客戶機通過向實現XML-RPC并接收HTTP響應的服務器發送HTTP請求來執行RPC。
每個XML-RPC請求都以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中,序列化代碼在org.apache.xmlrpc.parser.SerializableParser這個Java類中實現。
但是,Apache OFBiz中存在一個不安全的反序列化漏洞,這個漏洞是由于OFBiz被配置為在發送到“/webtools/control/xmlrpc”URL時使用XML-RPC攔截和轉換HTTP主體中的XML數據所導致的。發送到此端點的請求最初由org.apache.ofbiz.webapp.control.RequestHandler這個Java類來處理,它確定的URL的映射方式。接下來,org.apache.ofbiz.webapp.event.XmlRpcEventHandler類將調用execute()方法,XML解析首先需要通過XMLReader類來調用parse()方法,而這個方法需要在org.apache.ofbiz.webapp.event.XmlRpcEventHandler類的getRequest()方法中調用。
XML-RPC請求中的元素將會在下列類中被解析:
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版本,并添加了相應的注釋。
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 injection "); } 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 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 } }
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數據時,便會觸發該漏洞。