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:
- COPY TO PROGRAM: OS Command Execution
- pg_read_file: Arbitrary File Read
- Large Object File Write
- UDF RCE via Custom Extension: Linux
- Password Hash Extraction
- Privilege Escalation via CREATEROLE
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. Compile a .so with a function that executes shell commands, write it to disk using large object export, then register it with CREATE FUNCTION. Requires superuser and write access to a path PostgreSQL loads libraries from (typically /usr/lib/postgresql/<version>/lib/ or a path in dynamic_library_path).
This technique is complex and noisy: it leaves a binary on disk, a new function in pg_proc, and potentially a new extension entry. Prefer COPY TO PROGRAM whenever possible.
Step 1: Compile the Shared Library
Create the C source on your attacker machine. This defines a PostgreSQL-compatible function that passes a string argument to system():
#include "postgres.h"
#include "fmgr.h"
#include <stdlib.h>
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
PG_FUNCTION_INFO_V1(sys_exec);
Datum sys_exec(PG_FUNCTION_ARGS) {
char *command = text_to_cstring(PG_GETARG_TEXT_P(0));
PG_RETURN_INT32(system(command));
}
Compile against the PostgreSQL server headers. Match the target's PostgreSQL major version:
Explain command
| -shared | Build a shared library (.so) instead of an executable. |
| -fPIC | Position-independent code, required for shared libraries. |
If pg_config is not available on the attacker machine, use the header path directly. For PostgreSQL 14 on Debian/Ubuntu: -I/usr/include/postgresql/14/server. For cross-compilation targeting a different architecture, install the matching postgresql-server-dev-<version> package.
Step 2: Deploy via Large Objects
Convert the compiled library to hex and inject through the SQL session. First, determine the target library path:
setting ---------------------------- /usr/lib/postgresql/14/lib
Convert the .so to hex on the attacker machine:
Write the hex payload to disk on the target through large objects. Each pageno holds up to 2048 bytes of decoded binary (4096 hex characters). Split the hex file and paste each chunk:
Step 3: Register and Execute
Create the SQL function pointing to the deployed library:
CREATE FUNCTION
Execute commands through the function. Reverse shell:
The function returns the exit code of the command (0 = success).
Cleanup
Remove artifacts after obtaining access:
Delete the .so from disk through a shell if you have execution. The function registration in pg_proc is removed by DROP FUNCTION but database logs may still contain the CREATE and execution statements.
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.