
phpMyAdmin 5.2.3 - Alta - Remota - XSS - Designer Visual Query Builder
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · phpmyadmin-vqb-xss
Write-up: https://abraxaslabs.tech/research/phpmyadmin-vqb-xss
Classe: XSS Armazenado Alcance: Remoto
phpMyAdmin 5.2.3 (RELEASE_5_2_3 / 962857e) - phpMyAdmin
js/src/designer/history.js constrói o histórico de consultas visuais com innerHTML. Os nomes de coluna passam por $('<div/>').text(...).html(). Os nomes de tabela não. TableName ainda permite < e >. Uma conta com baixos privilégios com CREATE TABLE numa base de dados partilhada planta um identificador HTML. Quando um utilizador com privilégios superiores abre o Designer, getTableName() está na página e DesignerHistory.display() escreve-o em #ab.
Um XSS armazenado na origem do phpMyAdmin. Mesma classe que PMASA-2025-1, funcionalidade diferente. Os cabeçalhos de check-tables foram escapados. Este caminho não foi.
| ID | ainda sem CVE |
| Classe | XSS Armazenado |
| Alcance | Remoto (CREATE TABLE autenticado; a vítima abre o 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 |
| Produto | phpMyAdmin |
| Afetado | 5.2.3 tag RELEASE_5_2_3 (962857e4f63d42e38f11ff4d63f5e722018add76) |
| Autenticação | autenticado. O atacante precisa de CREATE TABLE numa base de dados que a vítima abra no Designer |
| Licença | GNU Affero GPL v3.0 |
| Laboratório | apenas 127.0.0.1 |
Plantar um nome de tabela HTML e depois esperar que um utilizador com privilégios superiores abra o Designer nessa base de dados e renderize o histórico de consultas visuais.
| Privilégio | Método / sink | Recon / integridade / disponibilidade | Comprovado em laboratório ou inferido |
|---|---|---|---|
| Injetar markup na origem do phpMyAdmin | DesignerHistory.display() concatena getTab() no innerHTML de #ab | Integridade da origem da vítima | Comprovado em laboratório. history.js em execução no phpmyadmin:5.2.3. <h3> continha <i id="pma-vqb"> |
Sair de um atributo table_name do designer | database_tables.twig small_tab_pref_1 usa designerTable.getTableName() | Recon do nome MySQL em bruto no DOM do Designer | Comprovado em laboratório como legível como o identificador em bruto |
Ler o cookie de sessão da vítima e o token CSRF | Script de mesma origem assim que o histórico é renderizado | Recon da sessão PMA da vítima | Inferido a partir do XSS armazenado na origem PMA. Este laboratório usa um marcador <i>, não um script |
| Executar SQL, gerir utilizadores, exportar, como a conta MySQL da vítima | POST /sql, /server/privileges, exportação com token | Integridade de tudo o que essa conta MySQL possa tocar | Inferido. Mesma classe de impacto que PMASA-2025-1 |
| Alterar 2FA / userconfig | POST /config/set como a vítima | Integridade do armazenamento de preferências da vítima | Inferido. Resíduo irmão, não este laboratório |
Fechado neste resíduo: acesso não autenticado, RCE no SO e o caminho do botão OK do VQB. .small_tab_pref_click_opt pré-escapa table_name e o Twig volta a escapá-lo automaticamente, pelo que esse clique atualmente alimenta entidades em getTab(). Isso não é uma correção do history.js. As células de coluna continuam com texto escapado (c<b id=pma-col>y, sem nó <b id="pma-col">).
Mesmo pin: resíduo do WebAuthn CustomServer.
Fixe o phpMyAdmin RELEASE_5_2_3. PMASA-2025-1/2/3 já estão na 5.2.2. O Designer ainda constrói o HTML do histórico VQB à mão.
js/src/designer/history.js (distribuído 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 verifica apenas se não está vazio, máximo de 64, sem espaço no final. < e > são legais. O MySQL os aceita em um identificador entre aspas.
templates/database/designer/database_tables.twig copia designerTable.getTableName() para table_name em .small_tab_pref_1. A célula de opção do VQB usa uma cópia pré-escapada do mesmo nome.
Caminhos errados já registrados: tratar o atributo do botão de opção com escape duplo como a correção (não é - small_tab_pref_1 ainda expõe o nome bruto, e history.js é o sink); tratar o helper de coluna como cobrindo nomes de tabela; alegar XSS não autenticado; um reverse shell. Teatro.
python3 phpmyadmin-vqb-xss-Abraxas-Labs.py
# or: cd lab && ./run.sh
Aponte apenas para http://127.0.0.1:19250. Imagens phpmyadmin:5.2.3 e mysql:8.0. Projeto Compose phpmyadmin-vqb-xss. Playwright Chromium no serviço poc. A stack é desmontada após a execução.
Dois usuários, shareddb compartilhado. O creator de baixo privilégio executa CREATE TABLE \tx` (`cy` INT). O victimde privilégio mais alto faz login no phpMyAdmin e abre o Designer comquery=1. O history.js em execução é a cópia do container (h3-unescaped=yes, helper de coluna presente). O Chromium chama DesignerHistory.display()com o nome da tabela obtido degetTableName()` na página.
Resultado: o innerHTML de #ab h3.tiger é <a href="#">t<i id="pma-vqb">x</i></a>. O texto do td da coluna é c<b id=pma-col>y sem nenhum nó b#pma-col.
SUCCESS phpmyadmin-vqb-xss h3-markup=yes col-escaped=yes how=live PMA-VQB-XSS-WITNESS
Escapar getTab() com $('<div/>').text(temp).html() ou Functions.escapeHtml antes de concatenar no <h3>. Não confie na dupla codificação do Twig de um único atributo. Mantenha o helper de nome de coluna como modelo.
962857e4f63d42e38f11ff4d63f5e722018add76)js/src/designer/history.jslibraries/classes/Dbal/TableName.phpGNU Affero GPL v3.0. Consulte LICENSE. Apenas laboratório em loopback. Sem garantias.