Skip to main content

Web Gauntlet 2 picoCTF 2021 Solution

Bypass an SQL injection filter with extended keyword restrictions to authenticate as admin on a web application.

Published: April 2, 2026Updated: August 25, 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 1Inspect the filter
    Observation
    The description mentions a dedicated /filter endpoint. Read the blocklist there before crafting anything, so you know exactly which keywords and characters are 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 OR in every case variant, so the payload never reaches SQLite. It matches case-insensitively on raw input as a substring, so splitting the letters with spaces does not help either. The || trick gets through because it is an operator token rather than a keyword on the list.

    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 2Concatenate the username with ||
    Observation
    The blocklist covers OR and other common keywords but leaves the || operator alone. SQLite's concatenation can build the string 'admin' out of fragments without ever typing a 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 3Submit the bypass and read the flag
    Observation
    The form POSTs credentials to /login, and a successful authentication renders the flag in the response. Send the crafted payload with curl, so you control the exact bytes and can read the response body 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: Percent-encode the payload by hand and then also pass it through curl --data-urlencode

    curl -d sends the body verbatim and encodes nothing, so raw pipes and quotes already arrive intact. --data-urlencode does encode, so handing it a string you already encoded produces %257C%257C, which the server decodes back to the literal text %7C%7C rather than ||, and the concatenation never happens. Encode exactly once: raw characters with -d, or the plain payload with --data-urlencode.

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

    The flag appears in the response only when the login succeeds and the session is authenticated. If the submission carries the wrong payload, say a browser autofill or extension escaped the apostrophes, the login fails quietly and you get a generic error page. curl with explicit -d values is more reliable, because you control the exact bytes sent and can read the response body yourself.

    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

Tools used in this challenge

Where to go next