연구원들이 Apache OFBiz에 존재하는 역직렬화 취약점을 보고했습니다. 이 취약점은 여러 Java 역직렬화 문제로 인해 발생하며 코드가 /webtools/control/xmlrpc에 대한 요청을 처리할 때 트리거될 수 있습니다. 인증되지 않은 원격 공격자는 이 취약점을 유발 및 악용하고 조작된 악의적인 요청을 보내 임의 코드를 실행할 수 있습니다.
Apache OFBiz는 기업이 많은 비즈니스 프로세스를 자동화하는 데 도움이 되는 일련의 엔터프라이즈 애플리케이션을 제공하는 오픈 소스 ERP(전사적 자원 관리) 시스템입니다. 여기에는 기업 내의 모든 애플리케이션이 공통 데이터, 논리 및 비즈니스 처리 구성 요소를 사용하는 데 사용해야 하는 공통 데이터 모델 및 비즈니스 프로세스를 제공하는 프레임워크가 포함되어 있습니다. Apache OFBiz는 프레임워크 자체 외에도 회계(계약 계약, 송장, 공급업체 관리, 총계정원장), 자산 유지 관리, 품목 분류, 제품 관리, 장비 관리, 창고 관리 시스템(WMS), 제조 실행/제조 등의 서비스도 제공합니다. 운영관리(MES/MOM) 및 주문처리 기능 외에 재고관리, 자동재고충원, 콘텐츠관리시스템(CMS), 인사(HR), 인력 및 팀관리, 프로젝트관리, 영업사원 자동화 등 다양한 기능 , 워크로드 관리, ePOS(전자 판매 시점 관리), 전자 상거래(e-Commerce) 및 스크럼(개발).
Apache OFBiz는 Java, JavaEE, XML 및 SOAP와 같은 다양한 오픈 소스 기술 및 표준을 사용합니다.
Hypertext Transfer Protocol은 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()/writeExternal() 및 readObject()/readExternal() 메서드를 구현하는 Serialized 또는 외부화 가능 인터페이스를 통해 직렬화해야 합니다. 객체가 직렬화되거나 역직렬화될 때 호출됩니다. 이러한 메서드는 직렬화 및 역직렬화 중에 코드를 수정하여 사용자 지정 동작을 활성화합니다.
XML-RPC는 XML을 사용하여 호출을 인코딩하고 HTTP를 전송 메커니즘으로 사용하는 RPC(원격 프로시저 호출) 프로토콜입니다. 이는 표준 사양이며 기성 구현을 제공하므로 다양한 운영 체제 및 환경에서 실행될 수 있습니다. XML-RPC에서는 클라이언트가 XML-RPC를 구현하는 서버에 HTTP 요청을 보내고 HTTP 응답을 수신하여 RPC를 수행합니다.
모든 XML-RPC 요청은 XML 요소 "
다음 예와 같이 일반적인 데이터 유형을 해당 XML 유형으로 변환할 수 있습니다.
1404 Something here 1
다양한 기본 형식의 인코딩 예는 다음과 같습니다.
1 -12.53 42
문자열의 인코딩 예는 다음과 같습니다.
Hello world!
구조에 대한 인코딩 예는 다음과 같습니다.
foo 1 bar 2
직렬화된 데이터는 "" 및 "" XML 요소 패키지로 표시됩니다. Apache OFBiz에서 직렬화 코드는 Java 클래스 org.apache.xmlrpc.parser.SerializedParser에서 구현됩니다.
그러나 Apache OFBiz에는 안전하지 않은 역직렬화 취약점이 있습니다. 이 취약점은 OFBiz가 "/webtools/control/xmlrpc" URL로 전송될 때 XML-RPC를 사용하여 HTTP 본문의 XML을 가로채서 변환하도록 구성되어 있기 때문에 발생합니다. . 데이터로 인해 발생합니다. 이 끝점으로 전송된 요청은 처음에 URL 매핑 방법을 결정하는 org.apache.ofbiz.webapp.control.RequestHandler Java 클래스에 의해 처리됩니다. 다음으로 org.apache.ofbiz.webapp.event.XmlRpcEventHandler 클래스는 Execution() 메서드를 호출합니다. 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(); 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数据时,便会触发该漏洞。
위 내용은 APACHE OFBIZ XMLRPC 원격 코드 실행 취약점 예시 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!