CVE-2022-22733: Apache ShardingSphere ElasticJob-UI privilege escalation

12 minute read

Introduction

A vulnerability discovered in Apache ShardingSphere ElasticJob-UI known as CVE-2022-22733, The vulnerability lead to exposure of sensitive informatiopns and as a results it allows an attacker who has guest account to do privilege escalation.

Testing Lab

For the testing lab the vulnerability affecting version 3.0.0 and prior versions. So, we can use docker to build our testing lab, First pull the docker image:

sudo docker pull apache/shardingsphere-elasticjob-lite-ui:3.0.0

Now, Let’s run the app:

sudo docker run -d --name elasticjob-ui -p 8088:8088 -e ELASTIC_JOB_GUEST_ENABLED=true apache/shardingsphere-elasticjob-lite-ui:3.0.0

Here it will run the app and enable the guest access, Therefore we can simulate and reproduce the vulnerability.

 

What is ShardingSphere ElasticJob-UI ?

ShardingSphere ElasticJob-UI is a web-based graphical user interface (GUI) that is part of the ShardingSphere ElasticJob project and provides an easy-to-use interface to manage, monitor, and visualize the status of jobs running in a ShardingSphere ElasticJob cluster. It simplifies the management and administration of distributed scheduling tasks, making it more convenient for users to manage their jobs without dealing directly with the underlying API or configuration files.

 

Static Analysis

Let’s Open burpsuite and take a look at the login request and response.

In the above screenshot when we login, It provide us back with a response contains the accessToken. If we copy the accessToken value and decode it:

As we can see after decoding the value, It’s exposed the guest username and password which is the user we logged-in with & Also exposed the root username and password, As a results we can use the exposed root creds and escalate our privileges. Let’s see the root cause of this issue by analyzing and going through the login/authentication process code. Now, Under the following class org.apache.shardingsphere.elasticjob.lite.ui.security.AuthenticationFilter we can see the following:

Which obvuise is the endpoint where the authentication process happens, Let’s dig deeper into the class code:

First, It’s defining the package and import needed libraries by scrolling down we can see the start of defining the class:

This AuthenticationFilter class implements the Filter interface and has a constant LOGIN_URI that represents the URI for the login endpoint, a Gson object for JSON serialization and deserialization, and a UserAuthenticationService object from the UserAuthenticationService class that can be set using a setter method. If we go to that class under the same location we will be able to see that it’s responsiable to check for the user in the authentication process. In other words, It’s a main part of the authentication. As we mentioned before about the Filter interface, It’s basically a part of the Servlet API and is used to define filters that can intercept requests and responses going to and coming from a web application, For example, modifying request parameters, add or modify request headers, perform logging, and even transform the response returned by the server. Something important we have to know about the Filter interface and It’s that the Filter interface contains three methods:

  • init(FilterConfig config): This method is called when the filter is initialized and It allows the filter to perform any initialization that is required.

  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain): This method is called for every incoming request that matches the filter mapping and It allows the filter to examine or modify the request, perform any filtering logic, and then pass the request on to the next filter in the chain or to the servlet.

  • destroy(): This method is called when the filter is destroyed and It allows the filter to perform any cleanup that is required.
    By completing our lines of codes we can see the doFilter() method:

The method takes three parameters servletRequest, servletResponse and filterChain, servletRequest and servletResponse parameters are instances of the ServletRequest and ServletResponse interfaces, respectively. The filterChain parameter is an object that represents the next filter in the chain or the servlet that the request is being sent to. Then, The method first casts the ServletRequest and ServletResponse objects to HttpServletRequest and HttpServletResponse, respectively. After that it checks if the request URI matches the LOGIN_URI constant. If it does, the handleLogin method is called to handle the login request and If not the method checks if the request has a valid access token by checking the value of the Access-Token header. If the access token is valid, the request is passed on to the next filter in the chain using the doFilter method of the FilterChain object. Otherwise, the respondWithUnauthorized method is called to return an unauthorized status code to the client.

The destroy() method is empty and is used to clean up any resources used by the filter and handleLogin method is responsible for handling user login requests. It receives a HttpServletRequest object, which contains the user’s credentials, and a HttpServletResponse object, which is used to return the server’s response.It’s first reads the user’s credentials from the request using the getReader method and converts them to a UserAccount object using the gson.fromJson method. Then calls the checkUser method of the userAuthenticationService object to check the validity of the user’s credentials. If the credentials are valid the method creates a HashMap object to hold the user’s information, including their username, accessToken, and whether they are a guest user. It then writes this information to the response using the httpResponse.getWriter().write method after converting it to a JSON string using the gson.toJson method. If the user’s credentials are invalid, the method calls the respondWithUnauthorized method, which writes a JSON string to the response indicating that the user is unauthorized.

In this line we can notice it’s where the accessToken returned to the user and it’s being called from the getToken() method from userAuthenticationService object which is the userAuthenticationService class. Now, If we go to the class:

package org.apache.shardingsphere.elasticjob.lite.ui.security;

import com.google.common.base.Strings;
import com.google.gson.Gson;
import lombok.Setter;
import org.apache.commons.codec.binary.Base64;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * User authentication service.
 */
@Component
@ConfigurationProperties(prefix = "auth")
@Setter
public final class UserAuthenticationService {
    
    private String rootUsername;
    
    private String rootPassword;
    
    private String guestUsername;
    
    private String guestPassword;
    
    private final Base64 base64 = new Base64();
    
    private Gson gson = new Gson();
    
    /**
     * Check user.
     *
     * @param userAccount user account
     * @return check success or failure
     */

We can see that it starts with defining the package, class and some variables which we can see in the accessToken when we decoded it such as rootUsername, rootPassword, guestUsername & guestPassword and creates a new Base64 object along with JSON object to store data into.

    public AuthenticationResult checkUser(final UserAccount userAccount) {
        if (null == userAccount || Strings.isNullOrEmpty(userAccount.getUsername()) || Strings.isNullOrEmpty(userAccount.getPassword())) {
            return new AuthenticationResult(null, null, false, false);
        }
        if (rootUsername.equals(userAccount.getUsername()) && rootPassword.equals(userAccount.getPassword())) {
            return new AuthenticationResult(rootUsername, rootPassword, true, false);
        }
        if (guestUsername.equals(userAccount.getUsername()) && guestPassword.equals(userAccount.getPassword())) {
            return new AuthenticationResult(guestUsername, guestPassword, true, true);
        }
        return new AuthenticationResult(null ,null, false, false);
    }
     /**
     * Get user authentication token.
     *
     * @return authentication token
     */
    public String getToken() {
        return base64.encodeToString(gson.toJson(this).getBytes());
    }
}

After that It provides a method to check user authentication by comparing the provided username and password with the pre-configured root and guest usernames and passwords. Finally, We can see the getToken() method that encodes the current UserAuthenticationService object as a Base64 encoded string which is returned as the authentication token (accessToken) and here comes the vulnerability with the getToken() method is returning the Base64 string with representation of the entire UserAuthenticationService object including the root username and password. As a results it’s exposed as we saw in the first of the analysis.

Dynamic Analysis

Now, Let’s Setup our lab in debugging mode to see how it works dynamically. As we pulled the image before we will just run it again with a different name and port.

sudo docker run -d --name elasticjob-ui-debug -p 8888:8088 -p 8000:8000 -e ELASTIC_JOB_GUEST_ENABLED=true -e JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n" apache/shardingsphere-elasticjob-lite-ui:3.0.0

Here we added a new port mapping which is 8000 for debugging and also added new environment variable to set debugging options JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n" & Finally, a port mapping for the web interface of the application 8888 to 8088. Now, It’s time to setup our debugger through IntelliJ IDE, We need to download the ShardingSphere version 3.0.0 source code from github and open it as a new project inside IntelliJ IDE.

After that go to Run Tab and then click on Edit Configurations:

Then Click on the + add sign and choose Remote JVM Debug:

And Name it as you want & configure the remote debugger, By adding the machine docker IP, Debugging port which is 8000 and the module to debug which is the ShardingSphere-ElasticJob-lite-UI:

Finally, Let’s set our breakpoint on the handlelogin function under the org.apache.shardingsphere.elasticjob.lite.ui.security.AuthenticationFilter class:

Now, Press on the debug button:

We can see it’s telling us that’s connected successfully to the targeted VM debug:

Let’s visit the application on our browser and login. Once we hit the Login button, We will be able to see that it’s hit the breakpoint and our debugger is working:

Here under this in the debugger which refers within the AuthenticationFilter class that it has the UserAuthenticationService Object which is made out of the UserAuthenticationService class itself:

We can see that it’s already carry the guest & root names and passwords, Along with the created Objects of Base64 and Gson. Now, Let’s step over and set a breakpoint to line where accessToken get added to the request:

In the above screenshot as we see, By stepping over until we arrive to checkUser() function which is taking the credentials entered by user.

By stepping over 2 more steps we can see that the authenticationResult started to created & Mentioned under it the username,password which are our credentials, success which is the authentication status and in this situation it’s true as credentials matched & Finally, isGuest which indicates if the user is a guest or no and in our case yes it’s.

here we can see the result which is a HashMap and will be sent with the response body. Clearly it’s generated the token and if we take this value & decode it we will be able to see the exposure of the root username and password, along with the guest username and password that loaded at first of the debugging in a refer with the UserAuthenticationService class object. If we do the same with the root account we obtained from the token which is root:root & see how it will be remain the same and will return the both users accounts in the token as we can see in the below screenshot:

Patch Diffing

Now, Coming to the patches that applied on the code, It’s a lot of modifications but, we will focus on the modifications made for the classes.

UserAuthenticationService.java

The vulnerability in the original code block is that the getToken() method of the UserAuthenticationService class returns a token that contains the root username and password in plaintext which used in the doFilter() method to authenticate the user, which allows an attacker to extract the root username and password by intercepting and decoding the token and the patched code block fixes the vulnerability by changing the getToken() method to isValidToken() and getToken() methods which check if the token is valid and return a new token that does not contain the root username and password. Additionally, the handleLogin() method in the patched code block now checks if the user is authenticated using a valid token instead of using the vulnerable getToken() method. If the token is valid, the filterChain.doFilter() method is called to allow the user to access the requested resource. Otherwise, the respondWithUnauthorized() method is called to deny access to the user.

UserAuthenticationFilter.java

The changes made to the AuthenticationFilter class prevent the vulnerability by implementing token based authentication instead of using a hardcoded username and password. Specifically, Adding the ability to generate a token based on a user’s credentials using the getToken() method in the UserAuthenticationService class. This token is generated using the com.auth0.jwt library and is signed using a randomly generated HMAC256 algorithm and isValidToken() method to the UserAuthenticationService class to check if the provided token is valid or not, Also Modified the handleLogin and doFilter methods. Additionally, the AuthenticationFilter class is modified to use the ObjectMapper instead of Gson for JSON serialization and deserialization. In the handleLogin() method the gson.fromJson() call is replaced with objectMapper.readValue() to deserialize the UserAccount object. Finally, in the doFilter() method, the if statement that checks for the access token is modified to use the isValidToken() method instead of checking for equality with the token obtained from userAuthenticationService.getToken().

Conclusion

At the end, We saw how the vulnerability occured and why, By showing the wrong implementation of authentication process and showed how it’s fixed in the patch applied to the code by using the JWT Library to generate the token with a secret key to which keeps the confidntiality of the data. As a results it’s not exposing the credentials anymore.

Resources

  • https://github.com/apache/shardingsphere-elasticjob-ui/commit/f3afe51221cd2382e59afc4b9544c6c8a4448a99?diff=split

  • https://hub.docker.com/layers/apache/shardingsphere-elasticjob-lite-ui/3.0.0-beta/images/sha256-9e5f309485b252a397f3cf91177be810e0f170349416de377c7393876d1069e2?context=explore

  • https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-22733

  • https://github.com/apache/shardingsphere-elasticjob-ui/releases/tag/3.0.0