Web Gauntlet picoCTF 2020 Mini-Competition Solution

Published: April 2, 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 1
    Round 1 - filter: or
    Observation
    I noticed filter.php showed only the keyword 'or' was blocked, which suggested that a SQL comment (--) could truncate the query after the username and bypass the password check without needing any boolean operator 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 the word 'or' (case-insensitive), so the payload is stripped or rejected before the query runs. The server returns an error or simply fails to log in. The '--' comment approach works because it does not require any 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 a comment syntax in MySQL but the database here is SQLite, which does not recognize # as a comment marker. The query syntax error causes login to fail. SQLite supports - line comments and /* */ block comments, so - is the correct syntax to 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 2
    Round 2 - filter: or, like, =, --
    Observation
    I noticed the Round 2 filter.php added '--' and '=' to the blocklist, which suggested switching to C-style block comments (/* */) that were not yet filtered and could 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 explicitly adds - to the blocklist, so the payload is rejected. The filter.php page shows the updated list; - is now filtered. Block comments (/* */) are the correct alternative since they achieve the same comment effect 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 hits two filter rules at once. Even bypassing one would not help because the other still triggers. The block comment approach sidesteps both restrictions entirely 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 3
    Round 3 - filter adds space
    Observation
    I noticed the updated filter.php added a space character to the blocklist, which suggested using the cross-field block comment trick (username opens /* and password supplies */) so no spaces are needed 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 active, the password field value 'anything' does not close the block comment that was opened in the username. The resulting SQL has an unclosed /* which causes a parse error and login fails. The password must supply the */ to close the comment and make the query syntactically valid.

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

    The filter implementation in this challenge blocks the space character specifically, but tab is also whitespace that SQL accepts as a separator. However, HTML form fields strip or normalize whitespace inconsistently, and the exact filter behavior may also catch tabs. The block comment approach avoids the need for any whitespace character in the injected tokens entirely.

    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 4
    Round 4 - filter adds 'admin'
    Observation
    I noticed filter.php added the literal string 'admin' to the blocklist, which suggested using SQLite's || concatenation operator to split the word into fragments ('adm'||'in') that the filter never recognizes as the 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 admin account in the database is stored as lowercase 'admin', and SQLite's WHERE clause comparisons on text are case-sensitive by default for the LIKE operator but exact-match equality (=) is also case-sensitive. Logging in as 'ADMIN' finds no matching row. The split-and-concatenate technique reconstructs exactly 'admin' (lowercase) so the row 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 5
    Round 5 - filter adds union
    Observation
    I noticed filter.php only added 'union' to the Round 5 blocklist, and reviewing the Round 4 payload showed it never contained UNION, which suggested re-testing the same adm'||'in'/* payload before attempting any new approach.
    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 does not introduce any new restriction on the Round 4 bypass technique. Attempting to design a subquery-based payload from scratch adds unnecessary complexity when the existing adm'||'in'/* payload already passes all five round filters. Always re-test the previous working payload before trying a new approach.

    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 6
    Retrieve the flag from /filter.php
    Observation
    I noticed the challenge description stated the flag is hidden in a comment on /filter.php after completing all rounds, which suggested curling or browsing that path directly to read the embedded PHP comment containing the flag.
    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 fundamentally broken because SQL parsers offer many syntactically equivalent alternatives for any blocked token: -- and /* */ are both comment styles, OR and || are both disjunctions, and string concatenation can reassemble blocked words at the database layer before the filter ever sees them. Filters that operate on raw input text are always one creative substitution away from bypass. Parameterized queries fix the root cause by treating user input as data rather than SQL syntax, making every bypass technique in this challenge impossible regardless of how clever the payload is.

Related reading

Want more picoCTF 2020 Mini-Competition writeups?

Tools used in this challenge

What to try next