Skip to main content

Web Gauntlet 3 picoCTF 2021 Solution

Bypass a strict SQL injection filter with character and keyword restrictions to log in as admin.

Published: April 2, 2026Updated: August 25, 2026

Description

Final boss! Log in as admin using the SQLite-backed login form. More keywords are filtered this time, and the combined username + password input is capped at 25 characters.

Visit /filter to see the full blocklist before attempting your bypass.

Open the challenge URL and check /filter to see blocked keywords.

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
The final gauntlet round blocks nearly all SQL keywords and restricts total input to 25 characters. The SQL Injection Payload Generator (Auth Bypass tab) shows the || concat variant that keeps the payload under the character limit. For the broader filter bypass library, see the SQL Injection for CTF guide.
  1. Step 1Enumerate the expanded blocklist
    Observation
    The description names a /filter endpoint and a 25-character cap. Read the expanded blocklist first, before crafting anything.
    GET /filter to see every blocked term. Compared to Web Gauntlet 2, this version adds =, >, <, ;, --, /*, and */ to the blocklist. Crucially, the || string-concatenation operator is still allowed, and so are IS NOT and GLOB. The twist is a hard 25-character limit on the combined username + password input.
    bash
    curl http://mercury.picoctf.net:<PORT_FROM_INSTANCE>/filter
    What didn't work first

    Tried: Trying the same 'ad'||'min' + OR 1=1 payload that worked in Web Gauntlet 2

    Both OR and = are blocked this round, so the usual OR 1=1 tautology is stripped before the query runs; /filter lists both operators explicitly. You need a different always-true expression, such as 'a' IS NOT 'b', that touches nothing on the list.

    Tried: Skipping /filter and guessing the blocklist from prior rounds

    Each round adds new blocked terms, so the previous round's list is always incomplete. This one adds =, >, <, ;, and the comment markers on top of Gauntlet 2. Craft a payload without checking /filter and you will reach for a newly banned operator and get a silent login failure with no useful error.

    Learn more

    Full blocklist. The filtered terms are: or and true false union like = > < ; -- /* */ admin. Everything else is fair game, including ||, IS NOT, GLOB, and the bitwise | operator.

    Strategy. The same || concatenation trick that cracked Gauntlet 2 still works here. The new challenge is staying within 25 total characters while also bypassing the password check without = or LIKE.

  2. Step 2Build 'admin' via || concatenation and bypass the password check
    Observation
    The blocklist bans OR, =, and LIKE while leaving || and IS NOT alone, and the 25-character cap forces something compact. Split 'admin' with ||, and use 'a' IS NOT 'b' as the always-true password tautology, which fits exactly.
    Split the blocked word admin across two string literals joined by ||: username adm'||'in. SQLite evaluates the concatenation before comparing, so the query sees username='admin' without the literal word ever appearing in your input. For the password, use IS NOT (not blocked) to construct a condition that is always true: a' IS NOT 'b. Together the two fields are 21 characters, well within the 25-character cap.
    bash
    # Username: adm'||'in   (9 chars)
    # Password: a' IS NOT 'b   (12 chars)
    # Total: 21 chars -- well within the 25-char limit
    
    curl -X POST http://mercury.picoctf.net:<PORT_FROM_INSTANCE>/login \
      --data-urlencode "username=adm'||'in" \
      --data-urlencode "password=a' IS NOT 'b"
    A successful login redirects to a page containing the flag.
    What didn't work first

    Tried: Using GLOB '*' as the tautology but exceeding the 25-character limit by choosing a long username split

    Both username splits ('adm'||'in' and 'ad'||'min') are 9 characters each. Combined with the 12-character password 'a' IS NOT 'b' the total is 21 characters, well within the 25-char cap. No alternative split is actually required by the character limit.

    Tried: Using 'OR 'a'='a or 1=1 as the password tautology since those patterns bypassed earlier gauntlet rounds

    Both = and OR sit on this round's blocklist, so either form is stripped and the login silently fails back to the login page with no SQL error. IS NOT is the unblocked alternative: 'a' IS NOT 'b' evaluates true in SQLite while touching nothing banned.

    Learn more

    How the query looks to SQLite. The server likely runs something like:

    SELECT username, password FROM users
    WHERE username='adm'||'in'
      AND password='a' IS NOT 'b'

    adm'||'in evaluates to admin, matching the row. The password half parses left to right as (password='a') IS NOT 'b': the inner comparison yields 0 or 1, and an integer is never the same value as the text 'b', so the whole expression is 1 regardless of the stored password.

    Why IS NOT works as a truth bomb. Unlike = (blocked), IS NOT is SQLite's null-safe inequality operator, and it never returns NULL. Because it shares precedence with = and associates left to right, appending it to the password comparison wraps that comparison in a test that is true for either outcome, which is the tautology that replaces the blocked OR 1=1 pattern.

    25-character accounting. If the limit is tight, count carefully: adm'||'in is 9 characters, a' IS NOT 'b is 12 characters, summing to 21 (well within the 25-char cap). Alternative: username ad'||'min (9 chars) plus a shorter tautology like a' GLOB '* (10 chars) totals 19.

    See SQL injection for CTF for the broader operator substitution trick library.

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{k3ep_1t_sh0rt_...}

Per-instance flag. All confirmed variants share the prefix k3ep_1t_sh0rt_ with a different hex hash suffix per team (e.g. fc8788aa1604881093434ba00ba5b9cd, ef4a5b40aa736f5016b4554fecb568d0, 30593712914d76105748604617f4006a).

Key takeaway

Filters that block individual SQL keywords still fail, because every SQL dialect offers dozens of semantically equivalent operators. Block = and LIKE and IS NOT supplies the tautology; block OR and || reassembles forbidden strings from allowed fragments. A character limit adds friction without preventing injection, since shorter operator aliases compress the payload. Parameterized queries remove the surface entirely, by never interpolating user text into query syntax.

Related reading

Tools used in this challenge

Where to go next