[P2O Vancouver 2023] SharePoint预认证RCE漏洞链(CVE-2023–29357 & CVE-2023–24955)
September 25, 2023 · 18 min · Nguyễn Tiến Giang (Jang)
目录
简要说明
在Pwn2Own Vancouver 2023期间,我可能成功利用了SharePoint目标。虽然现场演示仅持续约30秒,但值得注意的是,发现和构建完整利用链的过程耗费了近一年的细致努力和研究。
该利用链利用两个漏洞在SharePoint服务器上实现预认证远程代码执行(RCE):
- 认证绕过 – 未认证攻击者可通过伪造有效的JSON Web令牌(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配置文件时,我意识到至少可以使用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解码的头部分是:
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
4
|
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
//...
WWW-Authenticate: Bearer realm="3b80be6c-6741-4135-9292-afed8df596af",client_id="000000
|