IERG 4851 · Week 9

In-Class Lab: The Web, from the Request Up

Five mistakes, one app each. You attack the app assigned to you.

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.

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.

Use only your own port. The ports are consecutive (9200, 9201, …), so a neighbour's app is one digit away. Opening another student's port works — it is a normal app — but its flags belong to that student, and the score server refuses to credit them to you. If a flag you redeem is rejected as belonging to another student, you are on the wrong port: run myweb again and check the number.
Open it in your own browser — do this first. The app only listens inside the lab machine, so forward its port over SSH and open it locally (use your own port in both places):
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

flagwhat it does
-ishow 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 jarsave cookies to a file / send them back
-vshow the whole conversation

What the app contains

pagewhat it is
/the home page, with links to the rest
/logina login form; the app checks it against a small table of users
/guestbooka public message board — what you write is stored and shown to every visitor, as HTML
/mythe five challenges and the flag for each one you have solved
admin visiton 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:

6. Rules