
phpMyAdmin 5.2.3 - Alta - Remota - XSS - Designer Visual Query Builder
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · 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 das colunas passam por $('<div/>').text(...).html(). Os nomes das tabelas não. TableName ainda permite < e >. Uma conta com privilégios baixos com CREATE TABLE numa base de dados partilhada insere um identificador HTML. Quando um utilizador com privilégios mais elevados 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 abre no Designer |
| Licença | GNU Affero GPL v3.0 |
| Laboratório | apenas 127.0.0.1 |
Inserir um nome de tabela HTML e depois esperar que um utilizador com privilégios mais elevados abra o Designer nessa base de dados e renderize o histórico de consultas visuais.
| Privilégio | Método / sink | Reconhecimento / integridade / disponibilidade | Comprovado em laboratório ou inferido |
|---|---|---|---|
| Injetar marcação 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() | Reconhecimento do nome MySQL em bruto no DOM do Designer | Comprovado em laboratório, 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 | Reconhecimento da sessão PMA da vítima | Inferido a partir de 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 com token para /sql, /server/privileges, exportação | 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 semelhante, 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 das colunas permanecem com texto escapado (c<b id=pma-col>y, sem nó <b id="pma-col">).
Mesmo pin: Resíduo do WebAuthn CustomServer.
Fixeio 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 apenas verifica se não está vazio, máximo 64, sem espaço à direita. < e > são legais. O MySQL aceita-os num 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á registados: tratar o atributo do botão de opção com duplo escape como a correção (não é - small_tab_pref_1 ainda expõe o nome em bruto, e history.js é o sink); tratar o auxiliar de colunas como cobrindo nomes de tabelas; alegar XSS não autenticado; uma reverse shell. Teatro.
python3 phpmyadmin-vqb-xss-Abraxas-Labs.py
# or: cd lab && ./run.sh
Alvo apenas 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 utilizadores, shareddb partilhada. O creator com privilégios baixos executa CREATE TABLE \tx` (`cy` INT). O victimcom privilégios mais elevados inicia sessão no phpMyAdmin e abre o Designer comquery=1. O history.js em execução é a cópia do contentor (h3-unescaped=yes, auxiliar de colunas presente). O Chromium chama DesignerHistory.display()com o nome da tabela degetTableName()` na página.
Resultado: o innerHTML de #ab h3.tiger é <a href="#">t<i id="pma-vqb">x</i></a>. O texto da td da coluna é c<b id=pma-col>y sem 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 confiar na dupla codificação do Twig de um atributo. Manter o auxiliar de nomes de colunas como modelo.
962857e4f63d42e38f11ff4d63f5e722018add76)js/src/designer/history.jslibraries/classes/Dbal/TableName.phpGNU Affero GPL v3.0. Ver LICENSE. Apenas laboratório em loopback. Sem garantia.