搜尋
首頁運維安全APACHE OFBIZ XMLRPC遠端程式碼執行漏洞實例分析

概述

研究人員報告了一個存在於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元素「」開頭。此元素包含一個子元素「something」。元素“”包含子元素“”,該子元素可以包含一個或多個“”元素。 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中,序列化程式碼在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> 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");</string>

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数据时,便会触发该漏洞。

以上是APACHE OFBIZ XMLRPC遠端程式碼執行漏洞實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:亿速云。如有侵權,請聯絡admin@php.cn刪除

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。