Crocodilu
CSP bypass through unsupported www.youtube.com JSONP endpoint
Last updated
CSP bypass through unsupported www.youtube.com JSONP endpoint
Last updated
Check out my new video sharing platform!
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 some@email.prefix%
. 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.
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.
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.