BugForge — 2026.04.09

Gift List: OTP Recipient Manipulation → Admin Access

BugForge Authentication Bypass easy

Overview

  • Platform: BugForge
  • Vulnerability: OTP Recipient Manipulation (Authentication Bypass) — admin login send-code endpoint accepts arbitrary username, routing the OTP to any user’s message inbox
  • Key Technique: Changing the hidden username parameter on /admin-login/send-code to redirect the admin OTP to our own inbox, then submitting it to authenticate as administrator
  • Result: Full admin access and flag capture

Objective

Gain admin access to the BugForge “Gift List” application and capture the flag. The application is a gift list manager where users create wish lists, add items, and share lists via public links. Admin access is gated behind an OTP flow.

Initial Access

# Target Application
URL: https://lab-1775772686893-mxgmk6.labs-app.bugforge.io

# Auth details
POST /register with username, password, confirmPassword (form-urlencoded)
JWT cookie "token" (HS256, HttpOnly, SameSite=Lax)
Registered as: haxor (id:2, role: user)

Key Findings

  1. OTP Recipient Manipulation — Authentication Bypass (CWE-287: Improper Authentication) — The POST /admin-login/send-code endpoint accepts a username parameter that determines which user’s inbox receives the OTP code. The HTML form pre-fills this as a hidden input with value administrator, but an attacker can change it to any registered username. The OTP verification endpoint (/admin-login POST) validates the code independently of who received it — if the code is correct, the requester gets an admin JWT. This completely bypasses the admin authentication flow.

Attack Chain Visualization

┌──────────────┐     ┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Register   │────▶│ Recon: Map auth  │────▶│  Manipulate      │────▶│  Read OTP from   │
│   (haxor)    │     │ surface, find    │     │  send-code POST  │     │  /messages,      │
│   Get JWT    │     │ /admin-login     │     │  username=haxor  │     │  submit at       │
│              │     │ with hidden      │     │  (redirect OTP   │     │  /admin-login    │
│              │     │ username field   │     │  to our inbox)   │     │  → admin JWT     │
└──────────────┘     └──────────────────┘     └──────────────────┘     └──────────────────┘

Application Architecture

Component Detail
Backend Express (Node.js)
Frontend EJS templates (server-side rendering)
Auth JWT HS256 — HttpOnly cookie “token”, SameSite=Lax
CSS Bootstrap 5.3.2 (CDN)
Forms Form-urlencoded POST bodies throughout

Endpoint Surface

Endpoint Method Auth Notes
/register GET/POST No username, password, confirmPassword
/login GET/POST No username, password → JWT cookie
/admin-login GET No Separate admin login page
/admin-login/send-code POST No username param controls OTP recipient
/admin-login POST No code param — verifies OTP, grants admin JWT
/dashboard GET Yes User’s gift lists
/lists POST Yes Create new list
/list/:id GET Yes View list items
/lists/:id/items/add POST Yes Add item (name, store_url, priority, notes)
/delete/:list_id/:item_id POST Yes Delete item
/lists/:id/share POST Yes Generate share link
/lists/:id/delete POST Yes Delete list
/share/:uuid GET No Public shared list view
/share/:uuid/items/:id/bought POST No Mark item as bought
/messages GET Yes User’s message inbox
/logout GET Yes Clear JWT cookie

Known Users

Username ID Role
administrator 1 admin
haxor 2 user (us)

Exploitation Path

Step 1: Register and Authenticate

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

username=haxor&password=password&confirmPassword=password

Response: 302 → /login. After login, JWT cookie set with payload {"id":2,"username":"haxor","role":"user","iat":...,"exp":...}.

Step 2: Recon — Identify Admin Login Flow

Navigating to /admin-login reveals a two-step OTP authentication flow:

  1. Send code — a form with a hidden input username=administrator and a “Send Code” button. Submits POST to /admin-login/send-code.
  2. Verify code — after sending, a code input field appears. Submits POST to /admin-login with the OTP code.

The server response on send: “Code sent to administrator inbox” — implying the OTP is delivered as an in-app message, not via email or SMS.

Key observation: the username field is a hidden HTML input, not enforced server-side. The client supplies which user should receive the OTP.

Step 3: Redirect OTP to Our Inbox

Modified the send-code request to route the OTP to our user:

POST /admin-login/send-code HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: token=<our-jwt>

username=haxor

Response: 302 → /admin-login?sent=true (same behavior as the legitimate flow — no indication that the recipient was changed).

Step 4: Retrieve OTP from Messages

Navigated to GET /messages. The OTP code YlrjSdctafxy appeared in our inbox as a new message. The server generated the code and delivered it to whichever user was specified in the username parameter — no validation that the username matches “administrator”.

Step 5: Submit OTP for Admin Access

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

code=YlrjSdctafxy

Response: 302 → /administrator. The server issued a new JWT cookie with admin privileges. Flag displayed on the admin dashboard.


Flag / Objective Achieved

Vector Flag
OTP recipient manipulation on /admin-login/send-code → admin JWT bug{7r6xYgmvDuImv8o5y3eOoJzIiN0YweX5}

Key Learnings

  • Hidden form fields are not access controls. The username=administrator hidden input creates the illusion that only the admin account can receive the OTP, but the server blindly trusts whatever value the client sends. Hidden fields are a UI convenience, not a security boundary.

  • OTP delivery and OTP verification must be coupled. The server generates a code, sends it to user X, then accepts the code from anyone. The verification step checks “is this the right code?” but not “did we send it to the person submitting it?” This decoupling is the root cause — the system has authentication (code check) but no authorization (recipient check).

  • Rate limiting doesn’t matter when you have the code. The application rate-limits send-code (5/window) and verify (10/window), which would slow brute force. But recipient manipulation bypasses brute force entirely — you get the exact code delivered to your own inbox.

  • In-app messaging as OTP delivery is an unusual trust model. Using the application’s own messaging system for OTP delivery means any user who can read their messages can potentially receive misdirected OTPs. External channels (SMS, email) at least require the attacker to compromise a separate system.


Failed Approaches

Approach Result Why It Failed
Mass assignment on /register (role=admin) 200 OK, role unchanged in JWT Server-side registration only picks username/password, ignores extra fields

Tools Used

Tool Purpose
Caido HTTP proxy — request interception, parameter manipulation
Browser DevTools HTML inspection, hidden field discovery

Remediation

1. OTP Recipient Manipulation — Authentication Bypass (CVSS: 9.8 — Critical)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Issue: The POST /admin-login/send-code endpoint accepts a client-supplied username parameter that controls which user’s inbox receives the OTP code. An attacker changes this from administrator to their own username, receives the OTP in their messages, and submits it to gain admin access. No authentication is required — the send-code endpoint is public.

CWE Reference: CWE-287 — Improper Authentication

Fix:

// BEFORE (Vulnerable)
router.post('/admin-login/send-code', rateLimit, async (req, res) => {
  const { username } = req.body;
  const code = generateOTP();
  await sendMessageToUser(username, `Your admin code: ${code}`);
  // Store code for verification
  await storeOTP(code);
  res.redirect('/admin-login?sent=true');
});

// AFTER (Secure)
router.post('/admin-login/send-code', rateLimit, async (req, res) => {
  // Hardcode the recipient — never accept it from the client
  const adminUsername = 'administrator';
  const code = generateOTP();
  await sendMessageToUser(adminUsername, `Your admin code: ${code}`);
  await storeOTP(code);
  res.redirect('/admin-login?sent=true');
});

Additional recommendations:

  • Remove the username parameter from the send-code endpoint entirely — the recipient should be determined server-side, not by client input.
  • Bind the OTP to the admin user session: on verification, check that the submitter is the same user who was intended to receive the code.
  • Use an external OTP delivery channel (email, SMS, authenticator app) rather than in-app messages, so OTP delivery requires access to a separate system the attacker doesn’t control.
  • Consider time-limited OTP codes (e.g., 5-minute expiry) to reduce the window for interception.
  • Log OTP send events with the target username to detect manipulation attempts.

OWASP Top 10 Coverage

  • A07:2021 — Identification and Authentication Failures: The core vulnerability. The admin authentication flow trusts client-supplied input to determine OTP delivery, allowing an attacker to redirect the OTP to their own account and authenticate as admin.
  • A04:2021 — Insecure Design: The separation between OTP generation/delivery and OTP verification without binding the two to the intended recipient is a design-level flaw. The system was designed with the assumption that the hidden form field would always contain “administrator.”
  • A01:2021 — Broken Access Control: The send-code endpoint performs no authorization check — any user (or unauthenticated visitor) can trigger OTP generation and control where it’s delivered.

References


Tags: #otp-bypass #auth-bypass #parameter-manipulation #bugforge #webapp Document Version: 1.0 Last Updated: 2026-04-09

#otp-bypass #auth-bypass #parameter-manipulation #bugforge