Varnish, please tell us the flag!
The profile description was rendered as Markdown and then marked safe. That meant I could put raw HTML/CSS into my profile, including a <style> block.
The admin bot was the interesting part. When I reported my profile, the bot logged in as its own user and visited my page. That bot user's first name was the flag. The layout also copied the logged-in user's first name into the navbar:
So while the bot was viewing my profile, my injected CSS could match against the flag with CSS attribute selectors.
What Varnish is doing here
Varnish is a caching reverse proxy in front of the Flask app. Instead of every request going directly to Flask, requests first hit Varnish. For some paths, Varnish stores the response and serves the cached copy later.
In this challenge, Varnish cached uploaded image URLs like this:
The important mistake was that it cached those responses even when Flask returned
. So if the bot requested a nonexistent image once, that missing response stayed visible in the cache.
That gave us a way to ask: "did the bot's browser request this exact URL?"
The leak
JavaScript exfiltration was blocked by CSP, and loading images from an external server was blocked too. But same-origin images were allowed. So I could not send the flag to my own server, but I could make the bot request URLs on the challenge site itself.
For every possible next character, I generated one CSS rule. Each rule checked whether the bot's flag started with a certain prefix, and each rule used a different image URL:
The selector data-firstname^="PLFANZEN{I" means: match an element whose data-firstname attribute starts with PLFANZEN{I.
Since the navbar contained the bot's flag, exactly one of those selectors matched the real next character. If the flag started with PLFANZEN{I, the browser loaded: /uploads/2/c_I.png
The other URLs were never requested.
At that point, c_I.png did not exist yet. Flask returned 404, and Varnish cached that 404.
Turning the cache into an oracle
After the bot visited my profile, I uploaded real PNG files for every candidate name:
Then I requested each image URL myself.
If the response was 200, that meant Varnish had no cached 404, so Flask served my newly uploaded image. The bot never requested that URL.
If the response was still 404, that meant Varnish was serving the old cached miss from the bot's visit. Therefore that CSS rule had matched, and that character was correct.
So the result becomes:
Then I appended the winning character to the known prefix and repeated the same trick for the next position until the flag ended with }.
Very creative challenge i really enjoyed it!
The profile description was rendered as Markdown and then marked safe. That meant I could put raw HTML/CSS into my profile, including a <style> block.
The admin bot was the interesting part. When I reported my profile, the bot logged in as its own user and visited my page. That bot user's first name was the flag. The layout also copied the logged-in user's first name into the navbar:
Code:
<strong data-firstname="PLFANZEN{...}">
So while the bot was viewing my profile, my injected CSS could match against the flag with CSS attribute selectors.
What Varnish is doing here
Varnish is a caching reverse proxy in front of the Flask app. Instead of every request going directly to Flask, requests first hit Varnish. For some paths, Varnish stores the response and serves the cached copy later.
In this challenge, Varnish cached uploaded image URLs like this:
Code:
/uploads/<user_id>/<filename>.png
The important mistake was that it cached those responses even when Flask returned
Code:
404 Not Found
That gave us a way to ask: "did the bot's browser request this exact URL?"
The leak
JavaScript exfiltration was blocked by CSP, and loading images from an external server was blocked too. But same-origin images were allowed. So I could not send the flag to my own server, but I could make the bot request URLs on the challenge site itself.
For every possible next character, I generated one CSS rule. Each rule checked whether the bot's flag started with a certain prefix, and each rule used a different image URL:
Code:
strong[data-firstname^="PLFANZEN{I"] {
background-image: url("/uploads/2/c_I.png");
}
strong[data-firstname^="PLFANZEN{A"] {
background-image: url("/uploads/2/c_A.png");
}
strong[data-firstname^="PLFANZEN{B"] {
background-image: url("/uploads/2/c_B.png");
}
The selector data-firstname^="PLFANZEN{I" means: match an element whose data-firstname attribute starts with PLFANZEN{I.
Since the navbar contained the bot's flag, exactly one of those selectors matched the real next character. If the flag started with PLFANZEN{I, the browser loaded: /uploads/2/c_I.png
The other URLs were never requested.
At that point, c_I.png did not exist yet. Flask returned 404, and Varnish cached that 404.
Turning the cache into an oracle
After the bot visited my profile, I uploaded real PNG files for every candidate name:
Code:
c_I.png
c_A.png
c_B.png
...
Then I requested each image URL myself.
If the response was 200, that meant Varnish had no cached 404, so Flask served my newly uploaded image. The bot never requested that URL.
If the response was still 404, that meant Varnish was serving the old cached miss from the bot's visit. Therefore that CSS rule had matched, and that character was correct.
So the result becomes:
Code:
/uploads/2/c_I.png -> 404 correct character
/uploads/2/c_A.png -> 200 wrong character
/uploads/2/c_B.png -> 200 wrong character
Then I appended the winning character to the known prefix and repeated the same trick for the next position until the flag ended with }.
Very creative challenge i really enjoyed it!