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

本文详细分析了SharePoint服务器中的两个关键漏洞:认证绕过(CVE-2023–29357)允许未认证攻击者模拟任意用户,代码注入(CVE-2023–24955)通过恶意BDCMetadata文件实现远程代码执行,最终形成完整的预认证RCE利用链。

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

September 25, 2023 · 18 min · Nguyễn Tiến Giang (Jang)

目录

简要说明

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

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

  • 认证绕过 – 未认证攻击者可通过伪造有效的JSON Web令牌(JWTs),使用none签名算法绕过OAuth认证中的签名验证检查,模拟任意SharePoint用户。此漏洞在项目开始两天后即被发现。
  • 代码注入 – 具有SharePoint所有者权限的用户可通过替换Web根目录中的/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_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令牌(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
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
4
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
//...
WWW-Authenticate: Bearer realm="3b80be6c-6741-4135-9292-afed8df596af",client_id="000000
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计