Skip to main content

Web Gauntlet picoCTF 2020 Mini-Competition Solution

A multi-stage web login challenge that requires bypassing increasingly strict SQL injection filters to authenticate.

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

Description

Can you beat the filters? Log in as admin across 5 progressive rounds, each adding more filtered SQL keywords.

Check /filter.php to see what is being filtered in each round. The flag is in a comment in that page after completing all rounds.

Remote

Navigate to the Web Gauntlet challenge URL and open the login page.

Open the /filter.php page on the same host to see exactly which keywords are blocked in the current round.

Always check the raw HTTP response to see what query was executed.

If your cookie keeps getting reset, use a private browser window.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
Five rounds of escalating SQL keyword filters. Use the SQL Injection Payload Generator to generate and copy auth bypass payloads. The SQL Injection for CTF guide covers the || operator bypass used in later rounds when OR is blocked.
  1. Step 1Round 1 - filter: or
    Observation
    filter.php shows only the keyword 'or' is blocked. A SQL comment (--) can truncate the query after the username and skip the password check, with no boolean operator needed at all.
    Only 'or' is filtered. Use a comment to bypass the password check. Enter admin'-- as the username. The SQL becomes: SELECT * FROM users WHERE username='admin'--' AND password='...' and the - comment drops everything after it.
    bash
    Username: admin'--
    bash
    Password: anything
    What didn't work first

    Tried: Trying ' OR '1'='1 as the username to bypass authentication

    The filter blocks 'or' case-insensitively, so the payload is stripped or rejected before the query runs and the login simply fails. The '--' comment works because it needs no boolean operator: it just ends the query after the username check.

    Tried: Using admin' # as the username (MySQL hash comment instead of --)

    The # character is MySQL comment syntax, but the database here is SQLite, which does not recognize it. The resulting syntax error makes the login fail. SQLite supports - line comments and /* */ block comments, so use --.

    Learn more

    SQL injection occurs when user-supplied data is interpolated directly into a SQL query without sanitization. The single-quote closes the string literal, and -- is a SQL line comment that causes the database to ignore the rest of the line, including the AND password='...' check.

    The fix is parameterized queries (prepared statements), where the user value is passed as a data argument and can never be interpreted as SQL syntax.

  2. Step 2Round 2 - filter: or, like, =, --
    Observation
    Round 2 adds '--' and '=' to the blocklist. C-style block comments (/* */) are not filtered yet, and they swallow the password clause without using any blocked token.
    Double-dash comments and equality operators are now blocked, but C-style block comments (/* */) are not. Open a block comment in the username field with /*. The database sees SELECT ... WHERE username='admin' and then the /* swallows the rest of the line, including ' AND password='...' entirely. No manipulation of the password field is needed.
    bash
    Username: admin'/*
    bash
    Password: anything
    What didn't work first

    Tried: Reusing the Round 1 admin'-- payload since - is not blocked yet in the filter list

    Round 2 adds - to the blocklist, so the payload is rejected; filter.php shows the updated list. Block comments (/* */) do the same job without using the blocked token.

    Tried: Trying admin' OR '1'='1 with a space to get past the = block

    Both 'or' and '=' are blocked in Round 2, so this payload trips two rules at once and getting past one does not help. The block comment sidesteps both by discarding the password clause rather than overriding it with a boolean expression.

    Learn more

    The SQL template is roughly: SELECT * FROM users WHERE username='{username}' AND password='{password}'. With the payload above, it becomes:

    WHERE username='admin'/* AND password='anything'

    The block comment opener /* causes the database to treat everything after it as a comment until a matching */ is encountered. Since no */ appears, the entire remainder of the query (including AND password='anything') is discarded. The database simply checks whether a user named admin exists.

    Block comments are a standard SQL feature and are not filtered in this round, making them a direct drop-in replacement for the now-blocked -- line comment.

  3. Step 3Round 3 - filter adds space
    Observation
    The updated filter adds the space character. Splitting the block comment across fields, username opening /* and password supplying */, means no spaces appear anywhere in the injected tokens.
    Spaces in user input are now filtered. The cross-field block comment trick still works and requires no spaces in the input. Reuse the same pattern: username opens the comment, password closes it and concatenates an empty string. Since the space filter applies to user input, not to the SQL template itself, the injected SQL still evaluates correctly.
    bash
    Username: admin'/*
    bash
    Password: */||'
    What didn't work first

    Tried: Sending admin'/* in the username and leaving the password blank or as anything

    With the space filter on, a password value of 'anything' never closes the block comment the username opened. The SQL is left with an unclosed /*, which is a parse error, and the login fails. The password has to supply the */ to make the query valid.

    Tried: Using a tab character instead of a space to bypass the space filter

    The filter blocks the space character specifically, and SQL does accept a tab as a separator. But HTML form fields normalize whitespace inconsistently, and the filter may catch tabs too. The block comment approach avoids needing any whitespace in the injected tokens at all.

    Learn more

    The resulting SQL is: WHERE username='admin'/* AND password='*/||''. The comment eats the AND clause. The ||'' concatenates an empty string to admin, which is a no-op. The query selects the admin user with no password check.

    The space filter only looks at the raw input strings. Once inside the database parser, SQL has no memory of where the space came from. The block comment itself provides the needed separator without using any space character.

  4. Step 4Round 4 - filter adds 'admin'
    Observation
    The filter now blocks the literal string 'admin'. SQLite's || concatenation splits the word into fragments ('adm'||'in') that the filter never sees as a complete username.
    The literal string 'admin' in user input is now filtered. Use SQLite's string concatenation operator || to rebuild the word inside the username field itself: enter adm'||'in'/* as the username. The filter scans the raw input and sees only the fragments 'adm' and 'in', not the complete word 'admin'. The database evaluates the concatenation at query time and matches the admin row.
    bash
    Username: adm'||'in'/*
    bash
    Password: anything
    What didn't work first

    Tried: Using CONCAT('adm','in') instead of the || operator to rebuild the username

    SQLite does not have a CONCAT() function; it uses || for string concatenation. CONCAT() is a MySQL/PostgreSQL function. Calling it in SQLite produces an 'undefined function' error and the query fails. The || operator is the correct SQLite-compatible concatenation syntax.

    Tried: Using 'ADMIN' (uppercase) instead of splitting the word, since the filter might be case-sensitive

    The account is stored as lowercase 'admin', and SQLite's exact-match equality on text is case-sensitive. Log in as 'ADMIN' and no row matches. The split-and-concatenate technique rebuilds exactly 'admin' in lowercase, so the lookup succeeds.

    Learn more

    The resulting SQL is: WHERE username='adm'||'in'/* AND password='anything'. SQLite evaluates 'adm'||'in' as the string admin, which matches the stored username. The /* block comment then discards the password check entirely.

    This technique works because the filter operates on the raw input string before the SQL engine sees it. By the time the database receives the query, the full word has been reconstructed through an operator the filter does not block.

  5. Step 5Round 5 - filter adds union
    Observation
    Round 5 only adds 'union' to the blocklist, and the Round 4 payload never contained UNION. So re-test the same adm'||'in'/* payload before reaching for anything new.
    UNION is now blocked, but the exploit from Round 4 does not use UNION at all. Send exactly the same payload. It succeeds unchanged.
    bash
    Username: adm'||'in'/*
    bash
    Password: anything
    What didn't work first

    Tried: Trying a UNION-based payload to dump the users table since UNION is newly mentioned in the filter

    The UNION keyword is now blocked, so any payload containing it is stripped before reaching the database. More importantly, authentication bypass does not require UNION at all. The Round 4 concatenation-plus-block-comment payload works unchanged in Round 5 because it never used UNION.

    Tried: Crafting a new payload that avoids UNION by using a subquery (SELECT) instead

    Adding UNION to the filter puts no new restriction on the Round 4 technique. Designing a subquery payload from scratch adds complexity for nothing, since adm'||'in'/* already passes all five filters. Re-test the previous working payload before trying anything new.

    Learn more

    The Round 4 payload reconstructs the username via concatenation and comments out the password check. Neither of those techniques involves UNION, so the new filter has no effect on it.

    This progression through five rounds mirrors real-world iterative patching: each restriction closes one technique while leaving others open. The correct fix is parameterized queries, which would have stopped every round with a single change.

  6. Step 6Retrieve the flag from /filter.php
    Observation
    The description says the flag hides in a comment on /filter.php once all rounds are done. So curl or browse that path directly and read the embedded PHP comment.
    After completing all five rounds, navigate to /filter.php on the same host. The page displays the full PHP source code of the filter, including a PHP comment containing the flag.
    bash
    curl http://<host>/filter.php
    Learn more

    The flag is embedded as a comment in the filter source code. The base of the flag is picoCTF{y0u_m4d3_1t_...} where the trailing suffix is a 32-character hex hash unique to your challenge instance. The page also shows all the filters applied across the five rounds, confirming that space was introduced in Round 3 as suspected.

    Leaving diagnostic or filter-listing pages publicly accessible is a Security Misconfiguration vulnerability. In a real application, such pages should require authentication or be removed from production entirely.

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

The trailing suffix is a 32-character hex hash generated per challenge instance (e.g. a5f58d5564fce237fbcc978af033c11b). Your exact flag will differ. Rounds 2-5 use a /* block comment in the username to discard the AND password clause entirely. Rounds 4-5 additionally split the word 'admin' via the || concatenation operator (adm'||'in'/*) so the filter never sees the complete word in the raw input.

Key takeaway

Keyword-based SQL injection filters are broken by design, because SQL offers many equivalent alternatives for any blocked token: -- and /* */ are both comments, OR and || are both disjunctions, and concatenation reassembles blocked words inside the database before the filter ever sees them. A filter working on raw input text is always one substitution away from bypass. Parameterized queries fix the root cause by treating input as data rather than syntax, which makes every technique in this challenge impossible.

Related reading

Tools used in this challenge

Where to go next