Persistent XSS (Unvalidated oEmbed) at Medium.com
Are you aware of any (private) bug bounty programs? I would love to get an invite. Please get in touch with me: Jonathan@Protozoan.nl
The impact will be much larger; no special urls involved and no XSS auditors ruining our game. We call this a stored or persistent XSS attack. As you may remember, we had success before with this type of attack; see the AH.nl report.
As always we’re looking for a target that encourages us to search for bugs. So what about Medium.com? Woohoo! They’ve got a nice bug bounty program.
I love their platform for writing my reports. The design is clean, no ads and it works great. High five, erm, I mean Claps for them!
Today is a good day to claim our position in the their Hall of Fame: https://medium.com/humans.txt
Storing information and sharing it with others is what Medium is all about. We just need to find a way to put our code in the information in such a way that it gets executed. So let’s take a look at their story editor.
The editor supports different types of content; plain text, images and media embeds.
Through media embeds you are able to enrich your stories. For example loading external videos, showing tweets that are enriched with your twitter profile information. You just press the (+) in the editor, paste the url, press enter and wait for the magic. This magic has a name; oEmbed.
If you own a platform like Medium.com you want to support all sorts of embeds. That means maintaining a whitelist of approved external platforms, securely processing and transforming the embed data and keeping it all scalable at the same time.
Mmm. What if we can become a provider, providing malicious code? That would be perfect, injecting code right away into the story through the embed code.
Let’s make a fake login for our proof of concept.
So does that mean we’re done if our malicious external url contains the proper oEmbed tags? Think of a html page that contains oEmbed tags stating it is a video player, but silently loads a fake login page?
Not so fast sir, no. Your beautiful styled fake login embed will be rendered as a plain text box containing a title, description and the domain name; the fallback layout.
Only approved providers are allowed to embed their magic. I hear you say “well, become a provider!”. Unfortunately no. Applying to become a provider means that we need to social engineer, and that is not allowed by the responsible disclosure rules of Medium.com
Let’s open the Medium editor and see what the browsers does if we try to embed a vimeo video. Since Vimeo is on the whitelist it should work and we could learn more about the inner workings of Embed.ly.
How does the oEmbed implementation work, screenshot time!
Important to notice is that Embed.ly creates a mediaResourceId for every embed. This mediaResourceId is a MD5 hash of the URL. This is a smart move and allows them to cache the results. Is someone embedding an already processed URL? Embed.ly serves the embed immediately from their cache.
Medium uses the mediaResourceId inside their stories to refer to specific embeds. There is no HTML stored inside the story.
So we need to fool Embed.ly into creating a mediaResourceId for our fake login page. Furthermore this mediaResourceId should serve a response that loads our fake login through an iframe.
Let’s see what happens if we try to create our own mediaResourceId:
No success. Adding a few oEmbed or Open Graph tags in order to inject the fake login as a video player? No luck. I tried every combination I could think of and nothing worked.
So we have to think of something else.
Pretending we are Vimeo, raise the sails, setup your proxies!
As we learned from screenshot 5; Embed.ly embeds Vimeo videos fine and loads their vimeo player.
So what if we could do some sort of a man-in-the-middle-attack (MITM) and pretend we are Vimeo? We will change the Vimeo response so it loads our fake login page. Search for the string that points to the vimeo player
https://player.vimeo.com/video/142424242 and change it into
https://evildomain.ltd/fakelogin? Sounds good!
- Quick setup: Turn on your PHP server, upload fakelogin.html (file containing a properly designed fake login), upload proxy.php (miniProxy, allows us to load external urls, change the responses, serve the changed response).
- Add 1 line to proxy.php, line 381, above
//Parse the DOM:
$responseBody = str_replace("https://player.vimeo.com/video/142424242", "https://evildomain.ltd/embedly/fakelogin.html", $responseBody);
- Create a new Medium story
- Embed the url
- Medium.com their server will request the oEmbed details from
https://evildomain.ltd/embedly/proxy.php?https://vimeo.com/142424242, we send them a response that is identical to one from Vimeo, only now containing our fake login page instead of a video player.
- Wait for the magic, we got our code injected!
Let’s reload our article and see if our fake login embed loads.
Discussion, what is Coordinated Vulnerability Disclosure (CVD)?
As you may remember from the previous IKEA report; coordinated disclosure can take some time. Today we run into the same problem with Medium.com.
The problem is communication; it took us 11 emails before we got in direct contact with one of their engineers. When communication was setup we quickly discovered that the initial bug was resolved but their caching servers still serve malicious payloads. After Medium invalidated their cache the vulnerability was resolved and we were able to publish this report; 86 days after the initial report.
New guideline from the National Cyber Security Centre
On 04–10–2018 the Dutch Governement published a new guideline for coordinated vulnerability disclosure. This guideline is a revision of the Responsible Disclosure Guideline, published in 2013. They changed the name from Responsible Disclosure to Coordinated Vulnerability Disclosure. The main reason for that is that they want to focus on the importance of clear communication, the coordination of it.
Direct communication between a bug reporter and the technical staff is required for a good functioning CVD. Also the last resort option, full disclosure, is now mentioned in the guideline:
The main intention of CVD is to mitigate the vulnerability, but ‘full disclosure’ of the vulnerability is always an option for a reporting party if it feels that the process will take too long. This measure is the proverbial ‘big stick’ available to the reporting party. Naturally, this situation must be prevented as much as possible.
As you remember from the IKEA report, this is something we should try to avoid.
One of the lessons learned from this report is that even though a company has a CVD program we sometimes still need to have patience and persistence in getting a bug resolved.
For a company it’s important to have easy to approach engineers that coordinate the reported vulnerabilities and update the bug reporter of any updates. This saves both parties plenty of time ;-)
However this bug could still cause a lot of harm; a regular visitor won’t be able to see the difference between this fake login and a real login.
Impact of this attack
- Perfect for phishing
- Open redirect possible by using
top.location.href. We may auto redirect users to another page after they have entered their credentials for example.
- Attack visitors by embedding http://beefproject.com/
- Allows an attacker to perform clickjack attacks
Do I forget anything? Leave a comment!
Solutions that prevent this attack
- Improve oEmbed provider checks, disallow unapproved iframe sources
- Don’t allow iframes to break out by using
- Always invalidate the caches (and that is hard)
$100, mention in the humans.txt and a Medium t-shirt
08–07–2018 Discovered bug, wrote this report, informed Medium by email
11–07–2018 Requested confirmation, Medium confirmed
20–07–2018 Requested update from support, reply that I got a reward of $100, mention in the humans.txt and Medium t-shirt, no updates on the bug itself
08–08–2018 Requested update from support, about to publish another blog that describes the same type of attack, bug not fixed on Medium. So the other report is halted (responsible disclosure), no reply
15–08–2018 Requested update from support
15–08–2018 Medium support replied that they requested an internal update from engineers, will contact me later
02–09–2018 Requested update from support, no reply
04–09–2018 Sent LinkedIn message to a Full Stack Software Engineer at Medium (check if they are aware of the bug), no reply
13–09–2018 Sent LinkedIn message to Executive Assistant of CEO at Medium (check if they are aware of the bug), no reply
18–09–2018 Requested update from support, no reply
19–09–2018 Sent LinkedIn message to Head of Legal at Medium
24–09–2018 Reply from Head of Legal, will request security engineers to contact me
03–10–2018 No updates from security engineer, just confirmed bug still exists, shared this report with Head of Legal, requested updates.
03–10–2018 Head of Legal introduced me to a security engineer, security engineer explained that they previously marked this bug as resolved, apologized for the lack of communication, discovered unvalidated cache that causes the payload to still work, explained they invalidated all the caches, allowed me to disclose the report at 04–10–2018.
04–10–2018 Published this Report