
Un investigador de seguridad ofensiva + una IA contra un nuevo n-day: construyendo el primer PoC público para CVE-2026-53435 en una noche de viernes. Registro bruto de 8h20m dentro.
Primera prueba de concepto pública para CVE-2026-53435, construida cuando solo existía el aviso y no se había publicado ningún PoC en ninguna parte. Reconstruida a partir de un aviso vago de una línea durante ~8.5 horas un viernes por la noche — no escribiendo el exploit a mano, sino dirigiendo una IA a través de los callejones sin salida, los giros y la verificación hasta que surgió una cadena funcional.

El PoC alcanzando
/etc/passwden el controlador — lectura arbitraria de archivos confirmada.
| CVE | CVE-2026-53435 (Jenkins SECURITY-3707) |
| Clase | Deserialización insegura (bypass de ClassFilter vía config.xml) |
| Impacto (este PoC) | Lectura arbitraria de archivos autenticada en el controlador |
| Afectado | Jenkins weekly ≤ 2.567, LTS ≤ 2.555.2 |
| Corregido | Jenkins weekly 2.568, LTS 2.555.3 |
| Aviso | https://www.jenkins.io/security/advisory/2026-06-10/ (publicado el 2026-06-10) |
| Estado al momento de escribir | No existía ningún PoC público (repositorios de PoC en GitHub: 0) |
Este es un experimento para mostrar el proceso, no solo el artefacto.
Durante años, la salida de un investigador ofensivo era un único exploit terminado — las 8 horas de caminos equivocados que lo produjeron desaparecían. Este repositorio conserva los caminos equivocados. La transcripción completa y ligeramente editada de un humano + IA trabajando un n-day fresco desde el aviso hasta un PoC funcional se incluye como apéndice.
El punto que demuestra es deliberadamente no "la IA lo hizo". Las 8 horas son la
evidencia de que no fue así. La IA nunca generó la cadena de un solo golpe. Lo que llevó
el trabajo fue la experiencia en el dominio — saber que el aviso ocultaba un sumidero de deserialización,
saber buscar en tipos centrales de Jenkins en lugar de plugins, saber que un
DescribableList anterior al parche no imponía su tipo de elemento, y saber cómo
verificar una afirmación en lugar de confiar en ella. El valor del investigador no
desapareció; se trasladó de escribir el exploit a dirigir, podar y validar.
Ese cambio es lo que vale la pena documentar.
Jenkins protege la deserialización con un ClassFilter personalizado que solo permite
tipos definidos en el núcleo de Jenkins o en plugins. CVE-2026-53435 consiste en que esto no es
suficiente: un atacante que pueda enviar un config.xml puede lograr que Jenkins deserialice un
tipo arbitrario del núcleo/plugin en un contexto que nunca lo esperaba, y luego
acceder a ese objeto a través de HTTP mediante enrutamiento Stapler.
Este PoC planta un hudson.Plugin$DummyImpl (un tipo del núcleo de Jenkins, por lo que pasa
la lista blanca de ubicación) en la lista <properties> de un ListView —
un DescribableList<ViewProperty> que, antes del parche, no impone su tipo de
elemento. El objeto plantado lleva baseResourceURL=file:/. Enrutar una solicitud HTTP
hacia él sirve entonces archivos directamente del sistema de archivos del controlador.
Una cadena genérica de ysoserial no funciona aquí — el gadget debe ser un tipo residente en Jenkins/plugin para sobrevivir al filtro. Esa restricción es todo el juego.
Úsalo solo contra sistemas que poseas o para los que tengas autorización explícita para probar.
El exploit se ejecutó contra dos contenedores con configuración idéntica —
2.555.2 vulnerable y 2.555.3 parcheado — para confirmar que ejercita este
CVE y no alguna primitiva de lectura de archivos no relacionada:
La compilación parcheada acepta la solicitud pero la corrección rechaza el tipo plantado en el momento de la deserialización, por lo que la lista de propiedades vuelve vacía y no hay nada a lo que enrutar. El fallo recae exactamente en el mecanismo que describe el aviso — lo que convierte a esto en un PoC genuino para CVE-2026-53435.
python3 exploit_cve_2026_53435_v2.py <base_url> <user> <pass> <remote_file> [view_name]
# ejemplo (contra tu propio laboratorio):
python3 exploit_cve_2026_53435_v2.py http://127.0.0.1:8080 <user> <pass> /etc/passwd
base_url — http://host:port del objetivo (HTTP simple; no antepongas https
a menos que la instancia realmente termine TLS).config.xml de vista
(View/Configure), además de Overall/Read.docker compose -f lab/docker-compose.yml up -d
# vulnerable: http://127.0.0.1:8080 (Jenkins LTS 2.555.2)
# parcheado: http://127.0.0.1:8081 (Jenkins LTS 2.555.3, control negativo)
El laboratorio aprovisiona una base de datos de usuarios solo local mediante inicialización de Groovy para que se pueda ejercitar la ruta autenticada con privilegios bajos sin un IdP externo.
2026-06-12 (vie) 15:00 → 23:23 ≈ 8h 20m
15:00–15:12 Laboratorio construido (docker: vuln 2.555.2 / parcheado 2.555.3) + primer reconocimiento
15:12–21:00 Análisis del sumidero de deserialización y gadget
(los intentos de bypass de Commons-Collections fallaron → giro
de propiedades a acciones y de vuelta)
21:00–22:30 Análisis de ClassFilter + nueva búsqueda de gadgets, restringido a tipos del núcleo
22:30–23:00 PoC funcional (v1 → v2) + verificación diferencial (vuln vs parcheado)
La mayoría de esas horas fueron análisis, no escritura. El exploit en sí se ensambló en los últimos ~30 minutos — una vez que se conocieron el sumidero y el gadget correctos.
Construido con Claude Code (Anthropic), impulsado de forma interactiva por el autor:
La transcripción completa y ligeramente editada del trabajo está en
transcript/ — mantenida en bruto, incluyendo los callejones sin salida. (Editada solo
para suavizar algunas líneas de tono y ocultar detalles de la red local; no se eliminó nada
técnico.)
Solo para pruebas de seguridad autorizadas, investigación y educación. El autor y los colaboradores no aceptan responsabilidad alguna por mal uso. La vulnerabilidad objetivo está parcheada — actualiza a Jenkins LTS 2.555.3 / weekly 2.568 o posterior.
| 2.555.2 (vulnerable) | 2.555.3 (parcheado) |
|---|
createView | HTTP 200 | HTTP 200 |
<properties> almacenado | <hudson.Plugin_-DummyImpl/> — sobrevive | <properties/> — eliminado |
Disparador → /etc/passwd | root:x:0:0:... — leído | vacío — bloqueado |