
Self-contained Docker reproduction and analysis of CVE-2024-23897, the Jenkins CLI arbitrary file read via the args4j @-syntax argument expansion.
A self-contained, fully local reproduction of CVE-2024-23897, the critical (CVSS 3.1 base score 9.8) arbitrary file read in the Jenkins CLI. The project spins up two Docker stacks that differ only by the Jenkins minor version, runs the same proof of concept against both, and shows the vulnerability firing on the unpatched controller and going silent on the patched one.
Everything is driven by a single Python program, poc.py. The harness only
orchestrates the environment (Docker Compose, the official jenkins-cli.jar
client, and verbatim capture of the output). The vulnerability itself lives in
Jenkins' own Java code and is never reimplemented here.
A full written analysis, including the patch review and the CVSS decomposition,
is in report/report.pdf.
The Jenkins CLI builds its argument parser with the args4j library. args4j has a
feature called expandAtFiles, controlled by the atSyntax flag and enabled by
default, that rewrites any argument of the form @/path/to/file into the
contents of that file before the command runs. The file is opened with the
privileges of the Jenkins controller process. Because all three CLI transports
(HTTP, WebSocket, SSH) funnel into the same parser, any client that can send any
CLI command can read arbitrary files off the controller. The fix (commit
554f0378) adds a constant ALLOW_AT_SYNTAX that defaults to false, turning
the expansion off.
This is not classical path traversal: there is no ../ and no base directory to
escape. The path is opened directly. On the outcome axis it is an arbitrary file
read; on the mechanism axis it is argument expansion.
cve-2024-23897-jenkins-poc/
README.md # this file
LICENSE
poc.py # Python reproduction harness (all subcommands)
Dockerfile.vuln # jenkins/jenkins:2.426.2-lts + matrix-auth
Dockerfile.fix # jenkins/jenkins:2.426.3-lts + matrix-auth
docker-compose.vuln.yml # jenkins-vuln + attacker-vuln
docker-compose.fix.yml # jenkins-fix + attacker-fix
init.groovy.d/
01-create-users.groovy # bootstraps admin + readuser via matrix-auth
evidence/
docker-versions.txt # host Docker + Compose versions
output-vulnerable.txt # captured during `poc.py exploit`
output-fixed.txt # captured during `poc.py verify-fix`
report/
report.pdf # full written analysis
report.tex # LaTeX source (self-contained, no external figures)
No JDK is required on the host. Java runs inside the attacker container. The
compose files pin platform: linux/amd64 so the images behave identically on
Apple Silicon; this is an operational choice and does not touch the
vulnerability, which is platform independent.
From inside the repository:
python3 poc.py up-vuln # build and start the vulnerable stack, fetch the CLI jar
python3 poc.py place-proof # write the harmless marker file inside the controller
python3 poc.py exploit # run the PoC, writes evidence/output-vulnerable.txt
python3 poc.py up-fix # tear down vuln, build and start the patched stack
python3 poc.py verify-fix # run the same PoC, writes evidence/output-fixed.txt
python3 poc.py teardown # stop and remove both stacks
First run takes a few minutes (image pulls plus the plugin install). Subsequent runs are much faster.
poc.py exploit passes when the marker string POC-PROOF-LINE appears in the
captured output (the leak happened). poc.py verify-fix passes on the opposite
condition: the marker must be absent. Both subcommands exit non-zero on failure,
so the two evidence files plus their exit status are themselves the test result.
Both compose files run the same two services on one Docker network: a Jenkins
controller (the victim) and a small eclipse-temurin:17-jre attacker container.
poc.py runs on the host and drives Docker through subprocess, but the actual
java -jar jenkins-cli.jar ... invocation runs inside the attacker container.
The attacker has no access to the Jenkins data volume; it reaches the controller
only over the network, the way a remote attacker would.
init.groovy.d/01-create-users.groovy uses the matrix-auth plugin to create two
accounts with deliberately different permissions:
| Account |
|---|
This reproduces the exact split in the official advisory, Overall/Read versus anonymous, rather than the looser "any logged-in user versus anonymous" that the Jenkins core default would have produced. The full file leak is therefore attributable to the CVE alone, not to administrative reach.
The PoC runs four contexts against each controller. The table below summarises
the A/B comparison; the full captures are in evidence/.
The only variable that changes between the two columns is the Jenkins minor
version, and through it the post-patch default of atSyntax. The opposite
results therefore attribute the behaviour change to the parser fix in commit
554f0378.
Running Jenkins in a container is not a mitigation. The parser reads files with the privileges of the Jenkins JVM, and those files live inside the same container filesystem that holds the credentials store. The container boundary protects the host from the Jenkins process, not the Jenkins process from itself. The Docker setup here is a demonstration sandbox, nothing more.
Upgrade to a patched release: 2.442 (weekly), or 2.426.3 or 2.440.1 (LTS), all
published on 24 January 2024. If an immediate upgrade is not possible, leave the
system property hudson.cli.CLICommand.allowAtSyntax unset (its default), which
keeps ALLOW_AT_SYNTAX false and disables the expansion. Disabling individual
CLI transports is only a partial measure, since all three converge on the same
parser.
All work runs in a local Docker environment against a harmless three-line marker
file (/tmp/poc-proof.txt) that the harness creates itself. No public Jenkins
instance is scanned or contacted, and no real secret such as
secrets/master.key or credentials.xml is ever read.
554f0378 on jenkinsci/jenkins: https://github.com/jenkinsci/jenkins/commit/554f03782057c499c49bbb06575f0d28b5200edbParserProperties.withAtSyntax): https://github.com/kohsuke/args4j| Requirement | Notes |
|---|
| Docker Engine 24 or newer | exact version used is recorded in evidence/docker-versions.txt |
| Docker Compose v2 | shipped as the built-in docker compose plugin |
| Python 3.8 or newer | standard library only, no pip install needed |
| Disk space | about 1.5 GB for two Jenkins images, the temurin image and the matrix-auth plugin |
| Permissions |
|---|
| Role in the PoC |
|---|
admin | Jenkins.ADMINISTER | exists only to satisfy "at least one admin", never used to attack |
readuser | Jenkins.READ only (Overall/Read) | the authenticated attacker |
| anonymous | none | the unauthenticated attacker |
| Observation | Vulnerable 2.426.2 | Patched 2.426.3 |
|---|
@-token handling | expanded into file contents | treated as a literal string |
readuser + connect-node | full file disclosure (3 of 3 lines) | no disclosure |
anonymous + who-am-i / help | partial leak (first line) via parser error before the auth gate | no disclosure |
marker POC-PROOF-LINE in output | present | absent |