
phpMyAdmin 5.2.3 - Alta - Remota - XSS - Generador de consultas visuales Designer
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · phpmyadmin-vqb-xss
Write-up: https://abraxaslabs.tech/research/phpmyadmin-vqb-xss
Clase: XSS almacenado Alcance: Remoto
phpMyAdmin 5.2.3 (RELEASE_5_2_3 / 962857e) - phpMyAdmin
js/src/designer/history.js construye el historial de consultas visuales con innerHTML. Los nombres de columna pasan por $('<div/>').text(...).html(). Los nombres de tabla no. TableName todavía permite < y >. Una cuenta con pocos privilegios con CREATE TABLE sobre una base de datos compartida planta un identificador HTML. Cuando un usuario con más privilegios abre el Designer, getTableName() está en la página y DesignerHistory.display() lo escribe en #ab.
Un XSS almacenado en el origen de phpMyAdmin. Misma clase que PMASA-2025-1, funcionalidad distinta. Los encabezados de check-tables estaban escapados. Esta ruta no lo estaba.
| ID | aún sin CVE |
| Clase | XSS almacenado |
| Alcance | Remoto (CREATE TABLE autenticado; la víctima abre el Designer) |
| CWE | CWE-79 |
| CVSS | Alto: 8.0 CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N |
| Producto | phpMyAdmin |
| Afectado | 5.2.3 tag RELEASE_5_2_3 (962857e4f63d42e38f11ff4d63f5e722018add76) |
| Autenticación | autenticado. El atacante necesita CREATE TABLE sobre una base de datos que la víctima abra en el Designer |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1 |
Plantar un nombre de tabla HTML y luego esperar a que un usuario con más privilegios abra el Designer sobre esa base de datos y renderice el historial de consultas visuales.
| Privilegio | Método / sink | Reconocimiento / integridad / disponibilidad | Probado en laboratorio o inferido |
|---|---|---|---|
| Inyectar marcado en el origen de phpMyAdmin | DesignerHistory.display() concatena getTab() en el innerHTML de #ab | Integridad del origen de la víctima | Probado en laboratorio. history.js en vivo sobre phpmyadmin:5.2.3. <h3> contenía <i id="pma-vqb"> |
Escaparse de un atributo table_name del designer | database_tables.twig small_tab_pref_1 usa designerTable.getTableName() | Reconocimiento del nombre MySQL en bruto en el DOM del Designer | Probado en laboratorio, legible como el identificador en bruto |
Leer la cookie de sesión de la víctima y el token CSRF | Script del mismo origen una vez que se renderiza el historial | Reconocimiento de la sesión PMA de la víctima | Inferido a partir del XSS almacenado en el origen de PMA. Este laboratorio usa un marcador <i>, no un script |
| Ejecutar SQL, gestionar usuarios, exportar, como la cuenta MySQL de la víctima | POST respaldado por token a /sql, /server/privileges, export | Integridad de todo lo que esa cuenta MySQL pueda tocar | Inferido. Misma clase de impacto que PMASA-2025-1 |
| Cambiar 2FA / userconfig | POST a /config/set como la víctima | Integridad del almacén de preferencias de la víctima | Inferido. Resto hermano, no este laboratorio |
Descartado en este resto: acceso no autenticado, RCE del SO y la ruta del botón OK del VQB. .small_tab_pref_click_opt pre-escapa table_name y Twig lo autoescapa de nuevo, así que ese clic actualmente alimenta entidades a getTab(). Eso no es una corrección de history.js. Las celdas de columna siguen escapadas como texto (c<b id=pma-col>y, sin nodo <b id="pma-col">).
Mismo pin: resto de WebAuthn CustomServer.
Fijé phpMyAdmin RELEASE_5_2_3. PMASA-2025-1/2/3 ya están en 5.2.2. El Designer sigue construyendo el HTML del historial del VQB a mano.
js/src/designer/history.js (distribuido como 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 solo comprueba que no esté vacío, máximo 64, sin espacio final. < y > son legales. MySQL los acepta en un identificador entre comillas.
templates/database/designer/database_tables.twig copia designerTable.getTableName() en table_name sobre .small_tab_pref_1. La celda de opción de VQB usa una copia preescapada del mismo nombre.
Falsos caminos ya registrados: tratar el atributo del botón de opción con doble escape como la solución (no lo es - small_tab_pref_1 sigue exponiendo el nombre sin procesar, y history.js es el sink); tratar el helper de columna como si cubriera los nombres de tabla; afirmar XSS no autenticado; una reverse shell. Teatro.
python3 phpmyadmin-vqb-xss-Abraxas-Labs.py
# or: cd lab && ./run.sh
Dirígete solo a http://127.0.0.1:19250. Imágenes phpmyadmin:5.2.3 y mysql:8.0. Proyecto de Compose phpmyadmin-vqb-xss. Playwright Chromium en el servicio poc. El stack se desmonta tras la ejecución.
Dos usuarios, shareddb compartida. El usuario de bajos privilegios creator ejecuta CREATE TABLE \tx` (`cy` INT). El usuario de mayores privilegios victiminicia sesión en phpMyAdmin y abre Designer conquery=1. El history.js en vivo es la copia del contenedor (h3-unescaped=yes, el helper de columna presente). Chromium llama a DesignerHistory.display()con el nombre de tabla degetTableName()` en la página.
Resultado: el innerHTML de #ab h3.tiger es <a href="#">t<i id="pma-vqb">x</i></a>. El texto del td de la columna es c<b id=pma-col>y sin ningún nodo b#pma-col.
SUCCESS phpmyadmin-vqb-xss h3-markup=yes col-escaped=yes how=live PMA-VQB-XSS-WITNESS
Escapa getTab() con $('<div/>').text(temp).html() o Functions.escapeHtml antes de concatenar en el <h3>. No confíes en la doble codificación de Twig de un atributo. Mantén el helper de nombre de columna como el modelo.
962857e4f63d42e38f11ff4d63f5e722018add76)js/src/designer/history.jslibraries/classes/Dbal/TableName.phpGNU Affero GPL v3.0. Consulta LICENSE. Solo para laboratorio de loopback. Sin garantía.