[P2O Vancouver 2023] SharePoint 预认证 RCE 链(CVE-2023–29357 & CVE-2023–24955)
作者:Nguyễn Tiến Giang (Jang)
日期:2023年9月25日
阅读时间:18分钟
目录
简要说明
在 Pwn2Own Vancouver 2023 中,我可能成功利用了 SharePoint 目标。虽然现场演示仅持续约30秒,但发现和构建完整利用链的过程耗费了近一年的细致努力和研究。
该利用链利用两个漏洞实现 SharePoint 服务器上的预认证远程代码执行(RCE):
- 认证绕过 – 未经认证的攻击者可通过伪造有效的 JSON Web Tokens(JWTs),使用 none 签名算法绕过 OAuth 认证中的签名验证检查,冒充任何 SharePoint 用户。该漏洞在项目开始两天后发现。
- 代码注入 – 具有 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)
补丁下载:
漏洞 #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
当满足上述条件时,将调用 SPApplicationAuthenticationModule.ConstructIClaimsPrincipalAndSetThreadIdentity() 在 [2] 处继续处理认证请求。
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 Tokens(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 解码的头部分是:
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 请求示例: