Apache OFBiz的前世今生
Apache OFBiz的前世今生
前世
OFBiz 是开放的电子商务平台,是一个非常著名的开源项目,提供了创建基于最新J2EE/XML规范和技术标准,构建大中型企业级、跨平台、跨数据库、跨应用服务器的多层、分布式电子商务类WEB应用系统的框架。 OFBiz最主要的特点是OFBiz提供了一整套的开发基于Java的web应用程序的组件和工具。包括实体引擎, 服务引擎, 消息引擎, 工作流引擎, 规则引擎等。
CVE-2020-9496
已知条件是漏洞的路由在/webtools/control/xmlrpc
,直接搜xmlrpc无法找到对应路由,只能在 framework/webtools/webapp/webtools/WEB-INF/controller.xml
下找到,但是这还是看不出来啥。
<request-map uri="xmlrpc" track-serverhit="false" track-visit="false">
<security https="false"/>
<event type="xmlrpc"/>
<response name="error" type="none"/>
<response name="success" type="none"/>
</request-map>
选择搜索/control/*
,很多xml文件都存在这个路由,但是大致都差不多,都配置了一个ControlServlet
<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>
找到该类对应的代码framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlServlet.java
,doPost
方法调用了doGet
方法,直接看doGet
方法。代码很多,挑了关键的几行
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
.....
RequestHandler requestHandler = this.getRequestHandler();
.....
try {
// the ServerHitBin call for the event is done inside the doRequest method
requestHandler.doRequest(request, response, null, userLogin, delegator);
......
}
}
调用了requestHandler.doRequest
public void doRequest(HttpServletRequest request, HttpServletResponse response, String requestUri) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests {
HttpSession session = request.getSession();
Delegator delegator = (Delegator) request.getAttribute("delegator");
GenericValue userLogin = (GenericValue) session.getAttribute("userLogin");
doRequest(request, response, requestUri, userLogin, delegator);
}
他这里继续调用了自己的doRequest
方法,调用自身getControllerConfig
方法
自身getControllerConfig方法中调用ConfigXMLReader.getControllerConfig
,这个其实就是很明显去获取控制器的配置了,
public ConfigXMLReader.ControllerConfig getControllerConfig() {
try {
return ConfigXMLReader.getControllerConfig(this.controllerConfigURL);
} catch (WebAppConfigurationException e) {
// FIXME: controller.xml errors should throw an exception.
Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
}
return null;
}
此时传入的参数是RequestHandler.controllerConfigURL
,controllerConfigURL在初始化的时候是通过ConfigXMLReader.getControllerConfigURL(context)
去获取值的
ConfigXMLReader.getControllerConfigURL(context)
的具体内容如下
public static URL getControllerConfigURL(ServletContext context) {
try {
return context.getResource(controllerXmlFileName);
} catch (MalformedURLException e) {
Debug.logError(e, "Error Finding XML Config File: " + controllerXmlFileName, module);
return null;
}
}
此时的controllerXmlFileName
就是/WEB-INF/controller.xml
public class ConfigXMLReader {
public static final String module = ConfigXMLReader.class.getName();
public static final String controllerXmlFileName = "/WEB-INF/controller.xml";
private static final UtilCache<URL, ControllerConfig> controllerCache = UtilCache.createUtilCache("webapp.ControllerConfig");
private static final UtilCache<String, List<ControllerConfig>> controllerSearchResultsCache = UtilCache.createUtilCache("webapp.ControllerSearchResults");
public static final RequestResponse emptyNoneRequestResponse = RequestResponse.createEmptyNoneRequestResponse();
也就是上文搜xmlrpc的地方。
在RequestHandler的doRequest方法当中,调用了对应的类和方法
在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;
}
然后调用了对应的EventHandler的invoke方法,我们这是xmlrpc,所以直接看XmlRpcEventHandler#invoke()
,invoke里调用了execute()方法,直接看这个关键的execute方法吧。
进入getRequest方法,用XmlRpcRequestParser进行解析,XmlRpcRequestParser是在xmlrpc-common-3.1.3.jar
包中,是Java中处理XML-RPC的第三方库
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);
}
};
}
到后面其实就差不多了,就是CVE-2016-5003
的内容了。
修复及绕过
漏洞出现后,官方对这个接口进行了修复,diff如下:https://github.com/apache/ofbiz-framework/commit/d955b03fdc226d600d81d19d273e773f84b5c000。仅仅只是添加了鉴权,对该接口并没有完全修复反序列化,因此在后续的diff当中,过滤了</serializable>
,commit如下https://github.com/apache/ofbiz-framework/commit/15c209a475cb50525a6cbd1e24601355c7be1b0a,但是后续又发现<serializable>
可以通过<serializable >
绕过,于是又改成过滤<serializable
今生
CVE-2023-49070
分析
上文说到,对该接口的防护主要有以下两个
- 为
/webtools/control/xmlrpc
接口添加了鉴权 - 过滤了
<serilizable
关键字
去Ofbiz官网下载Ofbiz 18.12.09版本,使用vulhub的远程调试,在对应的cacheFilter打下断点。发送CVE-2020-9496的payload,可以看到,先是获取了uri是否等于/control/xmlrpc
,并且判断是否存在<serilizable
关键字,有则直接return。
由于对<serilizable
关键字的校验在uri之后,我们此时让"/control/xmlrpc".equals(uri.toLowerCase())
的执行结果为false,则不会对<serializable
进行校验。在Java中,分号在权限绕过的场景冲多次出现,我们可以利用分号来进行绕过。此时POST的接口为/webtools/control/xmlrpc;/
其次,我们来到LoginWorker类的checkLogin方法,需要我们的login()
方法的返回结果不是error
进入login()
方法,我们这要尽可能让他返回的结果不是error,直接找到返回error的代码
这里只需要传入参数requirePasswordChange=Y
即可绕过登录了。
修复
官方直接在https://github.com/apache/ofbiz-framework/commit/c59336f604f503df5b2f7c424fd5e392d5923a27这个commit中移除了XMLRPC相关的处理,但是并没有对登录绕过进行修复。
CVE-2023-51467
该漏洞主要是由于上文的认证绕过+自带的Groovy的代码执行功能导致。