Crocodilu

CSP bypass through unsupported www.youtube.com JSONP endpoint

Description

Check out my new video sharing platform!

Solution

Gaining Access

The first thing we needed to do was to gain access to the application. We can register a new user, but attempting to log in as that user would result in a "User not active" error.

Taking a look at auth.py, we would see that a successful password reset at /reset_password would set user.active to True, allowing us to access the app.

To do so, we first have to request an OTP at /request_code. This sets user.code to a random 4-digit number.

If no rate limiting is enforced on /reset_password, a 4-digit OTP would be trivial to brute-force. However, in this case, rate limiting is enforced on a per-email basis through a Redis store.

When a guess at the OTP is made, the value for the corresponding email address is incremented by 1. After 3 attempts, any further attempts for the same email address are blocked.

Interestingly, the SQL query that checks the OTP code uses the LIKE operator.

The final query is something like

which means that if we can insert the % wildcard at the start or end of either email or code, there's a good chance we can bypass the check in reasonable time.

Unfortunately, code is checked using code.isdigit(). Let's see if we can get past is_valid_email(email) instead.

The regular expression does not allow for special characters like %. However, re.match only matches at the beginning of the string, so this still allows for wildcards at the end of the email.

If zero or more characters at the beginning of string match the regular expression pattern, return a corresponding match object. Return None if the string does not match the pattern; note that this is different from a zero-length match.

There are two possibilities here - the first one is to create many accounts sharing the same prefix in their emails, increasing the chance that any code would be valid for [email protected]%. Because the registration form is reCAPTCHA-protected, this is not possible.

The approach we take instead relies on the ability to add any number of % characters at the end of the email. Because % matches 0 or more characters, the query will yield the same result no matter how many % characters are added.

Using this script, we can brute force the entire OTP space within a few minutes.

Bypassing HTML Sanitization

Now that we are in, where is the flag? When the container first starts up a post is made containing the flag. The post is admin-only, which means we need to stage a client-side attack against the admin.

Our first hurdle is BeautifulSoup. Our HTML content is parsed and checked for any blacklisted tags. Combined with a restrictive CSP, this greatly restricts what we can do.

Luckily for us, the built-in html.parser does not treat malformed HTML the same way as a standards-compliant HTML5 parser would. There is a section dedicated to this in the documentation.

One trick to exploit this parser differential is through HTML comments. Consider the following payload:

BeautifulSoup thinks that the comment spans the entire payload, ending at -->.

However, a HTML5 parser would accept <!--> as a valid comment. We can test this out on any modern browser using a DOM viewer.

Abusing YouTube JSONP Endpoint

Now that we can inject arbitrary HTML, we have to get past the rather restrictive CSP that is applied on all pages through the Nginx proxy.

Throwing this into Google's CSP evaluator shows us that www.youtube.com might host JSONP endpoints that we can abuse.

If so, we could use something like

to achieve an XSS.

But where? The evaluator is checking against a pre-defined list of known JSONP endpoints here. The only one that matches www.youtube.com is:

which seems to be outdated because visiting that URL just brings us to a YouTube profile called "Profile Style".

At this point, I tried getting Burp Suite to insert a callback= parameter to all JSON endpoints requested using an extension like this one and using YouTube as a normal user, hoping to get lucky.

Alas, this did not yield any results. After sleeping off my frustration, I came back to this challenge when my teammate sent a link to an obscure issue on Google's issue tracker.

This didn't seem very helpful. After all, Google decided not to implement JSONP on the /oembed API, right? Using the callback parameter seems to have no effect.

But when I randomly tried using alert(); instead of alert, the following response was returned.

Wait, did I just trigger a JSONP response? For some reason, using a "valid" callback name does not elicit a JSONP response, but an "invalid" one yields a JSONP response saying that the callback name is invalid. That's really weird and ironic.

With our callback parameter reflected into the response, we can now inject arbitrary JavaScript code. The only restrictions are that quotes and angle brackets are escaped.

To exfiltrate the contents of the admin's /profile page, the following callback value can be used.

Combined with the BeautifulSoup bypass above, the final payload we submit is:

We can then find the URL of the post containing the flag:

and repeat this one more time to fetch /post/68a30ae2-a8f3-4d12-9ffa-0564a3a7177b instead.

Last updated

Was this helpful?