Cracking the 2023 SANS Holiday Hack Challenge


Steeped in AI and the security risks of its use, the 2023 SANS Holiday Hack Challenge was an enrichening experience of navigating a series of 21 objectives that tested and broadened multiple cybersecurity skills.

The best challenges for me were hunting down AI hallucinations in a pentest report, escalating privileges on a Linux system, searching for cheats in Game Boy games, using the Azure REST API to search for an Azure Function app’s source code and ultimately to exploit a vulnerable SSH certificate service, practicing use of the Impacket tool suite and Certipy to exploit vulnerable Active Directory Certificate Services, and exploiting SQL injection and Java deserialization vulnerabilities in space apps.

Below, I share the path I followed to crack some of the most notable challenges.



ChatNPT, a large language model (LLM) used for the creation of some challenges, generated a penetration test report on vulnerabilities discovered in North Pole Systems’ network, some featuring as a part of upcoming challenges. However, ChatNPT hallucinated some of the details in the report. Using ChatGPT, or another favored LLM, the task was to flag the sections with hallucinated info. My approach was to ask ChatGPT specific questions about the content to explain what I did not understand at first and ultimately to discover the anomalies. Three of the nine sections contained errors.




As confirmed by ChatGPT, this section had an invalid port number of 88,555, far above the highest valid port number of 65,535:





Here I noticed immediately that SEND is not an HTTP request method.





ChatNPT confused the PHP version number mentioned in section eight of the report either as an HTTP protocol version or as mistaken text for what should have been “HTTP HEAD request” in this section. In addition, revealing Windows registration or product keys in the Location header is a bad piece of advice.



Linux PrivEsc


In this challenge, the final objective was to answer a question but that question was hidden in an inaccessible executable:


While there are various methods to escalate privileges on a Linux machine, this challenge allowed a custom executable called simplecopy with the SUID bit set to be abused. If the SUID bit for the owner of a file is set and the owner is root, then that file is always executed with root privileges even by non-root users on the system. I used the following command to search the entire system for regular files that have the SUID bit set for the owner, while discarding any error output:

Figure simple copy

simplecopy seemed to be a vulnerable, but simplified, version of the standard cp utility. Indeed the help message suggested the same:

Usage: simplecopy <source> <destination>

My approach was the following: create the information for a user with root privileges, append this information to a copy of the /etc/passwd file, then replace the old passwd file with the copy. Next, use su to login as the new user.

With root access to the system, I was able to find the executable runmetoanswer in /root, run it, and guess the answer: santa.

Figure run me to answer

The answer was also given in the config file /etc/runtoanswer.yaml, but this file could only be read with root privileges or by using simplecopy to copy it to /dev/stdout.

Game cartridges: vol 2 and vol 3


Two challenges involved light reverse engineering of Game Boy ROM files. The first was a game where the objective was to get past a guard, reveal a portal, and decode the airwave answer. We were given two versions of the game along with a hint to look at the diff between them. Copying a few of the differing hex bytes from one version to the other was enough to reveal the portal, which led to a room with a radio broadcasting the answer in Morse code:

Morse code














The second was a game where you could earn points by jumping to collect coins; however, earning over 998 points would wind your points around to 0 and, under certain conditions, trigger a message about an overflow error. The objective was to reveal floating steps to the next part of the map where the flag was stored, but this required adeptness at jumping. Instead, I figured out how to fly with the help of the BGB Game Boy emulator and a combination of its cheat searcher function and visual inspection of RAM during gameplay to find the hex byte that controls the y-position of the player on the map – basically, I sussed out a GameShark code.

The flag was !tom+elf!.

Certificate SSHenanigans

Although using certificates in place of public-private key pairs improves the security of authenticating over SSH, a misconfigured SSH certificate signing service may allow an attacker to illegitimately obtain a certificate to authenticate as another user. The challenge was set up in the following way.

An Azure Function app deployed on northpole-ssh-certs-fa.azurewebsites.net returns SSH certificates to anyone who provides an SSH public key. These certificates can be used to authenticate over SSH to ssh-server-vm.santaworkshopgeeseislands.org as the user monitor.

The host at this domain is an Azure virtual machine, so after logging in my first step was to collect information from the instance metadata as that would be needed for calls to the Azure REST API later, specifically, I needed the subscription ID and resource group name. I also needed an access token to use this API, which I was able to acquire by using a managed identity. This acquired token must then be used in an HTTP Authorization header when making calls to the Azure REST API.

At this point, I had everything needed to make the API call to get the source control configuration of the Azure Function app. I made the call and among the configuration properties I spotted a URL to the app’s source code on GitHub.

Inspection of the source code revealed that the app accepts a second parameter: principal. If the HTTP POST request to the /api/create-cert endpoint does not send a value for principal, then a default of elf is returned, but here lies a vulnerability. Using Burp Suite I can intercept the HTTP POST request and insert the value admin. I knew to request admin because it was the principal in the /etc/ssh/auth_principals/alabaster file on the virtual machine and I wanted to obtain access to Alabaster’s home directory.


With an SSH certificate for the admin principal in hand, I logged into the same virtual machine as alabaster and found Alabaster’s TODO list in his home directory. The list contained the flag word: gingerbread.

Active Directory

Starting on the same virtual machine as the previous challenge, this challenge looked at how a misconfigured Active Directory Certificate Service can be abused by an attacker to authenticate as another user. As alabaster I had a directory full of Impacket tools but most of them require a target server’s domain name and IP address, and a username and password for logging in – information I did not yet have.

So a good first step was to figure out my permissions for the Azure REST API as there is no need to call one API after another only to meet an authorization denied message. Thus, I listed all the permissions for the resource group that I discovered in the previous challenge.

Since I saw I had several permissions around reading key vaults, I moved on to listing them and found two: northpole-it-kv and northpole-ssh-certs-kv.

Time to switch APIs. Until now I had been making calls to endpoints on management.azure.com but some parts of the Azure Key Vault are on vault.azure.net and this resource requires its own access token. Once again I used my managed identity to acquire an access token but this time switching the resource to vault.azure.net.

In northpole-it-kv, I found the name for a secret. Using that name, I requested the value for this secret, which turned out to be a PowerShell script for creating an Active Directory user called elfy. Critically, I now had all the information needed to leverage the Impacket tool suite.

Using GetADUsers.py revealed another user in the domain that could be of interest: wombleycube. I was also able to connect via SMB to the Active Directory server using smbclient.py. The file share of interest contained a super_secret_research directory but I could not read it as elfy.

Luckily, I had access to another tool: Certipy. This is used to find misconfigured certificate templates for Active Directory Certificate Services and abuse them. The tool listed one vulnerable template due to it allowing a certificate requestor to supply an arbitrary subject alternative name and the issued certificate granting client authentication for the supplied name.


After requesting a certificate with wombleycube inserted into the subject alternative name field, I also used Certipy to get the NT hash for wombleycube using that certificate. Then, by passing Wombley’s hash to smbclient.py, I was able to connect via SMB to the Active Directory server as wombleycube and gain access to the super_secret_research directory, which contained the instructions for the next challenge in InstructionsForEnteringSatelliteGroundStation.txt.


Space Island door access speaker


To gain access to the challenges at the space system ground segment, it was required to use an LLM to generate a fake voice of Wombley Cube speaking the passphrase. Given an audio file of Wombley telling a story and the passphrase, it was trivial to use LOVO AI to generate a voice simulating Wombley’s to speak the passphrase and authenticate successfully.

Without additional safeguards, voice authentication faces serious challenges as a security mechanism in the age of LLMs.

Camera access


After speaking the passphrase, I boarded a train that whizzed me away to the ground segment responsible for communication with an in-game CubeSat, a type of small satellite. In the ground station we were given a Wireguard configuration to set up an encrypted connection to this CubeSat.

The software on this satellite is compatible with the NanoSat MO Framework (NMF), a software framework developed by the European Space Agency for CubeSats. This framework comes with an SDK for developing and testing space apps. It also provides the Consumer Test Tool (CTT), both as a ground app and as a command line tool, to connect to the onboard supervisor, a software orchestrator that takes care of starting and stopping space apps as well as coordinating other tasks.

The challenge was to figure out how to instruct the onboard camera app to take a picture, then retrieve the snapshot. I took the following steps.

After booting up the CTT interface, I entered the supervisor’s URI to connect to the supervisor. Then I checked the available apps, found the camera app, and started it. The camera app returned its URI, which I used to connect to it. Next, I executed the Base64SnapImage action, which instructed the onboard camera to take a picture.

The camera app also offers a parameter service that can return two values: the number of snaps taken and the JPG snapshot encoded in base64. However, the CTT interface did not seem to provide a way to view the image nor to copy parameter values directly from the interface, although I could see the desired value was present. So I needed a roundabout method of acquiring the image.

I discovered that the CTT interface has an enableGeneration button that triggers regularly scheduled publishing of a parameter value. From the CTT command line, I could then subscribe to the desired parameter, receive the value when it was published, and redirect it into a file.

Since I was running CTT in a Docker container, I copied the file onto my host system with docker cp, removed the cruft from the file content, then base64 decoded the image to view the flag: CONQUER HOLIDAY SEASON!.


Missile diversion

The final challenge was to use the missile-targeting-system app on the in-game CubeSat to redirect a missile from the earth to the sun. This app provided only one action: Debug. Running it didn’t seem to do much except print out the SQL VERSION command and its output as if it had been run by a database used by the app:

VERSION(): 11.2.2-MariaDB-1:11.2.2+maria~ubu2204

I immediately wondered if there was a SQL injection vulnerability at play. The CTT interface provided a field to enter an argument for the Debug action, so I tried injecting another command:


Grants for targeter@%: GRANT USAGE ON *.* TO `targeter`@`%` IDENTIFIED BY PASSWORD ‘*41E2CFE844C8F1F375D5704992440920F11A11BA’ |

Grants for targeter@%: GRANT SELECT, INSERT ON `missile_targeting_system`.`satellite_query` TO `targeter`@`%` |

Grants for targeter@%: GRANT SELECT ON `missile_targeting_system`.`pointing_mode` TO `targeter`@`%` |

Grants for targeter@%: GRANT SELECT ON `missile_targeting_system`.`messaging` TO `targeter`@`%` |

Grants for targeter@%: GRANT SELECT ON `missile_targeting_system`.`target_coordinates` TO `targeter`@`%` |

Grants for targeter@%: GRANT SELECT ON `missile_targeting_system`.`pointing_mode_to_str` TO `targeter`@`%` |

Well then, time to plunder the database! The pointing_mode and pointing_mode_to_str tables indicated where the missile was currently pointing:

; SELECT * FROM pointing_mode;

id: 1 | numerical_mode: 0 |

; SELECT * FROM pointing_mode_to_str;

id: 1 | numerical_mode: 0 | str_mode: Earth Point Mode | str_desc: When pointing_mode is 0, targeting system applies the target_coordinates to earth. |

id: 2 | numerical_mode: 1 | str_mode: Sun Point Mode | str_desc: When pointing_mode is 1, targeting system points at the sun, ignoring the coordinates. |

From this information I could see that I needed to change the numerical_mode value in the pointing_mode table to 1, but I did not have permission to update that table.

I did have permission to insert new rows into the satellite_query table, which currently had one row with an as yet unknown value in the object column and the source code of a Java class called SatelliteQueryFileFolderUtility in the results column.

Up to this point the output from the Debug action was easily viewable in a pane at the bottom of the Apps Launcher service tab provided by the supervisor in the CTT interface. However, the object value did not seem to be rendering correctly in the pane. Ideally, it would be good to see the hex dump of the object, which could be obtained with the help of Wireshark or by using the SQL HEX function. This revealed that I was dealing with a serialized Java object.

After reading up on the Java object serialization protocol, I managed to decode the hex bytes:

Hex byte


Remark (ASCII values of hex bytes in monospaced font)



A magic number.



The stream protocol version is 2.



The start of an object.



The start of a class definition.



The class name has a length of 31 bytes.



The class name is SatelliteQueryFileFolderUtility.



A unique identifier associated with this serialized class.



The class is serializable.



The class has three fields.


Data type – ASCII value Z

The first field is a boolean.



The name of this field has a length of 7 bytes.



The name of this field is isQuery.


Data type – ASCII value Z

The second field is a boolean.



The name of this field has a length of 8 bytes.



The name of this field is isUpdate.


Data type – ASCII value L

The third field is an object.



The name of this field has a length of 15 bytes.



The name of this field is pathOrStatement .



The class type of this object is given in a string.



This string has a length of 18 bytes.



The class type of this object is java/lang/String.



The end of a class definition.



No superclass defined.



The boolean field isQuery has the value false.



The boolean field isUpdate has the value false.



The value of the pathOrStatement field is a string.



The value of the pathOrStatement field has a length of 41 bytes.



The value of the pathOrStatement field is /opt/SatelliteQueryFileFolderUtility.java

Acquiring this object via Wireshark returned an incorrect magic number and serialVersionUID, but not when using the HEX function.

To understand what an INSERT into the satellite_query table would do, I inserted this object into a new row of the table and received back the same Java source code in the results column. In fact, this behavior corresponded to what I saw in that code as the getResults function of a SatelliteQueryFileFolderUtility object.

This function takes a different execution path depending on the values of the object’s three fields: isQuery, isUpdate, and pathOrStatement. If isQuery and isUpdate are false, then the function checks whether the pathOrStatement is a path and a directory. If so, it returns the list of files contained in the directory; otherwise, it assumes a file was provided and attempts to return the contents of that file.

On the other hand, if isQuery and isUpdate are true, then the function executes the content of pathOrStatement as a SQL UPDATE statement. What I needed to execute was the following:

UPDATE pointing_mode SET numerical_mode = 1;

I changed the necessary bytes (highlighted below) in the serialized object, and injected the winning command:

; INSERT INTO satellite_query






These are only some of the areas covered in the 2023 SANS Holiday Hack Challenge; there were many others that looked at the security of JSON web tokens, cracking passwords with hashcat, virtual cracking of luggage locks and rotary combination locks, Python NaN injection, using the Kusto Query Language for threat hunting, checking DKIM and SPF records to help identify malicious emails, and hackable minigames.

All in all, I am sure that such a wide-ranging set of fun challenges cannot fail to be instructive to anyone who attempts to take them on. And while I am already looking forward to next year’s challenge, a well-earned thank you goes out to the organizers of the SANS Holiday Hack Challenge for putting together this year’s challenge.

You can read my highlights from the 2022 challenge at Cracked it! Highlights from KringleCon 5: Golden Rings.

#Cracking #SANS #Holiday #Hack #Challenge