Google CTF 2024 Grand Prix Heaven

This is my solution for Grand Prix Heaven from Google 2024 CTF. I particularly enjoyed this challenge because of its detail level; multiple little vulnerabilities had to be chained together to achieve XSS on the target.
I will start with the exploit, then follow up with the notes which may help explain why the exploit does what it does.
Exploit⌗
Output:
Webhook:
Notes⌗
Important Again, these are my unedited notes, I wrote them for me and hence may contain ambiguities, inconsistencies and even mistakes. Read them with a gain of salt.
A car fans website, you can look at your favorite cars and add new ones through a form. The form supports image uploads.
Site is using a strict CSP:
default-src 'none'; script-src 'self' https://cdn.jsdelivr.net/npm/exifreader@4.22.1/dist/exif-reader.min.js; connect-src 'self'; style-src 'self'; font-src 'self'; img-src 'self';
It is also using a dedicated internal server to process templates in some weird custom format.
Hypothesis 1:
- CSP is unbypassable, we have to inject code within
self
using the custom templating language used.
In the entire application, there is only 3 instances of innerHTML
and these are all within a deprecated function within the custom templating server code which is well protected:
They are all within the mediaparser
template which is not part of the whitelist at the main server:
We have some very vulnerable parseMultipartData
code at the template server, however, we do not have direct access to it, it’s only exposed behind sanitized endpoints at the main server.
Note that the main server is communicating through a constant BOUNDARY=GP_HEAVEN variable. This can allow us to inject extra fields!
This is how it is called:
Can you spot the culprit? We have isNum(parseInt(k))
which is used to validate that the key is numerical. Now isNum
is secure, but parseInt is overly lenient, any variable starting with a numerical digit will qualify as a number and will be included in the string.
We can look up how node needle
is managing boundaries:
The multipart request sent to the template server is probably looking similar to this:
We control the filename (which is the key within the custom
attribute of the body within the api/new-car
endpoint in the main app)
We were able to bypass it, now we need to inject something into the EXIF tags of an image:
We are able to trigger mediaparser.js
, but how do we trigger the vulnerable code path?
The whole code path is stopped by image/jpeg
, and there is no way we can get retrieve.js
JSON to be of Content-Type: image/jpeg
! We have to perform a path traversal to an image somehow!
Looking at the retrieve.js
code (where Requester is defined)
We can see a really strict regular expression, but if you look closely A-z
(other than including all alpha characters) does include a few suspicious characters as well, specifically the \
! A quick attempt at:
www.google.com\x
We can see it gets normalized to www.google.com/x
! We can use this to bypass the /api/get-car
and direct it to our own image path!
We are able to inject our DOM, but it wouldn’t run, similarly, we are unable to to use the typical <img onerror>
or <iframe srcdoc>
due to the CSP.
After a while it seems like there is no way around this, we have to bypass the CSP… after a quick look, I was like wot! How did I not see this earlier? We can easily bypass CSP by not including it in the template at all! The condition enforces that 0 → csp
, but it does not enforce the existence of a 0
to start with!
Background⌗
That’s it.
I have been wanting to release a fully-fledged writeup for this amazing challenge since last June when the CTF ended but priorities came in the way and I did not get to it.
As two months have passed and I have gotten more busy, I decided to let it go and just post my exploit and the unedited notes I wrote while solving the challenge.
Hope you enjoyed it and see you in the next one.