| Bug | Type | Vector | Impact |
|---|---|---|---|
| Bug 1 | Use-After-Free | WS-Discovery SOAP | RCE (controlled vtable dispatch) |
| Bug 2 | Stack Overflow | HTTP Pipelining | Remote DoS / Potential RCE |
| Bug 3 | Race Condition | Session Management | Remote DoS / Info Leak |
I spent a few weeks digging into the WS-Discovery stack on Windows and found
three distinct memory corruption bugs in wsdapi.dll, all triggerable
from the network before any authentication. The target service is
FDResPub (Function Discovery Resource Publication), which runs
inside svchost.exe and listens on TCP port 5357 by default on Windows 11.
No credentials. No user interaction. Just a socket.
I'm writing this up because the research process itself is worth documenting — the non-invasive CDB technique, the heap spray logic, how I proved controlled vtable dispatch without writing a single byte of shellcode. The bugs themselves were confirmed on Windows 11 24H2, Build 26100.8115.
Setup
My lab target was a Windows 11 24H2 VM on Proxmox. The attack machine is Kali on the same /24. FDResPub starts automatically; you can verify it's running with:
sc.exe queryex FDResPub
Port 5357 is the WSD HTTP endpoint. You can confirm it's alive with any TCP connection — it speaks HTTP/1.1 and expects SOAP envelopes. The protocol is WS-Discovery (WSD), defined in the OASIS spec, used by Windows for network device announcement and discovery.
For crash capture I configured the target with kernel dump enabled and
AutoReboot = 0 so the system doesn't restart before the dump
writes. For user-mode crashes, Windows Error Reporting drops a minidump in
%LOCALAPPDATA%\CrashDumps automatically.
The key tool is CDB (the command-line WinDbg) in non-invasive mode. The
-pv flag attaches without suspending the target process, so
FDResPub keeps running and accepting connections while you're attached.
That matters because some bugs only trigger under concurrent load.
cdb.exe -pv -p <PID>
I automated the PID resolution and module base address lookup over SSH using a Python script (specter_brain / specter_agent) so I could run CDB commands from Kali without touching the Windows desktop.
Bug 1 — Use-After-Free with controlled vtable dispatch
This one took the longest to understand. The crash shows up as a CFG violation
(STATUS_STACK_BUFFER_OVERRUN 0xC0000409) rather than a typical
access violation, which is the CFG mitigation kicking in after wsdapi dispatches
a vtable call through a freed COM object.
The faulting offset is wsdapi+0x50e5c. What's happening there:
the code calls a method on a COM object (CWSDXMLReader) but
the object has already been freed. The AddRef that should pin
the object before the call is missing — classic refcount bug. After free,
the vtable pointer at offset 0 of the object points into whatever replaced
the allocation. When CFG validates the dispatch target against its bitmap,
it fails and terminates the process.
The crash dump confirms this. The RSI register holds the freed object's address:
rsi = 0x0000021eb4853070 ; freed CWSDXMLReader [rsi+0] = vtable ptr ; now pointing at LFH fill (0xfeeefeee region) call [vtable+0x30] ; vtable[6] dispatch → CFG violation
Without any spray, the freed memory gets filled with the default LFH pattern
(0xfeeefeee), and ntdll+0x4cd90 ends up in the vtable
slot. CFG rejects that target and crashes.
The spray is straightforward. CWSDXMLReader is 768 bytes (0x300),
solidly within LFH territory (the modern Segment Heap LFH handles up to 16 KB).
I hold a set of partial SOAP probe requests open with an oversized
Content-Length header — this pins live CWSDXMLReader
allocations in the heap — then cycle them to reclaim freed slots with controlled
content.
I don't need shellcode to prove this is exploitable. I set a breakpoint at
wsdapi+0xf7a5 — a valid function entry point that lives in the CFG
bitmap, so dispatching to it won't trigger the CFG check — using CDB:
bu 0x00007ffd895ff7a5 ".echo ==UAF_VTABLE6_CONTROLLED==; r rcx rdx r8 rsi; gc"
The hardcoded address is from my lab — 0x00007ffd895ff7a5 is
wsdapi base + 0xf7a5 on my build. Resolve the base for your version with
lm m wsdapi in CDB and add 0xf7a5 to get the correct address.
With the spray active, that breakpoint hits 30 out of 30 attempts. Without the spray, the process crashes every time. That's proof of controlled vtable dispatch — an attacker who fills those 768-byte slots with a crafted fake vtable gets a PC-control primitive.
Bug 2 — Stack buffer overflow (GS cookie violation)
This one is noisier and easier to trigger. The crash code is
0xC000071C (GS stack cookie check failure) at
wsdapi+0xa6b78. Four crashes in the Event Log across a single
fuzzing session.
The trigger is HTTP pipelining at volume. FDResPub handles multiple HTTP
requests per connection, and when you pipeline enough requests with
specific body sizes, the stack frame at +0xa6b78 gets smashed.
I used a 1MB SOAP SUBSCRIBE body with Transfer-Encoding chunked to hold
connections open, combined with a burst of concurrent pipelined probes on
separate connections.
The GS cookie check means the overflow reaches the cookie before the return address, which triggers the fast-fail. The process terminates before returning. The DoS is guaranteed and reliable — you don't need to win any timing race or get heap layout right, just pipeline enough requests and the service dies. Turning it into RCE is non-trivial because the cookie fires before you control the return address, but the primitive is there if you can bypass or brute-force the cookie. I stopped at DoS — that's sufficient proof for reporting.
Bug 3 — Lock-free session list UAF (AV read)
The third bug is a race condition in the session list management code around
wsdapi+0x23e45. The fault address is
0xfffffffffffffff1 — that's -15 sign-extended to 64
bits, which is what you get when a session lookup fails and returns an NTSTATUS
or HRESULT error code, and the caller uses that return value directly as a
pointer without checking it first.
The trigger is concurrent session creation and destruction. I open N connections, synchronize them to close simultaneously (using a Python threading barrier), and interleave that with new incoming connections. The race window is narrow but reproducible — four crashes across the session, all showing the same fault address pattern.
; wsdapi+0x23e45 (AV read) ; rax = 0xfffffffffffffff1 ; -15 sign-extended — failed lookup returned as pointer mov rax, [rax+8] ; → access violation read
The 0xfffffffffffffff1 address is consistent across crashes, which
tells me it's not random noise — the same failed lookup produces the same
sign-extended value every time, just at the wrong moment in the race. That kind
of consistency is useful; it means you have a real window to work with, not a
one-in-a-million timing fluke.
Proof approach
For each bug I wanted proof that didn't require shellcode. CDB non-invasive mode is the key tool here. You attach without suspending the process, set a conditional breakpoint at a location you've reasoned the CPU will reach if the bug is triggered, and log register state. If the breakpoint fires with the right register values, that's forensic proof of the code path.
The hardest part of this kind of research when you're working alone is staying disciplined about what you've actually proven vs. what you're assuming. The UAF vtable dispatch is proven — 30/30 breakpoints firing is not coincidence. The GS overflow is proven — four Event Log entries with the same crash offset is not coincidence. The race condition UAF is proven — the deterministic fault address tells you exactly what the code is doing wrong.
What I didn't prove is post-exploitation. I'm not going to, and I don't need to for research purposes. Controlled vtable dispatch at a CFG-valid target with register state pointing at attacker data is the demonstration.
The tooling
Everything ran from Kali over SSH. The Windows-side component
(specter_agent.py) is a lightweight HTTP server wrapping CDB,
exposing endpoints for running commands against a live process:
GET /context?pid=<pid> # lm, !peb, general state POST /command?pid=<pid> # arbitrary CDB command GET /discover?dll=wsdapi # resolve base address
The Kali side (specter_brain.py) coordinates the attack traffic,
CDB commands, and crash detection. Both are sitting in the repo if you want
to look at them.
Target environment
- Windows 11 24H2, Build 26100.8115
- wsdapi.dll version 10.0.26100.1 — same version shipped with 24H2 RTM, unchanged by subsequent LCUs; supports the "old code, low attention" angle
- FDResPub (svchost.exe), default configuration
- Port 5357 is open on Private and Domain network profiles by default; in corporate environments, GPOs for printer and device discovery frequently force workstations onto Domain profile permanently — making every machine on the LAN reachable without any user action
- Attacker: any machine with TCP access to port 5357 — same subnet is enough
Final note
Three distinct pre-auth memory corruption bugs in one service, one attack surface, no credentials required. The WSD stack is old code that hasn't gotten a lot of attention from the research community, and it shows. If you're looking for a place to dig, FDResPub and the broader Function Discovery infrastructure are worth your time.
I'll write up the spray technique in more detail in a follow-up post — the HOLD+RECLAIM approach for LFH buckets specifically, since most spray write-ups gloss over the details of how you keep slots pinned long enough to do anything useful with them.