SharePoint 预认证 RCE 链漏洞深度分析(CVE-2023–29357 与 CVE-2023–24955)

本文详细分析了SharePoint中的两个关键漏洞:认证绕过(CVE-2023–29357)允许攻击者通过伪造JWT令牌冒充任意用户,代码注入(CVE-2023–24955)利用DynamicProxyGenerator编译恶意程序集实现远程代码执行,最终形成完整的预认证RCE利用链。

[P2O Vancouver 2023] SharePoint 预认证 RCE 链(CVE-2023–29357 & CVE-2023–24955)

作者:Nguyễn Tiến Giang (Jang)
日期:2023年9月25日
阅读时间:18分钟

目录

简要说明

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

该利用链利用两个漏洞实现 SharePoint 服务器上的预认证远程代码执行(RCE):

  • 认证绕过 – 未经认证的攻击者可通过伪造有效的 JSON Web Tokens(JWTs),使用 none 签名算法绕过 OAuth 认证中的签名验证检查,冒充任何 SharePoint 用户。该漏洞在项目开始两天后发现。
  • 代码注入 – 具有 SharePoint 所有者权限的用户可通过替换 Web 根目录中的 /BusinessDataMetadataCatalog/BDCMetadata.bdcm 文件,注入任意代码,导致注入的代码被编译成程序集并由 SharePoint 执行。该漏洞于2022年2月发现。

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

受影响产品/测试版本

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

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

分析 web.config 文件时,我意识到至少可以使用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

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

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_token 或 Authorization 头中检索 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 请求的查询字符串参数 prooftoken 或 X-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 Tokens(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 解码的头部分是:

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

则 alg 字段的值为 HS256。

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

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

以下是 aud 字段的有效值示例:

1
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
GET /_api/web/ HTTP
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计