研究者は、Apache OFBiz にデシリアライゼーションの脆弱性を報告しました。この脆弱性は複数の Java 逆シリアル化の問題によって引き起こされ、コードが /webtools/control/xmlrpc に送信されたリクエストを処理するときに引き起こされる可能性があります。認証されていないリモート攻撃者がこの脆弱性をトリガーして悪用し、細工した悪意のあるリクエストを送信することで任意のコードを実行する可能性があります。
Apache OFBiz は、企業の多くのビジネス プロセスの自動化を支援する一連のエンタープライズ アプリケーションを提供するオープン ソースのエンタープライズ リソース プランニング (ERP) システムです。これには、企業内のすべてのアプリケーションが共通のデータ、ロジック、ビジネス処理コンポーネントを使用するために必要な共通のデータ モデルとビジネス プロセスを提供するフレームワークが含まれています。 Apache OFBizは、フレームワーク自体に加えて、会計(契約書、請求書、サプライヤー管理、総勘定元帳)、資産保守、品目分類、製品管理、設備管理、倉庫管理システム(WMS)、製造実行/製造などのサービスも提供します。運用管理(MES/MOM)機能や受注処理機能に加え、在庫管理、自動在庫補充、コンテンツ管理システム(CMS)、人事(HR)、人材・チーム管理、プロジェクト管理、営業部隊自動化などの多彩な機能、ワークロード管理、電子販売時点情報管理 (ePOS)、電子商取引 (電子商取引)、スクラム (開発)。
Apache OFBiz は、Java、JavaEE、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 またはメッセージ本文を介してキーと値のペアの形式でクライアントからサーバーに渡されます。たとえば、次の HTTP リクエストのサンプルでは、値が「1」の「param」という名前のパラメータがあり、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 シリアル化をサポートします。オブジェクトに対する操作を実行して、オブジェクトをコンパクトでポータブルなバイト ストリームとして表現できるようにし、このバイト ストリームをネットワーク経由で送信し、受信側のサーブレットまたはアプレットで使用できるように逆シリアル化することができます。次の例は、クラスをシリアル化してからデータを抽出する方法を示しています。
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 オブジェクトは、writeObject()/writeInitial () および readObject()/readExternal を実装する Serializable または Externalizable インターフェイスを通じてシリアル化する必要があります。 () メソッド。オブジェクトがシリアル化または逆シリアル化されるときに呼び出されます。これらのメソッドは、シリアル化および逆シリアル化中にコードを変更することでカスタム動作を有効にします。
XML-RPC は、XML を使用して呼び出しをエンコードし、HTTP をトランスポート メカニズムとして使用するリモート プロシージャ コール (RPC) プロトコルです。これは標準仕様であり、さまざまなオペレーティング システムや環境での実行を可能にする既製の実装を提供します。 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 コーディングJava クラス org.apache.xmlrpc.parser.SerializableParser に実装された例。
ただし、Apache OFBiz には安全でない逆シリアル化の脆弱性が存在します。この脆弱性は、OFBiz が「/webtools/control/xmlrpc」 URL に送信される HTTP を傍受して変換するために XML-RPC を使用するように設定されていることが原因です。本文内の 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版本,并添加了相应的注释。
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");
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数据时,便会触发该漏洞。
以上がAPACHE OFBIZ XMLRPC リモート コード実行の脆弱性の分析例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。