
wp2shell (CVE-2026-63030 & CVE-2026-60137) - cadena RCE completa
Prueba de concepto independiente para la inyección SQL por confusión de rutas en el lote REST de WordPress sin autenticación, asociada al aviso wp2shell de Searchlight Cyber.
Este repositorio no es el verificador oficial de Searchlight Cyber. check confirma la ruta del SQLi, read demuestra la lectura de la base de datos, y shell abre un shell de comandos respaldado por un plugin, ya sea con credenciales de administrador proporcionadas o ejercitando primero el puente de SQLi a admin.
El aviso de Searchlight Cyber enumera estos rangos de exposición a RCE de wp2shell:
| Rango de versiones | Estado |
|---|---|
| <= 6.8.5 | No afectada |
| 6.9.0 – 6.9.4 | Afectada |
| 7.0.0 – 7.0.1 | Afectada |
El endpoint de lote REST (/batch/v1) no requiere autenticación y ejecuta varias sub-solicitudes en una sola llamada, confiando en que cada sub-solicitud sea validada y verificada en cuanto a permisos por sí misma.
serve_batch_request_v1() construye dos arrays paralelos — $matches (el manejador coincidente por sub-solicitud) y $validation (el resultado de validación por sub-solicitud) — y luego indexa ambos por el mismo offset al despachar. Una sub-solicitud cuya ruta falla en wp_parse_url() se añade a $validation pero no a $matches, de modo que los arrays se desincronizan y una sub-solicitud se despacha bajo el manejador de una sub-solicitud diferente. Eso es la confusión de rutas.
El PoC anida la primitiva dos veces:
POST /wp/v2/posts que lleva un cuerpo requests se despacha bajo el propio manejador de lote. Al haber sido validada como solicitud de posts, su lista requests nunca se comprueba contra el esquema del lote, por lo que sus sub-solicitudes pueden usar GET — la lista de métodos permitidos se omite.GET /wp/v2/posts/999999 lleva parámetros de consulta de la colección de posts como author_exclude, orderby y per_page. El ID 999999 no necesita existir; es solo un ID de post improbable usado para coincidir con la ruta de ítem, cuyo esquema no valida esos parámetros exclusivos de colección. La desincronización entonces despacha la misma solicitud bajo get_items() de posts, donde author_exclude se asigna a la variable de consulta author__not_in de , que la build vulnerable interpola en SQL como cadena.El resultado es una inyección SQL ciega basada en booleano y en tiempo, alcanzable sin autenticación. Este PoC también incluye la primitiva de post falso por UNION utilizada en la cadena de SQLi a admin.
La ruta de RCE implementada aquí es:
wp_posts por UNION para renderizar contenido controlado por el atacante a través de una colección de posts. El puente de renderizado usa la fuente de ruta de ítem /wp/v2/posts/999999 — la misma ruta que usa la lectura SQLi para alcanzar get_items().POST /wp/v2/users, creando un administrador generado.Los pasos 1–5 son sin autenticación; el paso de ejecución de comandos es una carga de plugin autenticada como administrador.
Python 3.8+ y la biblioteca estándar. Sin dependencias de terceros.
Ejecútalo desde el directorio del repositorio:
./wp2shell.py <command> <url> [options]
O pip install . para obtener un comando wp2shell en tu PATH.
Primero imprime marcadores pasivos de WordPress y pistas públicas de versión, y luego envía una sonda benigna de marcadores de lote. Una implementación de lote vulnerable devuelve HTTP 207 con el patrón de marcadores de confusión de rutas parse_path_failed, block_cannot_read y rest_batch_not_allowed.
La sonda de marcadores se basa en la corrección del núcleo de WordPress. La solicitud malformada /// crea parse_path_failed; una solicitud /wp/v2/posts actúa como espaciador permitido en el lote; la ruta /wp/v2/block-renderer/... no está permitida en el lote pero devuelve block_cannot_read si su manejador se alcanza de forma anónima; /batch/v1 da rest_batch_not_allowed. En builds vulnerables, el error de parseo desincroniza los arrays del manejador de lote, por lo que la solicitud espaciadora se despacha bajo el manejador de block-renderer. Las builds corregidas mantienen los arrays alineados, por lo que este patrón exacto de los tres no debería aparecer para la sonda elaborada.
Por defecto, check se detiene ahí y no envía un payload de SQLi. Usa --confirm-sqli cuando también quieras una confirmación activa de SQLi. La confirmación intenta primero la primitiva de lectura por UNION y recurre a sondas de temporización emparejadas si la reflexión por UNION no está disponible.
Las señales son independientes: una pista de versión es solo una pista, el patrón de marcadores muestra confusión de rutas, y --confirm-sqli muestra que un payload llegó a la base de datos. Un WAF puede bloquear el payload, por lo que una confirmación fallida no demuestra que el fallo esté ausente.
./wp2shell.py check http://target
./wp2shell.py check targets.txt # scan every URL in the file
./wp2shell.py read http://target # server fingerprint
./wp2shell.py read http://target --preset users # user logins and password hashes
./wp2shell.py read http://target --query "SELECT @@version"
Por defecto la extracción usa --technique auto, que prueba los métodos disponibles en este orden:
WP_Post mediante UNION y lee de vuelta su título desde la respuesta REST como ||HEX(value)||. El payload usa la misma ruta de origen /wp/v2/posts/999999 con orderby=none y per_page=500 para que la fila falsa sobreviva como un post renderizado. Una solicitud por valor.EXTRACTVALUE/UPDATEXML filtran ~15 bytes por solicitud, cuando el objetivo refleja errores de MySQL (p. ej. WP_DEBUG_DISPLAY activado).X-WP-Total de la colección de posts como señal de verdadero/falso y no necesita ningún valor reflejado.Fuerza uno con --technique union|error|blind. Estas rutas de lectura no escriben filas en la base de datos.
Con --user y --password, shell inicia sesión con las credenciales de administrador proporcionadas y usa el comportamiento de carga de plugins de WordPress.
Sin credenciales, shell primero ejecuta el puente de SQLi a admin sin autenticación, inicia sesión como el administrador generado y luego carga el shell del plugin.
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # interactive shell
./wp2shell.py shell http://target --cmd id # pre-auth bridge
./wp2shell.py shell http://target -i # pre-auth interactive
shell carga un webshell de plugin (protegido tras una ruta aleatoria y un token por ejecución) e imprime su ruta. El webshell cargado se elimina automáticamente. Cuando el puente sin autenticación crea un administrador, esa cuenta generada se elimina automáticamente después de que termine la sesión de shell.
Actualiza a WordPress 7.0.2, o a 6.9.5 si el sitio está en la rama 6.9. Hasta entonces, bloquea tanto /wp-json/batch/v1 como el parámetro de consulta rest_route=/batch/v1 en el borde, o exige autenticación para el endpoint de lote mediante el filtro rest_pre_dispatch.
Solo para pruebas de seguridad autorizadas. Úsalo exclusivamente contra sistemas que poseas o para los que tengas permiso explícito por escrito para realizar pruebas. No se proporciona garantía alguna y no se acepta responsabilidad por el mal uso.
WP_Query| Opción | Aplica a | Descripción |
|---|
--proxy URL | all | Enruta el tráfico a través de un proxy HTTP (por ejemplo, Burp). |
--timeout N | all | Tiempo de espera de la solicitud en segundos. |
--sleep N | check | Retardo usado por el respaldo de temporización para --confirm-sqli. |
--samples N | check | Pares de temporización usados por el respaldo de temporización para --confirm-sqli. |
--confirm-sqli | check | Envía también un payload activo de confirmación de SQLi. |
--preset | read | fingerprint o users. |
--technique | read | auto (por defecto), union (en banda, forja un post falso), error (en banda, necesita errores visibles de BD), o blind. |
--query | read | Una expresión SQL escalar para leer. |
--prefix | read | Prefijo de tabla de base de datos (por defecto wp_). |
--max-length N | read | Máximo de caracteres leídos por valor (por defecto 128). |
--user / --password | shell | Credenciales de administrador opcionales; omite ambas para usar el puente sin autenticación. |
--cmd | shell | Comando a ejecutar (omítelo al usar -i). |
-i / --interactive | shell | Abre un shell interactivo después del despliegue. |