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方法

image-20240101111810930

自身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)去获取值的

image-20240101112235112

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的地方。

image-20240101112415633

在RequestHandler的doRequest方法当中,调用了对应的类和方法

image-20240101183415473

在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;
}
image-20240101184036125

然后调用了对应的EventHandler的invoke方法,我们这是xmlrpc,所以直接看XmlRpcEventHandler#invoke(),invoke里调用了execute()方法,直接看这个关键的execute方法吧。

image-20240101185650786

进入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

image-20240107162119257

今生

CVE-2023-49070

分析

上文说到,对该接口的防护主要有以下两个

  • /webtools/control/xmlrpc接口添加了鉴权
  • 过滤了<serilizable关键字

Ofbiz官网下载Ofbiz 18.12.09版本,使用vulhub的远程调试,在对应的cacheFilter打下断点。发送CVE-2020-9496的payload,可以看到,先是获取了uri是否等于/control/xmlrpc,并且判断是否存在<serilizable关键字,有则直接return。

image-20240107171327178

由于对<serilizable关键字的校验在uri之后,我们此时让"/control/xmlrpc".equals(uri.toLowerCase())的执行结果为false,则不会对<serializable进行校验。在Java中,分号在权限绕过的场景冲多次出现,我们可以利用分号来进行绕过。此时POST的接口为/webtools/control/xmlrpc;/

image-20240107171921443

其次,我们来到LoginWorker类的checkLogin方法,需要我们的login()方法的返回结果不是error

image-20240107173423732

进入login()方法,我们这要尽可能让他返回的结果不是error,直接找到返回error的代码

image-20240107173603687

这里只需要传入参数requirePasswordChange=Y即可绕过登录了。

修复

官方直接在https://github.com/apache/ofbiz-framework/commit/c59336f604f503df5b2f7c424fd5e392d5923a27这个commit中移除了XMLRPC相关的处理,但是并没有对登录绕过进行修复。

CVE-2023-51467

该漏洞主要是由于上文的认证绕过+自带的Groovy的代码执行功能导致。