phpMyAdmin setup/validate.php live after install
phpMyAdmin 5.2.3 setup/index.php dies when config.inc.php exists. setup/validate.php does not. Unauth POST with a login-page token opens a MySQL-protocol TCP connect to attacker host:port. Labbed.
- Name
- phpMyAdmin setup/validate.php live after install
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- phpMyAdmin
- Affected
- phpMyAdmin 5.2.3 (962857e) when setup/ is still in the docroot. Official phpmyadmin:5.2.3 image deletes setup/. PMASA-2016-44 did not apply the config-exists fatal to validate.php.
- Published
- 12 Oct 2026
- Updated
- 12 Oct 2026
- Tags
- 0-day, phpmyadmin, ssrf, setup, cwe-918, unauthenticated
Setup is disabled. Validate is not.
I am @abraxas_null. The proof of concept is on GitHub: abraxas/phpmyadmin-setup-validate (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is phpMyAdmin 5.2.3 (962857e). No CVE yet. CWE-918. 7.2 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N). Remote SSRF over the MySQL protocol. Unauthenticated if setup/ is still web-reachable. No MySQL login. CSRF token from the public login page.
setup/index.php dies when config.inc.php is present. setup/validate.php does not. It still loads under $isMinimumCommon, takes a token from GET /, and calls Validator::testDBConnection -> mysqli_connect to the host, port, user, and password in the JSON values field. The phpMyAdmin host opens a TCP MySQL handshake to that address.
The official phpmyadmin:5.2.3 image deletes setup/. This leftover only applies when that directory is still in the web root, which is the common leftover after an install. The lab restores setup/ from the 5.2.3 tarball so the pin matches RELEASE_5_2_3.
PMASA-2016-44 / CVE-2016-6621 covered some setup SSRF. The "disable setup when config exists" check was never applied to validate.php / config.php.
Why setup after install
Same wave as WebAuthn. Auth/CSRF/setup was the hunt. Setup is supposed to be a first-run wizard. After config.inc.php exists, the wizard is supposed to die. One script got the fatal. The validator did not. That is a leftover of a control, not a new SSRF primitive.
I only wanted Critical or High. HTTP metadata SSRF would be a different class. This handshake is MySQL. Internal 3306, credential stuffing against an internal server, a port oracle from timing and mysqli errors. Scope C because the PMA host is talking to an address the attacker chose. C:L I:L because the protocol is MySQL, not GET /latest/meta-data.
Pinning RELEASE_5_2_3
Latest 5.2 stable. Tag RELEASE_5_2_3. Commit 962857e. Official image plus setup/ restored from the 5.2.3 tarball. config.inc.php present. Setup UI disabled. Validate still live. TCP oracle on oracle:13306.
The skip list is half the hunt
- PMASA-2025-1 / 2 / 3 already patched in 5.2.2.
- PMASA-2016-44 / CVE-2016-6621 some setup SSRF. The config-exists fatal was the follow-up. It landed on
index.php. It did not land onvalidate.php. Do not re-file 2016. File the script that missed the fatal. - Official
phpmyadmin:5.2.3image deletessetup/. That is a deployment that does not have this leftover. FAIL ifsetup/is not deployed. Not a reason to skip tarball installs that left the directory. - WebAuthn, pmadb SQLi, CSRF
/sqlare different leftovers on this pin.
I am not filing "setup wizard can connect to MySQL." That is the wizard. I am filing the wizard's validator after the wizard is disabled.
Surfaces that returned nothing
File/LFI, deser, transformations, changelog leftovers at this bar came back empty or Medium. Hide-db validateRegex interpolates a user regex into preg_match. That is ReDoS. I did not run it. Inferred, not this SUCCESS. ACME-style HTTP fetch to metadata is not this handshake.
setup/lib/common.inc.php sets $isMinimumCommon and does not look for CONFIG_FILE. Token check still runs. POST without a token does not hit the oracle. That is the negative.
The fatal that only one script has
setup/index.php:
if (@file_exists(CONFIG_FILE) && ! $cfg['DBG']['demo']) {
die('Configuration already exists, setup is disabled!');
}setup/validate.php has no that check. Validator::testDBConnection / mysqli_connect (host, user, password, port, socket from JSON values). Secondary: validateRegex builds preg_match('/' . $values[$path] . '/', ...).
Unauthenticated client GETs /, picks up a session cookie and token, POSTs setup/validate.php id=Server with Servers/1/host and Servers/1/port. The PMA host connects.
The lab
Loopback http://127.0.0.1:19230. Compose project phpmyadmin-setup-validate. Dockerfile restores setup/ from the 5.2.3 tarball and runs a TCP oracle on port 13306.
GET /setup/index.php still said setup is disabled. POST without a token did not hit the oracle. POST with the login-page token connected to oracle:13306 and wrote PMA-SETUP-VALIDATE-WITNESS. Body was mysqli_connect(): (HY000/2006): MySQL server has gone away after the oracle accepted then closed. Peer was the PMA container.
cd lab && ./run.shLast line:
SUCCESS phpmyadmin-setup-validate how=live tcp=yes setup-index-disabled=yes PMA-SETUP-VALIDATE-WITNESSWrong turns, in the order I made them
Treating the official image as this leftover. It deletes setup/. Restore the directory from the tarball or you are labbing Docker Hub, not RELEASE_5_2_3.
POSTing without the login-page token. Zero oracle hits. The token check still runs under $isMinimumCommon. Get the token from GET /. You do not need a MySQL login.
Treating this as HTTP metadata SSRF. The handshake is MySQL. Something on that port has to speak MySQL or you get a protocol error. Internal 3306 is the interesting case. 169.254.169.254 is not, unless it answers MySQL.
Running the ReDoS path and calling it this leftover. hide_db regex is inferred. The SUCCESS is TCP.
A reverse shell. Theatre. The oracle is a TCP accept log on loopback.
What an attacker can do
Anyone who can reach a leftover setup/ tree can make the phpMyAdmin host open a MySQL-protocol TCP connection to a host and port they choose.
| Privilege | Method / path | Lab-proven or inferred |
|---|---|---|
| Open TCP from the PMA host to attacker host:port (integrity of outbound connect; recon of that port) | Unauthenticated POST /setup/validate.php id=Server with Servers/1/host + Servers/1/port plus a token from GET / |
Lab-proven. Oracle on oracle:13306 logged PMA-SETUP-VALIDATE-WITNESS from the PMA container |
| Confirm setup UI is off while validate stays on | GET /setup/index.php |
Lab-proven. Body contains "setup is disabled" / configuration already exists |
| Fail closed without the login-page CSRF token | Same POST, no token |
Lab-proven. Zero oracle hits |
| Port / credential oracle from mysqli errors and timing | Same testDBConnection |
Inferred. Lab body was MySQL server has gone away after the oracle accepted then closed |
| Talk MySQL to an internal 3306 and try posted user/password | Same mysqli_connect |
Inferred. Not pointed at the lab db service |
ReDoS via validateRegex interpolating user regex |
id=Servers/1/hide_db |
Inferred. Not exercised |
Closed on this lab: HTTP metadata SSRF, unauthenticated PMA login, OS shell, and the setup wizard itself (index.php stays disabled). FAIL if setup/ is not deployed.
I am not going to print a host:port you can paste at someone else's PMA. If you own the instance, run the proof of concept against loopback.
The fix
Put the file_exists(CONFIG_FILE) fatal in setup/lib/common.inc.php so every setup script dies. Restrict testDBConnection hosts. Do not build preg_match delimiters from user strings. Delete setup/ from the docroot after install, which the official image already does.
Same pin leftovers: phpmyadmin-webauthn, phpmyadmin-pmadb-central-sqli, phpmyadmin-designer-json-sqli, phpmyadmin-csrf-sql.
References
- Proof of concept: abraxas/phpmyadmin-setup-validate · phpmyadmin-setup-validate-Abraxas-Labs.py
- Lab:
lab/· Dockerfile · docker-compose.yml · run.sh · poc.py - @abraxas_null · github.com/abraxas · abraxaslabs.tech · abraxas.null@proton.me
- CWE-918 · 7.2 High
- Tree: phpmyadmin/phpmyadmin RELEASE_5_2_3 (
962857e) ·setup/index.php·setup/validate.php·Config/Validator.phptestDBConnection