
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.