1. Your address, and how you submit a flag
Do this first, from ~/ierg4851/contactless-ctf on the lab
machine. There is no form and nothing goes to Blackboard: you submit a flag
by redeeming it, and OK 250 is your receipt.
python3 score/keygen.py
wrote mykey.json address: 3c42822cbbcfb07cac13ad9afa7555cb10fc58f7 verified: address derives from this key registered as u1155203181
Then, for each flag you earn:
python3 score/redeem.py --flag IERG4851_WEB_ROBOTS_933e4aa8 --to 3c42822cbbcfb07cac13ad9afa7555cb10fc58f7
Each flag belongs to one account — yours. Redeem it to the address you registered; the ledger refuses a flag presented from another student's account.
OK 250 your total is now 250 points
myscore shows your points and rank. The projector page at
http://<lab-machine>:8080/ shows everyone and a tick per
challenge (user ierg4851, password given in class).
2. Where the app is, and how you reach it
Everything runs inside the lab machine — the same host you log in to for the card lab. The app is not on the internet.
- It listens on
127.0.0.1:<port>— loopback, inside that machine. - There is no public URL. Your laptop's browser cannot open it.
- You reach it by logging in to the lab machine and running
curlthere.
ssh <your-username>@<lab-machine> myweb
your web app: http://127.0.0.1:9388/ start: curl -i http://127.0.0.1:9388/ your flags: curl -s http://127.0.0.1:9388/my
Every command in this manual is typed in that SSH session. The number
above is only an illustration: use the port myweb printed for
you, in every command. Each student has a different one.
myweb again and check the number.ssh -L 9388:127.0.0.1:9388 <you>@<lab-machine>Then browse
http://127.0.0.1:9388/. Keep that SSH session open
while you work. This is how you will actually see an injected
script run.curl, the whole toolset you need
| flag | what it does |
|---|---|
-i | show the status line and headers with the body |
-H "Name: value" | add a request header |
-d "a=1&b=2" | send a form body (POST) |
--data-urlencode "x=<script>" | send a form field with the value URL-encoded |
-c jar -b jar | save cookies to a file / send them back |
-v | show the whole conversation |
What the app contains
| page | what it is |
|---|---|
/ | the home page, with links to the rest |
/login | a login form; the app checks it against a small table of users |
/guestbook | a public message board — what you write is stored and shown to every visitor, as HTML |
/my | the five challenges and the flag for each one you have solved |
| admin visit | on the guestbook: a button that asks the admin's browser to load this page |
Two challenges use the guestbook. It is the only place where your text becomes part of a page other people (including the admin) will load.
Reading and posting. The guestbook has a form with two
fields, name and text:
# read the page
curl -s http://127.0.0.1:<port>/guestbook
# post a message (the value is URL-encoded for you)
curl -d "name=alice" --data-urlencode "text=hello from alice" \
http://127.0.0.1:<port>/guestbook
# read it back — the new entry is at the end of the list
curl -s http://127.0.0.1:<port>/guestbook
Replace <port> with your port; the names
and text above are ordinary strings. If you post something that looks like a
tag (<what you want>), the browser renders it as HTML —
often as nothing at all. That is the bug in challenge 5, not a failed post.
If a payload makes the page unusable — a redirect, a blank
page, an unclosed tag — the guestbook has a clear the guestbook
button at the top, and the home page (/) has a
reset the guestbook button that works even when the
guestbook itself is wrecked. From a shell:
curl -X POST http://127.0.0.1:<port>/guestbook/clear.
It keeps only the welcome message.
A successful post answers 303 and redirects
to the guestbook; the message is stored, not just echoed. Posts live in the
app's memory: they survive reloads, and are cleared when the lab restarts.
Two fields, two behaviours. The
name is escaped and shown as text. The text is
inserted into the page as HTML — that is the field payloads belong in.
Putting a tag in name does nothing visible.
The admin's browser
One challenge (CSRF) needs the administrator to load a page. You cannot log in as them. On the guestbook there is a button:
ask the admin to visit this page
Press it and the app opens the guestbook in an admin session — the
admin's browser — loads what the page references, and the next screen shows
exactly what it fetched. From the shell, the same thing is
curl -d url=/guestbook http://127.0.0.1:<port>/report.
You never see its cookie; you make its browser do the request. The
screen after the button lists what it fetched: time,
kind (target = the page, sub:img /
sub:script = something that page loaded), URL and result.
3. The five challenges
Any order. Each entry shows one command — where to start. When the app
refuses you, read the refusal: it says what it is missing. After each
challenge, curl -s http://127.0.0.1:<port>/my shows the
flag for that challenge; redeem it as in section 1.
1 — robots
curl -i http://127.0.0.1:<port>/robots.txt
It names a directory the owner does not want indexed. Ask for that directory and follow the refusal.
Success: you end up with the flag, and /my
shows robots.
2 — the header the panel trusts
curl -i http://127.0.0.1:<port>/admin/panel
The 403 tells you which header it wants and shows the value it actually received. Send the header it asks for.
Success: the panel returns the flag.
3 — the login
curl -i http://127.0.0.1:<port>/login
The form is checked with a query built by pasting your input into it:
SELECT name FROM users WHERE name='<your name>' AND pass='<your password>'
Make that condition true without knowing the password, then keep the
cookie the app gives you and use it on /account.
Success: /account answers as the admin.
4 — CSRF: the admin's browser sends its own cookie
CSRF is not a link the admin clicks: it is a request the admin's browser makes on its own, because a page it loaded referenced it. The classic form is one tag:
<img src="/admin/grant?to=<name>">
The endpoint.
GET /admin/grant?to=<name> is an admin page that
does something permanent — it grants access — instead of just
showing information. And it asks for no proof that the request came from
the app's own form. The only thing it checks is the admin's cookie.
Why that is dangerous. A CSRF token is a random value the app puts in its own forms and checks when the form is submitted. This endpoint has none, and it answers a GET — so any page can make a browser request it with one tag, without JavaScript and without the admin clicking a button. That is the tag above.
Which page? You need a page that (a) contains your tag and (b) the admin will load. The guestbook is exactly that: it stores what you write and renders it as HTML, so your tag becomes part of the page. Post it there, then press the button below the form so the admin loads it.
What happens then. The admin's browser opens the
guestbook, sees
<img src="/admin/grant?to=…"> in the page, and loads it
as a subresource — with the admin's cookie. The grant runs. In the table
after the button, the guestbook is the target and the grant is
a sub.
This is the same page challenge 5 uses. The difference is
the tag: an <img> makes a request (CSRF), a
<script src> makes a browser run something of yours (XSS).
Success: /my shows csrf. The
table after the button shows the grant as a sub:img request,
not as the page the admin opened.
Use your own username in to=: the app
only counts a grant to its owner. If you see
not for this app (owner is …) in that table, the name was
someone else's. (The demo account ctf-demo owns port
9200, so its to= is ctf-demo.)
5 — run your JavaScript in a browser
No admin needed for this one: your own browser is the victim's browser.
First, see it run. Open your app through the port forward, post this in the guestbook, and reload:
<script>alert(document.domain)</script>
You get an alert: the page executed your text as a script. (Posting
<b>hello</b> shows the same more quietly — it comes
back bold.)
Now make it count. The payload must be JavaScript that makes a request of its own — not a tag that loads a file. Any of these is code running in the page:
<script>fetch('/collect?u=<your name>')</script>
<img src=x onerror="fetch('/collect?u=<your name>')">
<svg onload="fetch('/collect?u=<your name>')">
Reload the page. Your code runs and calls /collect; the app
marks xss in /my.
Why this is execution and not just a request: fetch()
sends Sec-Fetch-Dest: empty, which only JavaScript can produce.
An <img src=…> sends image, a
<script src=…> sends script — both are tag
loads, and neither counts. That is the line between challenge 4 and 5:
a tag makes a request, your code has to run.
In the real world you learn it ran the same way: your server gets the callback. The callback is evidence of execution, not the attack itself.
Success: /my shows xss.
4. Where you stand
Redeeming a flag is the submission; there is nothing else to hand in.
myscore shows your points and rank; the projector page shows
everyone, with points and a tick per challenge. Both follow the account
that redeemed the flag — your address. Each flag pays once.
A flag belongs to the student whose app it came from. If
the ledger refuses your redeem with this flag belongs to another
student, you are on the wrong port — run myweb and use the
number it prints.
5. The explain round
Three or four of you, picked by the board, spend three minutes each. You need the request you sent, the response that mattered, and one sentence:
- what did the server trust that it should not have?
- what did the browser do for the attacker, and why?
- what would you change, in one line?
6. Rules
- Attack only the app assigned to you.
- Everything is local and simulated; nothing here touches the internet.
- Redeem to your own registered address; each flag belongs to you and pays once.
- Ask when stuck — the first question is usually "which port is mine".