PostgreSQL RCE and Exploitation
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:
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:
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:
Set up the listener before executing:
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:
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:
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.
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:
Export a large object back to disk at a different path:
To write a web shell using this method, first insert the payload into a large object, then export it to the web root:
The hex string above decodes to <?php system($_GET["cmd"]); ?>. Generate it for any payload with:
lo_import and lo_export require superuser. Clean up the large object after use to reduce artifacts:
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.
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:
Or with john:
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:
Then reconnect as the new superuser account and proceed with the techniques above.
Alternatively, if the current user can ALTER existing roles:
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.
Was this helpful?
Your feedback helps improve this page.