
phpMyAdmin 5.2.3 - Élevé - À distance - XSS - Designer Visual Query Builder
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · phpmyadmin-vqb-xss
Classe : XSS stocké Portée : Distante
phpMyAdmin 5.2.3 (RELEASE_5_2_3 / 962857e) - phpMyAdmin
js/src/designer/history.js construit l'historique des requêtes visuelles avec innerHTML. Les noms de colonnes passent par $('<div/>').text(...).html(). Les noms de tables, non. TableName autorise toujours < et >. Un compte à faibles privilèges disposant de CREATE TABLE sur une base de données partagée plante un identifiant HTML. Lorsqu'un utilisateur à privilèges plus élevés ouvre le Designer, getTableName() se trouve sur la page et DesignerHistory.display() l'écrit dans #ab.
Un XSS stocké dans l'origine phpMyAdmin. Même classe que PMASA-2025-1, fonctionnalité différente. Les en-têtes Check-tables étaient échappés. Ce chemin ne l'était pas.
| ID | pas encore de CVE |
| Classe | XSS stocké |
| Portée | Distante (CREATE TABLE authentifié ; la victime ouvre le Designer) |
| CWE | CWE-79 |
| CVSS | Élevé : 8.0 CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N |
| Produit | phpMyAdmin |
| Affecté | 5.2.3 tag RELEASE_5_2_3 (962857e4f63d42e38f11ff4d63f5e722018add76) |
| Auth | authentifié. L'attaquant a besoin de CREATE TABLE sur une base de données que la victime ouvre dans le Designer |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Planter un nom de table HTML, puis attendre qu'un utilisateur à privilèges plus élevés ouvre le Designer sur cette base de données et affiche l'historique des requêtes visuelles.
| Privilège | Méthode / sink | Reconnaissance / intégrité / disponibilité | Prouvé en lab ou déduit |
|---|---|---|---|
| Injecter du balisage dans l'origine phpMyAdmin | DesignerHistory.display() concatène getTab() dans l'innerHTML de #ab | Intégrité de l'origine de la victime | Prouvé en lab. history.js en direct sur phpmyadmin:5.2.3. <h3> contenait <i id="pma-vqb"> |
Sortir d'un attribut table_name du designer | database_tables.twig small_tab_pref_1 utilise designerTable.getTableName() | Reconnaissance du nom MySQL brut dans le DOM du Designer | Prouvé en lab, lisible comme identifiant brut |
Lire le cookie de session de la victime et le token CSRF | Script de même origine une fois l'historique affiché | Reconnaissance de la session PMA de la victime | Déduit du XSS stocké dans l'origine PMA. Ce lab utilise un marqueur <i>, pas un script |
| Exécuter du SQL, gérer les utilisateurs, exporter, en tant que compte MySQL de la victime | POST /sql, /server/privileges, export, avec token | Intégrité de tout ce que ce compte MySQL peut toucher | Déduit. Même classe d'impact que PMASA-2025-1 |
| Modifier la 2FA / userconfig | POST /config/set en tant que victime | Intégrité du magasin de préférences de la victime | Déduit. Reliquat voisin, pas ce lab |
Fermé sur ce reliquat : accès non authentifié, RCE OS, et le chemin du bouton OK du VQB. .small_tab_pref_click_opt pré-échappe table_name et Twig l'auto-échappe à nouveau, donc ce clic alimente actuellement getTab() en entités. Ce n'est pas un correctif de history.js. Les cellules de colonnes restent échappées en texte (c<b id=pma-col>y, pas de nœud <b id="pma-col">).
Même pin : Reliquat WebAuthn CustomServer.
J'ai épinglé phpMyAdmin RELEASE_5_2_3. PMASA-2025-1/2/3 sont déjà dans 5.2.2. Le Designer construit toujours le HTML de l'historique VQB à la main.
js/src/designer/history.js (livré sous js/dist/designer/history.js) :
temp = historyArray[i].getTab();
str += '<h3 class="tiger"><a href="#">' + temp + '</a></h3>';
// ...
'<td width="175" style="padding-left: 5px">' + $('<div/>').text(historyArray[i].getColumnName()).html() + '<td>';
libraries/classes/Dbal/TableName.php vérifie seulement que le nom n'est pas vide, fait au maximum 64 caractères, sans espace final. < et > sont légaux. MySQL les accepte dans un identifiant entre guillemets.
templates/database/designer/database_tables.twig copie designerTable.getTableName() dans table_name sur .small_tab_pref_1. La cellule d'option du VQB utilise une copie pré-échappée du même nom.
Fausses pistes déjà consignées : traiter l'attribut doublement échappé du bouton d'option comme le correctif (ce n'est pas le cas - small_tab_pref_1 expose toujours le nom brut, et history.js est le sink) ; traiter l'helper de colonne comme couvrant les noms de tables ; prétendre à un XSS non authentifié ; un reverse shell. Du théâtre.
python3 phpmyadmin-vqb-xss-Abraxas-Labs.py
# or: cd lab && ./run.sh
Cible uniquement http://127.0.0.1:19250. Images phpmyadmin:5.2.3 et mysql:8.0. Projet Compose phpmyadmin-vqb-xss. Chromium Playwright dans le service poc. La stack est démontée après l'exécution.
Deux utilisateurs, shareddb partagée. Le compte à faibles privilèges creator exécute CREATE TABLE \tx` (`cy` INT). Le compte à privilèges plus élevés victimse connecte à phpMyAdmin et ouvre le Designer avecquery=1. history.js en direct est la copie issue du conteneur (h3-unescaped=yes, helper de colonne présent). Chromium appelle DesignerHistory.display()avec le nom de table issu degetTableName()` sur la page.
Résultat : l'innerHTML de #ab h3.tiger est <a href="#">t<i id="pma-vqb">x</i></a>. Le texte de la td de colonne est c<b id=pma-col>y sans nœud b#pma-col.
SUCCESS phpmyadmin-vqb-xss h3-markup=yes col-escaped=yes how=live PMA-VQB-XSS-WITNESS
Échapper getTab() avec $('<div/>').text(temp).html() ou Functions.escapeHtml avant de concaténer dans le <h3>. Ne pas se fier au double encodage Twig d'un seul attribut. Conserver l'helper de nom de colonne comme modèle.
962857e4f63d42e38f11ff4d63f5e722018add76)js/src/designer/history.jslibraries/classes/Dbal/TableName.phpGNU Affero GPL v3.0. Voir LICENSE. Lab en boucle locale uniquement. Aucune garantie.