- JavaScript 76.6%
- HTML 10.4%
- CSS 9.8%
- Dockerfile 2.1%
- Go Template 1.1%
| backend | ||
| frontend | ||
| scripts | ||
| templates | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| conf.json | ||
| docker-compose.yml | ||
| README.md | ||
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
- De frontend staat nu op http://localhost:8080.
- De backend staat nu op http://localhost:3000.
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:
- Maak
backend/vragen/<naam>/<naam>.jsmet de data-laag, dus wat de backend moet doen of teruggeven. - Maak
backend/vragen/<naam>/route.jsmet eenregister(app)-functie. - Maak
frontend/vragen/<naam>/index.htmlen eventueel bijbehorende script om de vraag op de frontend te kunnen bezoeken. - Voeg de naam toe aan een of meer versies in
conf.json. - Restart
npm run devof docker, zodatserver.jsenindex.htmlopnieuw 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.