
wp2shell (CVE-2026-63030 & CVE-2026-60137) - cadeia RCE completa
Prova de conceito independente para a injeção SQL de confusão de rota do lote REST não autenticado do WordPress associada ao advisory wp2shell da Searchlight Cyber.
Este repositório não é o verificador oficial da Searchlight Cyber. check confirma o caminho do SQLi, read demonstra a leitura do banco de dados e shell abre um shell de comando baseado em plugin, seja com credenciais de administrador fornecidas ou primeiro exercitando a ponte SQLi-to-admin.
O advisory da Searchlight Cyber lista estas faixas de exposição RCE do wp2shell:
| Faixa de versão | Status |
|---|
| <= 6.8.5 | Não afetado |
| 6.9.0 – 6.9.4 | Afetado |
| 7.0.0 – 7.0.1 | Afetado |
O endpoint de lote REST (/batch/v1) não é autenticado e executa várias sub-requisições em uma única chamada, confiando que cada sub-requisição seja validada e verificada quanto a permissões por conta própria.
serve_batch_request_v1() constrói dois arrays paralelos — $matches (o manipulador correspondente por sub-requisição) e $validation (o resultado da validação por sub-requisição) — e então indexa ambos pelo mesmo deslocamento ao despachar. Uma sub-requisição cujo caminho falha em wp_parse_url() é anexada a $validation, mas não a $matches, de modo que os arrays saem de sincronia e uma sub-requisição é despachada sob o manipulador de uma sub-requisição diferente. Essa é a confusão de rota.
O PoC aninha a primitiva duas vezes:
POST /wp/v2/posts que carrega um corpo requests é despachada sob o próprio manipulador de lote. Tendo sido validada como uma requisição de posts, sua lista requests nunca é verificada contra o esquema de lote, portanto suas sub-requisições podem usar GET — a lista de permissão de métodos é contornada.GET /wp/v2/posts/999999 carrega parâmetros de consulta da coleção de posts como author_exclude, orderby e per_page. O ID 999999 não precisa existir; é apenas um ID de post improvável usado para corresponder à rota de item, cujo esquema não valida esses parâmetros exclusivos da coleção. A dessincronização então despacha a mesma requisição sob get_items() de posts, onde author_exclude é mapeado para a variável de consulta author__not_in do WP_Query, que a build vulnerável interpola no SQL como uma string.O resultado é uma injeção SQL cega baseada em booleano e tempo, acessível sem autenticação prévia. Este PoC também inclui a primitiva UNION de post falso usada pela cadeia SQLi-to-admin.
O caminho de RCE implementado aqui é:
wp_posts para renderizar conteúdo controlado pelo atacante através de uma coleção de posts. A ponte de renderização usa a fonte da rota de item /wp/v2/posts/999999 — a mesma rota que a leitura do SQLi utiliza para alcançar get_items().POST /wp/v2/users, criando um administrador gerado.As etapas 1–5 são sem autenticação; a etapa de execução de comando é upload de plugin pelo administrador autenticado.
Python 3.8+ e a biblioteca padrão. Nenhuma dependência de terceiros.
Execute a partir do diretório do repositório:
./wp2shell.py <command> <url> [options]
Ou pip install . para obter um comando wp2shell no seu PATH.
Primeiro imprime marcadores passivos do WordPress e dicas de versão pública, depois envia uma sonda benigna de marcadores de lote. Uma implementação de lote vulnerável retorna HTTP 207 com o padrão de marcadores de confusão de rota parse_path_failed, block_cannot_read e rest_batch_not_allowed.
A sonda de marcadores é baseada na correção do núcleo do WordPress. A requisição malformada /// cria parse_path_failed; uma requisição /wp/v2/posts atua como um espaçador permitido em lote; a rota /wp/v2/block-renderer/... não é permitida em lote, mas retorna block_cannot_read se seu manipulador for alcançado anonimamente; /batch/v1 fornece rest_batch_not_allowed. Em builds vulneráveis, o erro de análise desloca os arrays do manipulador de lote para fora de sincronia, então a requisição espaçadora é despachada sob o manipulador block-renderer. As builds corrigidas mantêm os arrays alinhados, portanto esse padrão exato de três marcadores não deve aparecer para a sonda criada.
Por padrão, check para por aí e não envia um payload SQLi. Use --confirm-sqli quando também quiser uma confirmação ativa de SQLi. A confirmação tenta primeiro a primitiva de leitura UNION e recorre a sondas de temporização emparelhadas se a reflexão UNION não estiver disponível.
Os sinais são independentes: uma dica de versão é apenas uma dica, o padrão de marcadores mostra confusão de rota, e --confirm-sqli mostra que um payload chegou ao banco de dados. Um WAF pode bloquear o payload, portanto uma confirmação falha não prova que o bug está ausente.
./wp2shell.py check http://target
./wp2shell.py check targets.txt # scan every URL in the file
./wp2shell.py read http://target # fingerprint do servidor
./wp2shell.py read http://target --preset users # logins de usuários e hashes de senha
./wp2shell.py read http://target --query "SELECT @@version"
Por padrão, a extração é --technique auto, que tenta os métodos disponíveis nesta ordem:
WP_Post via UNION e lê seu título de volta da resposta REST como ||HEX(value)||. O payload usa a mesma rota de origem /wp/v2/posts/999999 com orderby=none e per_page=500 para que a linha falsa sobreviva como um post renderizado. Uma requisição por valor.EXTRACTVALUE/UPDATEXML vaza ~15 bytes por requisição, quando o alvo reflete erros MySQL (por exemplo, WP_DEBUG_DISPLAY ativado).X-WP-Total da coleção de posts como sinal verdadeiro/falso e não precisa de valor refletido.Force um com --technique union|error|blind. Estes caminhos de leitura não escrevem linhas no banco de dados.
Com --user e --password, shell faz login com credenciais de administrador fornecidas e usa o comportamento de upload de plugin do WordPress.
Sem credenciais, shell primeiro executa a ponte SQLi-to-admin sem autenticação, faz login como o administrador gerado e então faz upload do shell do plugin.
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # shell interativo
./wp2shell.py shell http://target --cmd id # ponte sem autenticação
./wp2shell.py shell http://target -i # interativo sem autenticação
shell faz upload de um webshell de plugin (bloqueado por um caminho aleatório e um token por execução) e imprime seu caminho. O webshell carregado é removido automaticamente. Quando a ponte sem autenticação cria um administrador, essa conta gerada é removida automaticamente após o término da sessão de shell.
| Opção | Aplica-se a | Descrição |
|---|---|---|
--proxy URL | todos | Roteie o tráfego através de um proxy HTTP (por exemplo, Burp). |
--timeout N | todos | Tempo limite de requisição em segundos. |
--sleep N | check | Atraso usado pela fallback de temporização para --confirm-sqli. |
--samples N | check | Pares de temporização usados pela fallback de temporização para --confirm-sqli. |
--confirm-sqli | check | Também enviar um payload de confirmação SQLi ativo. |
--preset | read | fingerprint ou users. |
--technique | read | auto (padrão), union (in-band, forja um post falso), error (in-band, precisa de erros DB visíveis), ou blind. |
--query | read | Uma expressão SQL escalar para ler. |
--prefix | read | Prefixo da tabela do banco de dados (padrão wp_). |
--max-length N | read | Máximo de caracteres lidos por valor (padrão 128). |
--user / --password | shell | Credenciais de administrador opcionais; omita ambas para usar a ponte sem autenticação. |
--cmd | shell | Comando a executar (omitir ao usar -i). |
-i / --interactive | shell | Abrir um shell interativo após a implantação. |
Atualize para WordPress 7.0.2, ou 6.9.5 se o site estiver no branch 6.9. Até lá, bloqueie tanto /wp-json/batch/v1 quanto o parâmetro de consulta rest_route=/batch/v1 na borda, ou exija autenticação para o endpoint de lote através do filtro rest_pre_dispatch.
Apenas para testes de segurança autorizados. Use-o exclusivamente contra sistemas que você possui ou para os quais tem permissão explícita por escrito para testar. Nenhuma garantia é fornecida e nenhuma responsabilidade é aceita por uso indevido.