Een websiteomgeving met basismogelijkheden voor inspectie van websites. https://challenges.femkew.nl/
  • JavaScript 76.6%
  • HTML 10.4%
  • CSS 9.8%
  • Dockerfile 2.1%
  • Go Template 1.1%
Find a file
2026-05-25 21:16:28 +02:00
backend Voeg optie toe om te beginnen bij een ander nummer met de vragen 2026-05-24 22:13:34 +02:00
frontend Voeg optie toe om te beginnen bij een ander nummer met de vragen 2026-05-24 22:13:34 +02:00
scripts Voeg optie toe om te beginnen bij een ander nummer met de vragen 2026-05-24 22:13:34 +02:00
templates Voeg vraag home toe waarbij IDOR mogelijk is 2026-05-24 21:31:02 +02:00
.dockerignore add docker ignore, otherwise it may crash due to conflicting node modules 2026-05-25 21:16:28 +02:00
.env.example Voeg optie toe om te beginnen bij een ander nummer met de vragen 2026-05-24 22:13:34 +02:00
.gitignore Voeg structuur toe zodat vragen verwijderd kunnen worden bij het bouwen. Handig voor de oefentoets waarin de vragen voor de echte toets niet al beschikbaar moeten zijn. 2026-05-24 17:20:05 +02:00
conf.json Add new SQL-injection question with transactions database 2026-05-24 21:48:50 +02:00
docker-compose.yml Voeg optie toe om te beginnen bij een ander nummer met de vragen 2026-05-24 22:13:34 +02:00
README.md Voeg optie toe om te beginnen bij een ander nummer met de vragen 2026-05-24 22:13:34 +02:00

ctf-toetswebsite

Een websiteomgeving met basismogelijkheden voor inspectie van websites.

Projectstructuur

backend/             Express-server
  vragen/<naam>/     Per vraag: data-laag (<naam>.js) en route-registratie (route.js)
frontend/            Statische frontend (HTML + JS)
  vragen/<naam>/     Per vraag: index.html en eventueel script.js
templates/           Templates voor het gegenereerde server.js en index.html
scripts/
  build-version.js   Genereert server.js / index.html voor de actieve versie
                     en verwijdert vragen die er niet in zitten
conf.json            Bepaalt welke vragen in welke versie zitten
.env                 Selecteert de actieve versie en startnummer (zie .env.example)

Welke vragen actief zijn wordt bepaald door conf.json in combinatie met de versie uit .env. Bij npm run dev of docker compose up draait scripts/build-version.js om server.js en index.html te genereren voor die versie.

Setup

Kies eerst een versie. Beschikbare versies staan in conf.json. De versie zet je in .env (kopieer .env.example):

cp .env.example .env
# zet VERSION=1a (of 1b)
# zet STARTVRAAG=1 (of een ander startnummer, bv. 16 voor "Vraag 16, 17, ...")

STARTVRAAG bepaalt het nummer van de eerste vraag op de homepagina; handig wanneer deze vragenset aansluit op een andere toets en de nummering moet doorlopen.

Je kan de waarden ook direct meegeven met docker:

VERSION=1b STARTVRAAG=16 docker compose up --build

Development

Backend:

cd backend
npm install
npm run dev

Frontend (in een andere terminal):

cd frontend
npm run dev

npm run dev draait eerst scripts/build-version.js om server.js / index.html te genereren voor de versie uit .env, en start dan de server. De backend gebruikt node --watch voor hot-reload. Wijzigingen aan conf.json of templates/ hebben een herstart van npm run dev nodig.

Production

docker compose up --build

De prod-build gebruikt een Dockerfile die de inactieve vragen/* mappen volledig verwijdert voordat de runtime image gebouwd wordt. Vragen die niet in de gegeven versie zitten, zitten ook niet in de image.

Let op: wanneer je dit project op een ander domein gaat draaien, moet je de BASE_URL in frontend/constants.js aanpassen naar https://<jouw-domein>/api.

Website bezoeken

Versies beheren

conf.json bepaalt welke vragen in welke versie zitten:

{
    "versions": {
        "1a": ["bestelling", "boodschap", "deknop"],
        "1b": ["cijferlijst", "login", "webwinkel"]
    }
}

Dit is handig voor wanneer je twee versies wil draaien: bijv. een oefentoets en de echte toets.

Een nieuwe vraag toevoegen

Dit zijn de stappen om een nieuwe vraag toe te voegen:

  1. Maak backend/vragen/<naam>/<naam>.js met de data-laag, dus wat de backend moet doen of teruggeven.
  2. Maak backend/vragen/<naam>/route.js met een register(app)-functie.
  3. Maak frontend/vragen/<naam>/index.html en eventueel bijbehorende script om de vraag op de frontend te kunnen bezoeken.
  4. Voeg de naam toe aan een of meer versies in conf.json.
  5. Restart npm run dev of docker, zodat server.js en index.html opnieuw gegenereerd worden.

De vragen

Hieronder per vraag het beveiligingsprobleem en wat de leerling moet doen om de vraag te halen. Elke vraag heeft één of meerdere tags die aangeven welke techniek de leerling moet gebruiken om de vraag op te lossen:

  • INSPECTOR: de leerling moet de devtools (Elements/Sources) gebruiken om naar de HTML of JavaScript van de pagina te kijken, of waardes in de pagina aan te passen.
  • NETWORK: de leerling moet kijken naar het verkeer tussen frontend en backend (Network-tab).
  • CONSOLE: de leerling moet code uitvoeren in de Console-tab van de devtools.
  • SQL-INJECTION: de leerling moet via een invoerveld extra SQL meesturen om de query op de backend te manipuleren.
  • IDOR: de leerling moet een parameter in de request aanpassen om gegevens van een andere gebruiker te zien (Insecure Direct Object Reference / broken access control).

Bestelling

Tags: INSPECTOR, NETWORK

De frontend beperkt het invoerveld met maxlength="4", maar de backend controleert pas of de bestelling langer is dan vier tekens. Er zit dus geen serverside-beperking op de invoer.

De leerling moet de frontend-beperking omzeilen. Dat kan door via de devtools het maxlength-attribuut weg te halen, een langere waarde in te plakken nadat het veld is bewerkt, of het backend-endpoint /api/vragen/bestelling?bestelling=... rechtstreeks aan te roepen met een waarde van meer dan vier tekens.

Boodschap

Tags: NETWORK

De backend stuurt méér terug dan de frontend toont: naast bericht zitten er ook een geheim en een instructie in het JSON-antwoord.

De leerling moet kijken wat de server écht teruggeeft. Via de Network-tab in de devtools (of door /api/vragen/boodschap rechtstreeks te openen) is het hele JSON-antwoord zichtbaar, inclusief het geheim.

Cijferlijst

Tags: SQL-INJECTION

De zoekterm voor het vak wordt zonder controle in de SQL geplakt: ... WHERE vak LIKE '%<input>%' AND naam = 'Lorenzo'. Dat is een klassieke SQL-injection waarmee de filter op naam te omzeilen is.

De leerling moet de SQL doorbreken zodat de naam-check niet meer geldt. Bijvoorbeeld door als zoekterm %' OR '1'='1 of %' OR naam LIKE '% in te voeren, zodat ook de cijfers van andere leerlingen verschijnen. Het idee hier is dat de query wordt gegokt.

Debank

Tags: SQL-INJECTION

Een persoonlijke bankpagina waarop de leerling (als Olga Draaisma op rekening NL48INGB6638473472) zoekt door haar transacties op omschrijving. De zoekterm wordt zonder controle in de SQL geplakt en de backend voert meerdere statements achter elkaar uit.

De leerling moet via SQL-injection extra queries laten uitvoeren, bijvoorbeeld door '; SELECT * FROM rekeninghouder-- als zoekterm in te voeren, waardoor naast de eigen transacties ook een tweede tabel met alle rekeninghouders verschijnt. Een datalek kan ook met één query: bijvoorbeeld ' OR 1=1--, waarmee de filter op het hard gecodeerde rekeningnummer wordt omzeild en de transacties van alle rekeningen verschijnen.

Deknop

Tags: INSPECTOR, NETWORK

De drempelwaarde (75030) staat op de backend, maar het huidige aantal kliks zit in de frontend en wordt iedere klik als URL-parameter meegestuurd. De backend vertrouwt die waarde blind.

De leerling moet zelf een hoog nummer naar de backend sturen, zonder daadwerkelijk 75030 keer te klikken. Dat kan via de devtools (het verborgen <p id="nummer"> aanpassen) of door het endpoint direct aan te roepen, bijvoorbeeld /api/vragen/deknop?nummer=99999999.

Home

Tags: NETWORK, IDOR

Een persoonlijke bankpagina die de persoonsgegevens, rekeningen, betaalpassen en transacties uit transactiesdb laat zien voor de klant uit de querystring van de pagina zelf, bijvoorbeeld /vragen/home/?naam=Bas Mansveld. Als er geen naam in de URL staat, redirect de pagina automatisch naar ?naam=Bas Mansveld. De frontend doet vervolgens een GET naar /api/vragen/home?naam=.... De backend haalt de gegevens op met een veilige (parameterized) SQL-query, maar controleert verder niet of de aanvrager deze gegevens mag inzien.

De leerling moet de naam in de URL-balk van de pagina aanpassen, bijvoorbeeld naar ?naam=Stefan Eckhart of ?naam=Eline Aarts. Daarmee verschijnt direct de complete dashboard van een andere klant. Het idee is dat SQL-injection hier niet werkt, maar dat het probleem zit in het ontbreken van een autorisatiecontrole (broken access control / IDOR).

Inloggen

Tags: INSPECTOR, NETWORK

De controle of de gebruikersnaam en het wachtwoord kloppen staat in de frontend (script.js); de backend stuurt het succesbericht zonder verdere controle terug.

De leerling kan dit op twee manieren oplossen: de hard-coded gebruikersnaam en wachtwoord in script.js opzoeken via de devtools en die invullen, of het backend-endpoint /api/vragen/inloggen rechtstreeks aanroepen, omdat dat geen authenticatie vereist.

Kluis

Tags: INSPECTOR, CONSOLE

Op de pagina staat een kluis die niet via een knop te openen is. In de HTML staat een inline script met een functie openKluis() die aan window wordt gehangen. Die functie haalt het geheim op vanaf de backend en zet het op de pagina, maar wordt nergens automatisch aangeroepen.

De leerling moet de functie ontdekken door in de devtools naar de pagina-source te kijken, en daarna openKluis() in de console aanroepen om het geheim op de pagina te zien verschijnen. Het idee hiervan is dat je ook geen geheimen in de JavaScript op de frontend moet zetten.

Login

Tags: SQL-INJECTION

De gebruikersnaam wordt zonder controle in de SQL geplakt. De backend voert eerst een query uit om de salt op te halen en daarna een query om de combinatie van gebruikersnaam en hash te zoeken.

De leerling moet de tweede query zo manipuleren dat de wachtwoord-check niet meer meedoet. Dat kan met een SQL-injection in het gebruikersnaamveld, bijvoorbeeld met ' OR 1=1 --, waardoor het deel met de wachtwoord-hash wordt uitgecommentarieerd en het deel bij de gebruikersnaam altijd waar wordt. De gebruiker 'admin' zit niet in de database.

Webwinkel

Tags: SQL-INJECTION

De zoekterm op productnaam wordt zonder controle in de SQL geplakt en de backend voert meerdere statements achter elkaar uit. Dat maakt het mogelijk om naast de bestellingen van klant 6 ook andere gegevens uit de database op te halen.

De leerling moet via SQL-injection extra queries laten uitvoeren, bijvoorbeeld door '; SELECT * FROM klant-- als zoekterm in te voeren. Daarmee verschijnt naast het oorspronkelijke resultaat ook een tweede tabel met informatie van andere klanten of producten. Uiteraard kan er ook al een datalek komen met één query, bijvoorbeeld met de input ' OR 1=1--, waarin het gedeelte in de WHERE altijd waar wordt.