Description
A piece of paper is a blank canvas, what do you want on yours? Source code: paper-2.tar .
Setup
Download and extract paper-2.tar.
Read the source code to understand the app's architecture: XSLT upload, Redis caching, bot visit flow, and CSP headers.
tar -xf paper-2.tar
cat app.py
cat *.xml *.xsl 2>/dev/null
Solution
- Step 1Understand the attack surfaceThe app stores a bot's secret cookie in Redis with a 60-second TTL and allows XSLT stylesheets to be uploaded. CSP is set to `script-src 'none'` and `default-src 'self'`, blocking JavaScript and external exfiltration. XSLT stylesheets can use `document()` to fetch the `/secret` endpoint with the bot's auth context, reading the secret from the HTML. The challenge is exfiltrating this same-origin data with no JS and no external URLs.cat app.py
- Step 2Prepare marker pairs and prefill the Redis LRU bufferThe attack exploits Redis's `allkeys-lru` eviction policy. Upload 2,560 marker pairs (10 replicas × 128 bits × 2 markers per bit). Each 20 KB marker represents a binary 0 or 1 for a specific bit of the 32-character hex secret. Then upload ~1,500 dummy 60 KB files to create LRU age separation between the markers and any freshly evicted keys.python3 solve.py --phase prepare# Uploads marker pairs and dummy files to establish LRU baseline
- Step 3Deploy the XSLT payloadGenerate XSLT documents using `<xsl:if>` with `contains()` and `substring()` to test each hex character of the secret. For each bit that tests true, the stylesheet conditionally renders an `<img>` tag pointing to the corresponding '1' marker (which performs a Redis `get()`, refreshing its LRU timestamp). False bits leave the '0' markers untouched.# XSLT payload structure:<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <xsl:variable name="secret" select="document('/secret')//span[@id='secret']"/> <xsl:if test="contains('0123456789abcdef', substring($secret,1,1))"> <img src="/marker/bit0_1"/> </xsl:if> <!-- ... repeat for each bit position --> </xsl:template> </xsl:stylesheet>python3 solve.py --phase payload
- Step 4Trigger the bot and run evictionSubmit your XSLT page to the bot. The bot visits and the XSLT processes, conditionally loading marker images (~25 seconds). Those loaded markers get their LRU timestamps updated. Then upload ~4,500 postfill entries to exceed Redis's 512 MB memory limit, forcing the LRU eviction policy to evict untouched (zero-bit) markers.python3 solve.py --phase trigger# Submits page to bot, waits ~30s, then uploads postfill to trigger eviction
- Step 5Recover the secret via HEAD requests and submit for the flagIssue HEAD requests to all 2,560 markers. Alive markers (response >100 bytes) were accessed by the bot = bit is '1'. Evicted markers = bit is '0'. Use majority voting across 10 replicas per bit to reconstruct the full 32-character hex secret. Submit it to `/flag` -- the endpoint uses `getdel` so you only get one attempt.python3 solve.py --phase recover# Queries all markers, reconstructs secret via majority vote, submits to /flag
Flag
picoCTF{i_l1ke_frames_on_my_canvas_953d5fff}
Paper-2 is an XSLT + Redis LRU side-channel attack. Since CSP blocks JS and external URLs, XSLT's document() function reads the secret in-origin, and Redis eviction patterns leak which bits were accessed by the bot. The 60-second TTL and single-guess /flag endpoint make this a precision timing attack.