Sep 29, 2023

Exploiting ASP.NET TemplateParser — Part II: SharePoint (CVE-2023-33160)

In Part I, we dug into the internals of the ASP.NET TemplateParser and elaborated its capabilities in respect to exploitation.

In this part, we will look into whether and how this can also be exploited to gain Remote Code Execution. While this research was originally focussed on the TemplateParser, the newly discovered technique was also applicable to SharePoint on-premises and SharePoint Online. So we’ll elaborate on how SharePoint protects against the use of malicious code and will present a novel trick that allowed to bypass these security measures (CVE-2023-33160).

Introduction

SharePoint users are allowed to upload and create custom .aspx pages:

A fundamental assumption of the Windows SharePoint Services technology is that “untrusted users” can upload and create ASPX pages within the system on which Windows SharePoint Services is running. These users should be prevented from adding server-side code within ASPX pages, but there should be a list of approved controls that those untrusted users can use. One way to provide these controls is to create a Safe Controls list.

https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2007/ms581321(v=office.12)

For that, there is the SPPageParserFilter class in SharePoint that extends the PageParserFilter class of ASP.NET to validate the server control type specified in the source code against this Safe Controls list. This validation happens already during the tokenization step in the TemplateParser.ProcessBeginTag(Match, string) method:

// If we have a control type filter, make sure the child control is allowed
if (_pageParserFilter != null) {
    Debug.Assert(childType != null);

    if (!_pageParserFilter.AllowControlInternal(childType, subBuilder)) {
        ProcessError(SR.GetString(SR.Control_type_not_allowed, childType.FullName));
        return true;
    }
}

In this code excerpt the childType variable represents the Type of the specified server control. The inner workings of SharePoint’s SPPageParserFilter were previously elaborated by Soroush Dalili in A Security Review of SharePoint Site Pages as well as in Room for Escape: Scribbling Outside the Lines of Template Security by Oleksandr Mirosh and Alvaro Muñoz. We recommend reading their work first to be on the same page.

TemplateParser Challenges and Ideas

As detailed in Part I, the parsing step of the TemplateParser has the following policy:

  • Type can only be controlled using custom server controls and the @ Register directive:

    <%@ Register
        TagPrefix="MyPrefix"
        Assembly="MyAssembly"
        Namespace="MyNamespace"
    %>
    <MyPrefix:MyTypeName
        runat="server"
    />
    
  • Server control tag names can only be [\w:.]+

  • Server control type resolution is done using Assembly.Load("MyAssembly").GetType("MyNamespace" + "." + "MyTypeName")

  • Property types are derived from the specified server control type by reflection

Let’s look into the challenges one by one.

Controlling The Property Type

Property types are derived from the specified server control type by reflection

Idea: Use a generic type where the type argument defines the property’s type.

Consider the generic class ExpandedWrapper<TExpandedElement> where the generic type parameter TExpandedElement determines the type of the ExpandedElement property:

typeof(System.Data.Services.Internal.ExpandedWrapper<DateTime>)
    .GetProperty("ExpandedElement").PropertyType == typeof(DateTime)

This also means, assigning a value to the ExpandedElement property of a ExpandedWrapper<DateTime> instance would result in the DateTimeConverter.ConvertFromInvariantString(string) method being called.

But how can we create an instance of ExpandedWrapper<DateTime>?

Controlling The Custom Server Control Type

Server control type resolution is done using Assembly.Load("MyAssembly").GetType("MyNamespace" + "." + "MyTypeName")

Idea: Maybe we could trick .NET into ignoring the appendix, similar to what we have observed in Bypassing .NET Serialization Binders.

What if we could specify the targeted type in the Namespace entirely and somehow hide the appended "." + "MyTypeName" portion?

Let’s make a quick test and insert an arbitrary character right after the namespace and see whether the type can still be resolved:

Quick brute-force test that allowed dropping the appendix

Surprisingly, a null character \0 appears to terminate the parsing of the type name and the appendix .MyTypeName gets ignored! What year is it, again!?

Server control tag names can only be [\w:.]+

Check. As the appended type name gets ignored it can be chosen arbitrarily.

And because a null character can be appended to the Namespace attribute of the @ Register directive, we can specify a custom server control of type ExpandedWrapper<DateTime> and set its ExpandedElement property:

<%@ Register
    TagPrefix="x"
    Assembly="System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    Namespace="System.Data.Services.Internal.ExpandedWrapper`1[[System.DateTime,mscorlib]]&#0;"
%>
<x:y
    runat="server"
    ExpandedElement="foobar"
/>

Great! So this works — at least out of the context of SharePoint’s SPPageParserFilter:

Successful test of ExpandedWrapper<DateTime> with a direct call to TemplateParser.Parse(string)

The remaining challenge was to find a generic type that passed the SPPageParserFilter. As well as a targeted type that provides an interesting static Parse(string) method or has associated an interesting TypeConverter that gets called on property setting. And there sure was at least one in SharePoint:

Successful Remote Code Execution on SharePoint Online printing diagnostics

We won’t publish the exploit here and leave it as an exercise to the reader.

The Patch

Microsoft fixed this vulnerability (CVE-2023-33160) in the July 2023 security updates for SharePoint by also checking the type arguments of a generic type. Here is the diff of the decompiled SPageParserFilter class files of the assembly file versions 16.0.16130.20548 (June 2023) and 16.0.16130.20640 (July 2023):

diff --git a/Microsoft.SharePoint/Microsoft/SharePoint/ApplicationRuntime/SPPageParserFilter.cs b/Microsoft.SharePoint/Microsoft/SharePoint/ApplicationRuntime/SPPageParserFilter.cs
index d1ee368..7f893c5 100644
--- a/Microsoft.SharePoint/Microsoft/SharePoint/ApplicationRuntime/SPPageParserFilter.cs
+++ b/Microsoft.SharePoint/Microsoft/SharePoint/ApplicationRuntime/SPPageParserFilter.cs
@@ -146,9 +146,32 @@ namespace Microsoft.SharePoint.ApplicationRuntime
 					{
 						flag = false;
 					}
-					else if ((this.IsTypeOrSubclass(typeof(Control), controlType) || !this._safeModeDefaults.ControlCompatMode) && !this._pageParserSettings.AllowUnsafeControls)
+					else if ((this.IsTypeOrSubclass(typeof(Control), controlType) || !this._safeModeDefaults.ControlCompatMode) && (!this._pageParserSettings.AllowUnsafeControls || AllowUnsafeControlsOverride.AlwaysBlockUnsafeControls))
 					{
 						flag = this._safeControls.IsSafeControl(this._isAppWeb, controlType, out text);
+						if (flag)
+						{
+							foreach (Type type in controlType.GetGenericArguments())
+							{
+								try
+								{
+									flag = this.AllowControl(type, childBuilder);
+									if (!flag)
+									{
+										break;
+									}
+								}
+								catch
+								{
+									ULS.SendTraceTag(539066894U, ULSCat.msoulscat_WSS_Runtime, ULSTraceLevel.Medium, "AllowControl failed on templateType: {0} in ControlType VirtualPath: {1}", new object[]
+									{
+										type,
+										controlType
+									});
+									throw;
+								}
+							}
+						}
 						if (!flag && this.IsTypeOrSubclass(typeof(UserControl), controlType) && this._allowedUserControlVirtualPaths != null)
 						{
 							if (this._allowUserControlTypes == null)
@@ -182,7 +205,7 @@ namespace Microsoft.SharePoint.ApplicationRuntime
 							return false;
 						}
 						string text2;
-						if (!this._pageParserSettings.AllowUnsafeControls && !this._safeControls.IsSafeControl(this._isAppWeb, controlType, out text2))
+						if ((!this._pageParserSettings.AllowUnsafeControls || AllowUnsafeControlsOverride.AlwaysBlockUnsafeControls) && !this._safeControls.IsSafeControl(this._isAppWeb, controlType, out text2))
 						{
 							ULS.SendTraceTag(2744449U, ULSCat.msoulscat_WSS_Runtime, ULSTraceLevel.High, "Allowing ControlCompatMode=true class on page. Class: {0}, VirtualPath: {1}, TagName: {2}", new object[]
 							{
@@ -226,7 +249,7 @@ namespace Microsoft.SharePoint.ApplicationRuntime
 		{
 			this._IsMasterPage = this.IsTypeOrSubclass(typeof(MasterPage), baseType);
 			string text = null;
-			bool flag = this.Exclusion || this._pageParserSettings.AllowUnsafeControls || this._safeControls.IsSafeControl(this._isAppWeb, baseType, out text);
+			bool flag = this.Exclusion || (this._pageParserSettings.AllowUnsafeControls && !AllowUnsafeControlsOverride.AlwaysBlockUnsafeControls) || this._safeControls.IsSafeControl(this._isAppWeb, baseType, out text);
 			if (!flag && text != null)
 			{
 				throw new SafeControls.UnsafeControlException(SPResource.GetString("UnsafeBaseTypePageParserFilterError", new object[]
@@ -253,7 +276,7 @@ namespace Microsoft.SharePoint.ApplicationRuntime
 				case VirtualReferenceType.Page:
 					return !SPRequestModule.IsExcludedPath(referenceVirtualPath, false);
 				case VirtualReferenceType.UserControl:
-					flag = (this._pageParserSettings.AllowUnsafeControls || this._safeControls.IsSafeControl(this._isAppWeb, referenceVirtualPath));
+					flag = ((this._pageParserSettings.AllowUnsafeControls && !AllowUnsafeControlsOverride.AlwaysBlockUnsafeControls) || this._safeControls.IsSafeControl(this._isAppWeb, referenceVirtualPath));
 					if (flag)
 					{
 						if (this._allowedUserControlVirtualPaths == null)

Note that this patch only applies to SharePoint and not to ASP.NET in general. So if you have control over the code to be parsed by TemplateParser, you still can achieve Remote Code Execution.

Conclusion

The TemplateParser of ASP.NET holds unexpected capabilities, which can lead to Remote Code Execution during the parsing process.

Special thanks are due to the null character truncation that enables the use of generic types. This truncation also applies to assembly and type loading in general, which may allow bypassing security protections such as serialization binders (see also Bypassing .NET Serialization Binders).

In fact, it appears that even currently supported .NET runtimes are still affected:

Runtime Assembly Name Truncation Type Name Truncation
.NET Framework 4.8.1 ✔️ ✔️
.NET 6 ✔️ ✔️
.NET 7 ✔️
.NET 8 ✔️

The vulnerabilities in Sitecore (CVE-2023-35813) and SharePoint (CVE-2023-33160) demonstrate that vulnerabilities utilizing the ASP.NET TemplateParser are not just theoretical but have an actual impact on popular real world applications. And due to the fact that assembly and type name truncation still works in current .NET releases, it is likely that other vulnerable software is out there waiting to be found.