Skip to content

Fix/WUA applicability subfeatures UI#19

Open
mawussid wants to merge 2 commits intoGoSecure:masterfrom
mawussid:fix/wua-applicability-subfeatures-ui
Open

Fix/WUA applicability subfeatures UI#19
mawussid wants to merge 2 commits intoGoSecure:masterfrom
mawussid:fix/wua-applicability-subfeatures-ui

Conversation

@mawussid
Copy link
Copy Markdown

Fix pywsus for modern Windows (10/11/Server 2025): XML resource overhaul & Code improvements

Hi everyone ! Over the past few weeks I've been digging into the subject. I spent a lot of time reading the MS-WUSP spec, comparing XML exchanges in Wireshark against a WSUS server, and testing on multiple VM builds while upgrading the tool in real time. Turns out the root cause was in the XML resource files.

I've put together what I think is a pretty complete fix, tested on:

OS Build Release Date Status
Windows 10 22H2 19045 October 2022 Working
Windows 11 24H2 26100 October 2024 Working
Windows 11 25H2 26200 September 2025 Working
Windows Server 2025 26100 November 2024 Working

In short, these are my results from my lab environment (domain-joined clients pointing directly at pywsus via GPO). I'd love to hear if your results vary depending on your setup.

What was actually broken?

First, some additional information related to PR #18:
The WUA (Windows Update Agent) evaluates updates in 3 steps:

Prerequisites -> IsInstallable -> IsInstalled

If any step fails, the update is silently dropped: no error, no log, just "no updates found" (the exact symptom described in issue #17).

The original sync-updates.xml had this in the Install parent's <Xml> fragment:

<Prerequisites>
  <AtLeastOne IsCategory="true">
    <UpdateIdentity UpdateID="0fa1201d-4330-4fa8-8ae9-b877473b6441"/>
  </AtLeastOne>
</Prerequisites>

That GUID references a specific WSUS category that pywsus never serves. Here's the chain reaction:

  1. The <AtLeastOne> condition fails (category not in client's local DB)
  2. Prerequisite Ruleset returns FALSE
  3. Update immediately marked "Not Applicable"
  4. GetExtendedUpdateInfo never called
  5. No download, no install, no error

I removed the <Prerequisites> block entirely. Without prerequisites, the ruleset is satisfied by default.

The full evaluation logic is documented in the CSAPI Update Schema and MS-WUSP §3.2.1 (Client Abstract Data Model).

I saw @5tuk0v's PR that also mentions misconfigurations in the XML files. We converge on the core fix (<Prerequisites> removal and get-config.xml rework), but my work also includes a bunch of additional sub-features that came out of my testing:

  • OS fingerprinting + targeted KB titles with build numbers for operational credibility
  • Session rotation (automatic + manual)
  • check_build.py scraper to keep win_builds.json up to date with new Windows builds
  • Client isolation with an IP/OS build dictionary (known_clients.json)
  • HTTP response headers spoofed to match real IIS/WSUS (values, casing, order)
  • Verbose logging system with full XML bodies for easier debugging

Happy to discuss and merge efforts if that makes more sense.

What this PR changes

XML resources (the actual fixes)

These are the changes to the XML templates that pywsus serves to the WUA. This is where the real breakage was: the Python code was fine, the XML wasn't.

File Change Why
sync-updates.xml Removed <Prerequisites> block Root cause of #17: WUA prerequisite evaluation failed silently
sync-updates.xml LastChangeTime -> dynamic {last_change} Was hardcoded to 2020-02-29
sync-updates.xml DriverSyncNotNeeded=false + FlagBitmask Spec conformity (§2.2.2.2.4). In my tests, Win11 24H2+ and Server 2025 always do a driver sync pass regardless of this flag: responding with an empty NewUpdates via sync-updates-empty.xml matches real WSUS behavior
sync-updates.xml Fixed w4 -> w3 namespace link Typo in the original
get-config.xml Added ConfigurationProperty block MaxExtendedUpdatesPerRequest is a MUST in the spec, ProtocolVersion=3.2 for proper negotiation
get-config.xml IsRegistrationRequired=true Forces RegisterComputer -> enables OS fingerprinting for targeted KB titles
get-config.xml AuthInfo with empty PlugInID Anonymous auth: WUA generates its cookie locally, GetAuthorizationCookie is never called
get-extended-update-info.xml Dynamic {kb_number} / {kb_title}, per-client rendering Was hardcoded KB1234567 / Probably-legal-update. Now rendered on the fly per IP with realistic metadata (support URLs, bulletin ID, Microsoft format)
sync-updates-empty.xml New file Empty NewUpdates response for driver sync requests

Python: HTTP handler improving

Wireshark comparison between pywsus and a real WSUS showed several fingerprinting differences. Some of these were already mentioned as comments in the source code, so I went ahead and implemented them to make pywsus responses match what a real IIS/WSUS server returns:

Change Why
Server: Microsoft-IIS/10.0 header Original sent BaseHTTP/0.6 Python/3.x.x. Achieved via version_string() override
HTTP header order matching real IIS Cache-Control, Content-Type, Server, X-AspNet-Version, X-Powered-By, Date, Content-Length: using send_response_only() to control emission order
Content-Length on all SOAP responses HTTP/1.1 conformity: preventive, the original code works without it
Content-Type casing + charset typo Content-type -> Content-Type, chartset -> charset
Driver sync detection (<SystemSpec>) Empty response for driver sync requests to match real WSUS behavior
Realistic cookie (os.urandom(47)) Original was b'A'*47 -> QUFBQUFB..., felt worth replacing with something more realistic. New cookie per session/rotation

Python: Code improvements

On top of the fixes, I added some tooling to make the tool more practical to use during engagements:

  • Rich terminal UI with colored output, interactive keys (r manual rotation, q quit), and 3-level verbosity (-v metadata, -vv full XML in log file with CLIENT -> / <- SERVER directions)
  • OS fingerprinting from RegisterComputer: generates targeted KB titles per client (e.g. 2026-04 Cumulative Update for Windows 11, version 25H2 for x64-based Systems (KB5064579) (26200)) via two-pass lookup in data/win_builds.json
  • Per-client isolation + persistence: each client gets its own title via known_clients.json. Includes check_build.py to scrape Microsoft Release Health pages for new builds
  • Session rotation: automatic (--rotate HOURS) + manual (r key), hot swap without server restart, client data carried over
  • Minor fixes: ReportEventBatch parsing (brand, model, status in terminal), lxml version bump, threading architecture for concurrent keyboard/timer handling

Note: the core WSUS SOAP logic in do_POST is unchanged from the original. The bugs were all in the XML templates. The Python changes are either HTTP hardening or overall user experience.

Here is a quick demo with the new changes:

pywsus_demo

What I observed during my tests

A few things worth noting from my Wireshark captures and WUA logs:

Driver sync is always performed: on Win11 24H2+ and Server 2025, the WUA systematically does two SyncUpdates passes (software then driver with <SystemSpec>), regardless of DriverSyncNotNeeded. Responding with software updates to a driver sync didn't cause a confirmed bug in my tests, but returning an empty response matches what a real WSUS does, so that's what I went with. I feel like sending software blocks when driver blocks are expected could lead to abnormal behavior.

One-session delay: I repeatedly observed a behavior where the current session's update wouldn't install, but the previous session's would. I investigated Content-Length and driver sync as causes but couldn't confirm either in isolation: the original code works without both. It might be related to VMware snapshot cache state (see operational notes in the changelog). The Content-Length and driver sync detection are added as preventive conformity measures, because I could sometimes reproduce the bug without these features, but not consistently.

Win11 25H2 reports OSDescription: "Windows 10 Pro": the kernel is still NT 10.0, so the fingerprinting code does a two-pass lookup (first by OSDescription match, then fallback across all client sections) to handle this. Build 26100 is also shared between Win11 24H2 and Server 2025: the lookup uses a server/client split to disambiguate.

If you want more details, check out the changelog: it documents all the work with MS-WUSP spec references and operational notes.

Cheers ^^

@5tuk0v
Copy link
Copy Markdown

5tuk0v commented Apr 16, 2026

Seems like you went really deep into this, this looks very very cool, nice work, going to test this as well 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants