Phân tích lỗ hổng CVE-2022-22005 Microsoft Sharepoint RCE

Phân tích lỗ hổng CVE-2022-22005 Microsoft Sharepoint RCE

Đọc bản Tiếng Anh tại đây

Microsoft Sharepoint

1-1

SharePoint là nền tảng chia sẻ và quản lý nội dung, kiến thức cùng các ứng dụng để hỗ trợ hoạt động làm việc nhóm giúp nhanh chóng tìm thông tin và hợp tác rành mạch trong toàn tổ chức. Hơn 200.000 tổ chức cùng 190 triệu người sử dụng SharePoint cho mạng nội bộ, site nhóm và hoạt động quản lý nội dung. Số lượng trên đủ để thấy đây luôn là một mục tiêu lớn cho các nhà nghiên cứu bảo mật tìm kiếm lỗ hổng.

Với SharePoint, người dùng có thể tạo mạng nội bộ (hoặc hệ thống internet nội bộ) hoạt động giống như bất kỳ trang web nào khác. Ngoài một site lớn cho tổ chức, sharepoint có thể chia các sub-site nhỏ cho từng nhóm, phòng ban trong nội bộ. Bên cạnh đó, đây là một nền tảng quản lý chia sẻ nội dung tuyệt vời với các list có thể tùy biến đa dạng theo ý muốn. Một số kiểu list được xây dựng sẵn trên Sharepoint như list ảnh, tài liệu, biễu mẫu ... Ngoài những list được xây dựng người dùng có thể cài đặt một list mới và tùy biến các thuộc tính của list đó theo ý muốn. Bộ công cụ đắc lực cho việc tùy biến trên Sharepoint là Sharepoint Designer và InfoPath Designer.

CVE-2022-22005

Bản vá tháng 2 - 2022 của Microsoft khắc phục một lỗ hổng có mã CVE-2022-22005. Lỗ hổng này cho phép kẻ tấn công thực thi mã từ xa và được chấm điểm 8.8 theo bộ tính CVSSv3. Các phiên bản bị ảnh hưởng được nêu dưới đây

  • Microsoft SharePoint Server Subscription Edition
  • Microsoft SharePoint Server 2019
  • Microsoft SharePoint Enterprise Server 2013 Service Pack 1
  • Microsoft SharePoint Enterprise Server 2016

Những phân tích dưới bài viết được thực hiện trên phiên bản Microsoft SharePoint Enterprise Server 2016

Phân tích bản vá

Tiến hành cài đặt bản vá tháng 1 và tháng 2 - 2022 của Sharepoint 2016, gom các tệp dll của Sharepoint và tiến hành decompile thành source. Thêm một vài bước hậu kỳ để lược bỏ các yếu tố không cần thiết (comment, ...). Cuối cùng so sánh hai bản vá để tìm vị trí các đoạn code được các lập trình viên Microsoft dùng để vá lỗi. Một vị trí vá lỗi Deserialization được tìm thấy ở Microsoft.Office.Server.Internal.Charting.UI.WebControls.ChartPreviewImage.loadChartImage()

2

Bản vá sử dụng binder giới hạn những kiểu được phép deserialize, đây là cách mà Microsoft thường sử dụng cho các lỗi tương tự thế này trước đây. Về lỗi deserialize bạn có thể tìm hiểu thêm tại đây.

Trace code

Tìm hiểu một chút về chart - biểu đồ trên Sharepoint, đây là một webpartpage - thành phần của một page trên Sharepoint. Như vậy có thể hiểu để dữ liệu đi được đến vị trí deserialize thì phải có được tài khoản người dùng có quyền tạo page. Kết hợp đặt debug và tạo một page có sử dụng chart, đoạn code được gọi khi chart đang thực hiện load dữ liệu. Quan sát lại hàm gây lỗi loadChartImage, dữ liệu được deserialize nằm ở biến buffer được set giá trị qua hàm FetchBinaryData(sessionKey).

// Microsoft.Office.Server.Internal.Charting.UI.WebControls.ChartPreviewImage.loadChartImage()
private ChartImageSessionBlock loadChartImage()
{
    byte[] buffer = CustomSessionState.FetchBinaryData(this.sessionKey);
    ChartImageSessionBlock result = null;
    using (
        MemoryStream memoryStream = new MemoryStream(buffer)
    )
    {
        IFormatter formatter = new BinaryFormatter();
        result = (ChartImageSessionBlock)formatter.Deserialize(memoryStream);
    }
    return result;
}

Đoạn code liên quan đến session state trong Sharepoint. Đây là một cơ chế để lưu trữ trạng thái của một đối tượng trong sharepoint, trạng thái đó có thể là của file, ảnh, ... hay cụ thể trong trường hợp này là một ChartImageSessionBlock object sau khi đã serialize. Các trạng thái này sẽ được lưu trữ ở database dưới dạng binarydata và được mapping cho một session key. Như vậy để khai thác được lỗi này chúng ta cần control được binary data ở database, sau đó thông qua hàm loadChartImage để deserialize một object tùy ý. Sử dụng công cụ Burp Suite trong quá trình debug chúng ta sẽ có được request dùng để kích hoạt hàm gây lỗi .

GET /_layouts/15/Chart/WebUI/Controls/ChartPreviewImage.aspx?sk=5264ebfb259840faa703bdbc976e069b_74929f85360d499d9f1d4f337bf49300&hash=2551012 HTTP/1.1
Host: sharepoint2016:33257
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Referer: http://sharepoint2016:33257/SitePages/testpage.aspx
Cookie: stsSyncAppName=Client; stsSyncIconPath=; WSS_FullScreenMode=false
Connection: Keep-Alive

Biến sk ở đây chính là sessionKey được đưa vào hàm FetchBinaryData, có dạng guid1_guid2 trong đó guid1 là id của database còn guid2 là id của ChartImageSessionBlock. Để khai thác lỗi thì chúng ta sẽ thay đổi guid2 thành id của một session state khác có chứa binarydata gây lỗi. Vậy là xong công đoạn kích hoạt lỗi, việc cần làm tiếp theo là tìm cách đưa binary data bất kỳ vào bảng session state trong database thế nào.

Một bài viết được public trên trang ZDI về một lỗ hổng trước đây có mã CVE-2021-27076 liên quan đến session state, sử dụng cơ chế attachment trên một infopath form. Khi bắt đầu tạo mới một item trong một infopath list, item sẽ được đăng ký với một session key là itemId. Tiếp theo khi attachment một file vào item mới này file đó sẽ được lưu dưới dạng binary trong database với key là attachmentId.

Binary data bất kỳ sẽ nằm trong file attachment, và attachmentId là thứ mình cần lấy được để kích hoạt lỗi. Vấn đề nằm ở chỗ khi tạo mới item trong infolist, sẽ chỉ có itemId được trả về. Thông qua dựng lab, nhận thấy rằng gía trị của attachmentId nằm trong binarydata của item, như vậy cần tìm cách lấy attachmentId thông qua itemId. Bài viết của ZDI cũng đã chỉ ra cách giải quyết vấn đề này, chính là phát lại itemId đến FormServerAttachments.aspx, service này sẽ lấy binarydata của item và trả về dưới dạng một file.

Đến đây có hai hướng để tìm được request phù hợp đến FormServerAttachments.aspx, một là thử các chức năng, hai là đọc code và tự tạo request. Phương án đầu sẽ tốt hơn vì sẽ đỡ tốn thời gian và mình cũng sẽ có được request chuẩn để làm, trong trường hợp không xác định được chức năng thì bắt buộc phải theo phương án 2 là đọc code. Do binary được trả về dưới dạng file nên hàm FileDownload lọt vào tầm ngắm của mình.

// Microsoft.Office.InfoPath.Server.Controls.FormServerAttachments.FileDownload(HttpContext) 
private static bool FileDownload(HttpContext context)
{
    string text = context.Request.QueryString["fid"];
    string text2 = context.Request.QueryString["sid"];
    string value = context.Request.QueryString["key"];
    string strA = context.Request.QueryString["dl"];
    int num = 0;
    string empty = string.Empty;
    if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(value) || (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0 && string.Compare(strA, "ip", StringComparison.OrdinalIgnoreCase) != 0))
    {
        ULS.SendTraceTag(1831874679U, ULSCat.msoulscat_formservices_runtime, ULSTraceLevel.Medium, "Invalid request incorrect or missing query strings: {0}", new object[]
        {
                    context.Request.Url.ToString()
        });
        return false;
    }
    using (new GlobalStorageContext(text))
    {
        try
        {
            SPSite spsite = SiteAndWebCache.Fetch().EnsureRequestSite();
            Solution solutionById = SolutionCache.GetSolutionById(spsite, new SolutionIdentity(text2));
            if (Canary.VerifyCanaryFromCookie(context, spsite, solutionById))
            {
                context.Response.Clear();
                context.Response.Cache.SetExpires(DateTime.Now.AddDays(2.0));
                using (BinaryWriter binaryWriter = new BinaryWriter(context.Response.OutputStream))
                {
                    Base64DataStorage.Base64DataItem item = null;
                    StreamUtils.DeserializeObjectsFromString(value, delegate (EnhancedBinaryReader binaryReader)
                    {
                        item = new Base64DataStorage.Base64DataItem(binaryReader);
                        DocumentChildState.StateInfo stateInfo = new DocumentChildState.StateInfo();
                        ((IBinaryDeserializable)stateInfo).Deserialize(binaryReader);
                        StateKey stateKey = StateKey.ParseKey(stateInfo.SerializedKey);
                        item.EnsureData(stateKey);
                    });
                    byte[] dataAsBytes = item.GetDataAsBytes();
                    using (Stream stream = new MemoryStream(dataAsBytes, false))
                    {
                        if (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0)
                        {
                            context.Response.AppendHeader("Content-Disposition", "attachment;filename=\"image\"");
                            context.Response.AppendHeader("X-Download-Options", "noopen");
                            context.Response.ContentType = ImageUtils.GetContentType(dataAsBytes);
                            return InlinePicture.ReadInfoFromStream(binaryWriter, stream);
                        }
                        context.Response.ContentType = "application/octet-stream";
                        if (FileAttachment.ReadInfoFromStream(binaryWriter, out num, out empty, stream))
                        {
                            FilePathUtils.AddFileDownloadHttpHeader(context, empty);
                            return true;
                        }
                        return false;
                    }
                }
            }
            ULS.SendTraceTag(1831874680U, ULSCat.msoulscat_formservices_runtime, ULSTraceLevel.Verbose, "Can't verify canary from cookie for FileDownload");
            return false;
        }
        catch (InfoPathException)
        {
            ULS.SendTraceTag(1831874681U, ULSCat.msoulscat_formservices_runtime, ULSTraceLevel.Medium, "InfoPathException occurred downloading fileattachment or inline picture");
        }
    }
    return false;
}

May mắn là các biến cần thiết cho request khá rõ ràng - fid, sid, key, dl. Cùng phân tích sâu một chút vào các thành phần, đoạn code sau là điều kiện trả về lỗi

// fid -> text, sid -> text2, key -> value, dl -> strA
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(text2) || string.IsNullOrEmpty(value) || (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0 && string.Compare(strA, "ip", StringComparison.OrdinalIgnoreCase) != 0))

Các param bắt buộc phải khác rỗng, trong đó dl phải là một trong hai chuỗi 'fa' hoặc 'ip'.

// fid -> text, sid -> text2, key -> value, dl -> strA
SPSite spsite = SiteAndWebCache.Fetch().EnsureRequestSite();
Solution solutionById = SolutionCache.GetSolutionById(spsite, new SolutionIdentity(text2));
if (Canary.VerifyCanaryFromCookie(context, spsite, solutionById))
{
    ...
}

Đoạn code này thực hiện lấy solutionId từ sid và xác thực với infopath canary bên trong cookie, ví dụ một cookie như sau

_InfoPath_CanaryValueAGQX2G3RUCCXQRUNZHR3UB7IIEMSOL2MNFZXI4ZPORSXG5C7NFXGM327NRUXG5BPJF2GK3JPORSW24DMMF2GKLTYONXCWMKZLBZTE4TDI5WXC4ZSIIZGIUTINE4EI6DBGFWVKNKDLFZGSTJYLFNHE33VMJ5EGSLEOM=KBxeU4WXMZ3Yg8v0ZPZfAWcpoiLL/R3sfejthMFTfL1x9GqMoiIOMSS9XrT0gguJmdn0Yj2qw0gqlDJXT7X49A==|637806206864107501

Cookie này có key ở định dạng '_InfoPath_CanaryValue'+ <hậu tố>. Phần hậu tố chính là sid cần tìm. Tiếp theo là đoạn code lấy binary data từ session key.

// fid -> text, sid -> text2, key -> value, dl -> strA
Base64DataStorage.Base64DataItem item = null;
StreamUtils.DeserializeObjectsFromString(value, delegate (EnhancedBinaryReader binaryReader)
{
    item = new Base64DataStorage.Base64DataItem(binaryReader);
    DocumentChildState.StateInfo stateInfo = new DocumentChildState.StateInfo();
    ((IBinaryDeserializable)stateInfo).Deserialize(binaryReader);
    StateKey stateKey = StateKey.ParseKey(stateInfo.SerializedKey);
    item.EnsureData(stateKey);
});
byte[] dataAsBytes = item.GetDataAsBytes();

Session state key sẽ được lấy ra từ biến key, cùng xem chi tiết hàm DeserializeObjectsFromString

// fid -> text, sid -> text2, key -> value, dl -> strA
internal static void DeserializeObjectsFromString(string value, Action<EnhancedBinaryReader> readerMethod)
{
    using (Base64Stream base64Stream = new Base64Stream(value))
    {
        using (EnhancedBinaryReader enhancedBinaryReader = new EnhancedBinaryReader(base64Stream))
        {
            readerMethod(enhancedBinaryReader);
        }
    }
}

Như vậy key cần ở dạng base64, xem tiếp hàm Base64DataStorage.Base64DataItem(binaryReader)

// Microsoft.Office.InfoPath.Server.SolutionLifetime.Base64DataStorage.Base64DataItem.Base64DataItem(EnhancedBinaryReader)
internal Base64DataItem(EnhancedBinaryReader reader)
{
    this._state = (Base64ItemState)reader.ReadCompressedInt();
    this._sessionDataType = (Base64DataStorage.Base64DataItem.DataTypeInSessionState)reader.ReadCompressedInt();
    this._itemId = new Base64SerializationId(reader);
}

Vậy 3 vị trí đầu tiên trong cấu tạo của key sẽ là

  • base64ItemState (int)
  • dataTypeInSessionState (int)
  • base64SerializationId (guid string)

cùng xem tiếp hàm DocumentChildState.StateInfo.Deserialize(binaryReader)

// DocumentChildState.StateInfo.Deserialize(binaryReader)
void IBinaryDeserializable.Deserialize(EnhancedBinaryReader reader)
{
    this._serializedKey = reader.ReadString();
    this._size = reader.ReadCompressedInt();
    this._version = reader.ReadCompressedInt();
}

vậy 3 vị trí kế tiếp trong cấu trúc của key sẽ là

  • serializedKey (string)
  • size (int)
  • version (int)

Tiếp theo cần xem xét các thành phần nào sẽ bắt buộc phải đưa giá trị chính xác, phần lấy session state key như sau

StateKey stateKey = StateKey.ParseKey(stateInfo.SerializedKey);
item.EnsureData(stateKey);

Vậy serializedKey sẽ có dạng guid1_guid2, trong đó guid1 sẽ là database id còn guid2 là itemId chúng ta đưa vào, tiếp theo xem hàm item.EnsureData(stateKey)

// Microsoft.Office.InfoPath.Server.SolutionLifetime.Base64DataStorage.Base64DataItem.EnsureData(StateKey)
internal void EnsureData(StateKey stateKey)
{
    if (this.State == Base64ItemState.DelayLoad)
    {
        byte[] sessionData = StateManager.GetManager(HttpContext.Current).PeekState(stateKey); // get binary data from stateKey
        this.SetSessionData(sessionData);
        return;
    }
    if (this.State == Base64ItemState.Removed)
    {
        throw new InfoPathLocalizedException(InfoPathResourceManager.Ids.ServerGenericError, new string[0]);
    }
}

Điều kiện đầu tiên bắt buộc phải thỏa mã để lấy được binarydata từ database, do đó base64ItemState phải có giá trị của enum Base64ItemState.DelayLoad, xem bên trong Base64ItemState

internal enum Base64ItemState
{
    NoChange,
    Updated,
    Removed,
    New,
    DelayLoad // 4
}

Từ đó base64ItemState sẽ lấy giá trị là 4. Tiếp theo xem các giá trị enum của dataTypeInSessionState

private enum DataTypeInSessionState
{
    Unknown,
    Utf8String,
    ByteArray  // 2
}

Dữ liệu chúng ta cần được lưu ở bảng session state dưới dạng binary data, do đó giá trị của dataTypeInSessionState phải là 2. Tổng kết lại key có cấu trúc như sau

3

Sau khi lấy được binarydata, đây là đoạn code trả về dưới dạng file

using (Stream stream = new MemoryStream(dataAsBytes, false))
{
    if (string.Compare(strA, "fa", StringComparison.OrdinalIgnoreCase) != 0)
    {
        context.Response.AppendHeader("Content-Disposition", "attachment;filename=\"image\"");
        context.Response.AppendHeader("X-Download-Options", "noopen");
        context.Response.ContentType = ImageUtils.GetContentType(dataAsBytes);
        return InlinePicture.ReadInfoFromStream(binaryWriter, stream);
    }
    context.Response.ContentType = "application/octet-stream";
    if (FileAttachment.ReadInfoFromStream(binaryWriter, out num, out empty, stream))
    {
        FilePathUtils.AddFileDownloadHttpHeader(context, empty);
        return true;
    }
    return false;
}

vậy dl phải có giá trị là 'ip'. Các biến gửi đến FormServerAttachments.aspx có dạng như sau

4

Các bước khai thác

Sau khi phân tích chi tiết, các bước khai thác lỗi được tóm gọn như sau:

  1. Tạo infopath list trên site.
  2. Mở form tạo một item mới trên list, lưu lại itemId từ response.
  3. Attachment file chứa payload lên item đó, tuy nhiên không ấn save để session state được giữ nguyên ở database.
  4. Đưa thông tin itemId có được từ bước 2 vào request gửi đến FormServerAttachments.aspx, lưu lại thông tin attachmentId từ response.
  5. Đưa attachmentId vào request kích hoạt deserialize ở ChartPreviewImage.

Quyền

Mặc định một tài khoản bình thường có quyền tạo sub-site và tài khoản đó sẽ có toàn quyền trên site mới. Do đó để khai thác lỗi chỉ cần tài khoản có các quyền cơ bản.

Proof of Concept

https://youtu.be/1Ckjh-uuNu4

REFERENCES

https://www.zerodayinitiative.com/blog/2021/3/17/cve-2021-27076-a-replay-style-deserialization-attack-against-sharepoint