Web Gauntlet 2 picoCTF 2021 Solution

Published: April 2, 2026

Description

This website looks familiar... Log in as admin, and use the filter to find the flag. Unlike Web Gauntlet 1, the filter here blocks keywords in a case-insensitive way, catching OR, or, Or, and oR alike.

Visit the /filter endpoint to see what words and characters are blocked.

Open the challenge URL in your browser. Check /filter to see the current blocklist.

bash
curl http://mercury.picoctf.net:<PORT_FROM_INSTANCE>/filter

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
When common keywords like OR are filtered, SQLite's || concatenation operator is the standard bypass. The SQL Injection Payload Generator (Auth Bypass tab) includes the concat bypass variant. For the full bypass library, see the SQL Injection for CTF guide.
  1. Step 1
    Inspect the filter
    Observation
    I noticed the challenge description mentioned a dedicated /filter endpoint, which suggested reading the blocklist before crafting any payload so I could see exactly which keywords and characters were off limits.
    Visit /filter to read the blocklist. Common entries: OR, AND, UNION, SELECT, WHERE, --. The filter is case-insensitive for blocked keywords, so case-flipping does not help here. String concatenation (||) is the bypass route.
    bash
    curl http://mercury.picoctf.net:<PORT_FROM_INSTANCE>/filter
    What didn't work first

    Tried: Attempt a classic OR-based bypass like ' OR '1'='1 after seeing the blocklist

    The filter blocks the word OR in all case variants, so the payload is rejected before it reaches SQLite. The blocklist is applied as a case-insensitive substring match on the raw input, meaning even splitting the letters with spaces does not help here. The || concatenation trick bypasses this because || is an operator token, not a keyword on the blocklist.

    Tried: Try mixed-case variants like Or or oR to slip past the filter

    Unlike Web Gauntlet 1, the filter here is case-insensitive for blocked keywords, so 'Or', 'or', and 'oR' are all caught just like 'OR'. Checking /filter directly confirms exactly which strings are blocked before guessing.

    Learn more

    Assumed query. Web Gauntlet servers run a SQLite-backed login that builds the query by string interpolation, roughly:

    SELECT * FROM users WHERE username='${u}' AND password='${p}'

    With OR blocked you can't use the classic ' OR '1'='1. SQLite has another trick: the || operator concatenates strings, and it isn't a keyword. See SQL injection for CTF for the broader bypass library.

  2. Step 2
    Concatenate the username with ||
    Observation
    I noticed the blocklist blocked OR and other common keywords but did not include the || operator, which suggested using SQLite's string concatenation operator to build the string 'admin' from split fragments without ever typing the blocked word.
    SQLite's || joins two string literals: 'adm' || 'in' evaluates to 'admin'. The split keeps the literal word 'admin' out of your input and bypasses simple word filters.
    Learn more

    Operator precedence. || is the SQL string-concatenation operator (in SQLite, PostgreSQL, Oracle). It binds tighter than the comparison =, so:

    username='adm'||'in'   <==>   username = ('adm' || 'in')   <==>   username = 'admin'

    With this idiom you can build any string out of allowed substrings: 'a'||'d'||'m'||'i'||'n', or pull from a SELECT subquery. None of the joined fragments contain the blocked keywords.

    If quotes survive but -- is filtered. Submit the password as anything and rely on the password column being NULL for the admin row, or use another || for the password too.

  3. Step 3
    Submit the bypass and read the flag
    Observation
    I noticed the login form POSTs credentials to /login and that a successful authentication renders the flag in the response, which suggested sending the crafted adm'||'in payload via curl so I could control the exact bytes and read the server response directly.
    POST username=adm'||'in (and any password). On success the server renders the authenticated page with the flag.
    bash
    curl -X POST http://mercury.picoctf.net:<PORT_FROM_INSTANCE>/login \
      -d "username=adm'||'in&password=x"
    What didn't work first

    Tried: URL-encode the pipe characters as %7C%7C and submit username=adm%27%7C%7C%27in

    curl -d already applies form-encoding, so double-encoding the pipes turns || into the literal string %7C%7C in the SQL query, breaking the concatenation entirely. The server sees username='adm%7C%7C'in' which is a syntax error or a non-matching string. The single quotes still need to be percent-encoded as %27, but the || operators must be sent as literal pipe characters so SQLite can parse them.

    Tried: Try sending the payload through the browser form manually and inspect the raw HTML for the flag

    The flag is embedded in the server response only when login succeeds and the session is authenticated. If the form submission does not carry the correct payload (e.g. the apostrophes get escaped by a browser autofill or extension), the login fails silently and returns a generic error page. Using curl with explicit -d values is more reliable because you control the exact byte string sent and can verify the response body directly.

    Learn more

    Lesson. Keyword blocklists are unfixable: there are too many ways to spell every reserved word, and SQL dialects have escape hatches like ||, CHAR(), and hex blob literals. Parameterized queries are the only correct mitigation.

Interactive tools
  • SQL Injection Payload GeneratorGenerate SQL injection payloads for auth bypass, UNION extraction, blind SQLi, NoSQL operator injection, and sqlmap commands. Supports MySQL, PostgreSQL, SQLite, and MSSQL.

Flag

Reveal flag

picoCTF{0n3_m0r3_t1m3_...}

SQLite || concatenation bypass ('adm'||'in') to log in as admin and retrieve the flag.

Key takeaway

SQL injection filters that block keywords like OR and UNION are fundamentally broken because SQL dialects provide many alternative ways to express the same logic. String concatenation operators (|| in SQLite and PostgreSQL, CONCAT() in MySQL), CHAR() with ASCII codes, and hex literals all reconstruct blocked strings without ever using the forbidden text. The only correct fix is parameterized queries, which separate SQL syntax from user data at the protocol level so no amount of crafted input can alter query structure.

Related reading

Want more picoCTF 2021 writeups?

Tools used in this challenge

What to try next