# RCA Report: CVE-2024-23897 — Jenkins CLI @-file Expansion Arbitrary File Read

## 1. Summary

**CVE:** CVE-2024-23897
**Product:** Jenkins core (controller)
**Vulnerable Versions:** weekly ≤ 2.441; LTS ≤ 2.426.2
**Patched Versions:** 2.442; 2.426.3; 2.440.1
**Claimed Surface:** api_remote (CLI over HTTP)
**Claimed Impact:** code_execution (via leaked secrets enabling RCE)
**Observed Impact:** info_leak (arbitrary file read on the controller)
**Reproduction Status:** CONFIRMED

Jenkins core's CLI command parser uses the args4j library's `expandAtFiles` feature, which replaces any CLI argument beginning with `@` with the contents of the referenced file, split into individual lines. Because this feature was enabled by default in Jenkins 2.441 and earlier, an attacker could send a CLI command over HTTP with an argument such as `@/etc/passwd`. Jenkins would read the referenced file from the controller's filesystem and inject its contents as command arguments. When those arguments appear in error messages (e.g., `connect-node` reporting "No such agent"), the file contents are disclosed to the attacker.

## 2. Root Cause

The root cause is that Jenkins did not disable args4j's `@`-file expansion (`expandAtFiles`) in its CLI argument parser (`hudson.cli.CLICommand`). The args4j `CmdLineParser` expands any argument starting with `@` into the file's lines, treating each line as a separate argument. This expansion happens **server-side** on the Jenkins controller when CLI commands are dispatched over the HTTP CLI protocol.

The vulnerable code path:
1. The Jenkins CLI jar sends a command and its arguments to the Jenkins controller via HTTP (`/cli` endpoint, `-http` protocol).
2. The controller's `CLICommand.main()` invokes args4j's `CmdLineParser` to parse the arguments.
3. `CmdLineParser` with `expandAtFiles=true` reads the file referenced by `@<path>` from the controller's filesystem.
4. The file's lines become individual arguments to the CLI command.
5. Commands such as `connect-node` echo argument values in error messages (e.g., `No such agent "<line>" exists`), disclosing the file contents.

**Key distinction:** The file read occurs on the **Jenkins controller** (server), not on the client. This makes it a remote arbitrary-file-read vulnerability exploitable over the HTTP API.

## 3. Reproduction

### Environment
- **Docker image:** `jenkins/jenkins:2.441-jdk17` (vulnerable) and `jenkins/jenkins:2.442-jdk17` (fixed)
- **Setup wizard:** disabled via `-Djenkins.install.runSetupWizard=false`
- **Anonymous access:** enabled (default when setup wizard is skipped)
- **CLI protocol:** HTTP (`-http` flag on `jenkins-cli.jar`)

### Steps
1. Start Jenkins 2.441 in a Docker container with `-Djenkins.install.runSetupWizard=false`.
2. Wait for "Jenkins is fully up and running" in the container logs.
3. Download `jenkins-cli.jar` from the controller (`/jnlpJars/jenkins-cli.jar`).
4. Execute: `java -jar jenkins-cli.jar -s http://localhost:8080/ -http connect-node "@/etc/passwd"`
5. Observe that every line of `/etc/passwd` is reflected in the error output.
6. Repeat with Jenkins 2.442 (fixed) and verify the `@` is treated literally.

### Evidence

**Vulnerable (Jenkins 2.441) — connect-node @/etc/passwd:**
Each line of /etc/passwd appears in "No such agent" error messages. 19 of 19 ground-truth lines were confirmed leaked. Example lines:
- `root:x:0:0:root:/root:/bin/bash: No such agent "root:x:0:0:root:/root:/bin/bash" exists.`
- `jenkins:x:1000:1000::/var/jenkins_home:/bin/bash: No such agent "jenkins:x:1000:1000::/var/jenkins_home:/bin/bash" exists.`

**Fixed (Jenkins 2.442) — same command:**
`ERROR: No such agent "@/etc/passwd" exists.` — the `@` is treated literally, no file content disclosed.

**Anonymous access:** `who-am-i` returned `Authenticated as: anonymous` — no credentials needed.

### Proof Artifacts
- `logs/vuln_attempt1.log` / `logs/vuln_attempt2.log` — vulnerable CLI output showing leaked /etc/passwd
- `logs/fixed_attempt1.log` / `logs/fixed_attempt2.log` — fixed CLI output showing @ treated literally
- `logs/vuln_passwd_ground_truth.txt` — direct cat /etc/passwd from vuln container for verification
- `logs/whoami_vuln.out` / `logs/whoami_fixed.out` — anonymous access confirmation
- `logs/docker_vuln.log` / `logs/docker_fixed.log` — Jenkins startup logs

## 4. Impact Analysis

### Direct Impact: Arbitrary File Read (info_leak)
- **Unauthenticated users** can read the first few lines of arbitrary files on the controller using CLI commands accessible to anonymous users.
- **Users with Overall/Read permission** can read entire files.

### Escalation to RCE (claimed impact)
The ticket claims `code_execution`. The arbitrary file read enables RCE through an escalation chain:
1. Read sensitive files (`secrets/master.key`, `secrets/initialAdminPassword`, remember-me secrets).
2. Use leaked secrets to forge authentication tokens/cookies or access the resource root URL.
3. Leverage forged credentials to execute arbitrary code (script console, job creation, plugin install).

**This reproduction confirms the core arbitrary file read (the precondition for RCE) but does not demonstrate the full RCE chain.** Observed impact is `info_leak`; claimed impact is `code_execution`.

## 5. Fix

Jenkins 2.442 disabled `@`-file expansion by default. The system property `-Dhudson.cli.CLICommand.allowAtSyntax=true` can re-enable it (strongly discouraged). The fix ensures `@<path>` arguments are treated as literal strings.

## 6. Verdict

- **Claim outcome:** partial (core file-read vulnerability confirmed; full RCE chain not demonstrated)
- **Claim block reason:** impact_mismatch (observed info_leak vs claimed code_execution)
- **Repro result:** confirmed (file read on vulnerable, blocked on fixed)
- **Validated surface:** api_remote (Jenkins HTTP CLI endpoint)
- **Evidence scope:** production_path (real Jenkins controller in Docker)
- **Observed impact class:** info_leak
- **Exploitability confidence:** high
- **Exploit chain demonstrated:** false
