Ben 10
My writeup for the Srdnlen CTF 2025 "Ben 10" challenge
You can directly go to Solution if you don’t want to see my process for solving this challenge.
Introduction
This is the first time I’ve tried a CTF with a time limit; unfortunately, I started on the last day, so I couldn’t try all of the challenges. Regardless, this challenge was fun as it forced me to do a code review to understand the issue fully.
BlackBox approach
First Glance
When going to the target site I got an option to either Signup, Login, or Forgot Password
Figure 1
Since I don’t have an account yet, I signed up first. And after I logged in using the account I created I was taken to a page that has a bunch of different images.
Figure 2
Clicking directly on an image redirects me to a page of the image I clicked. But for the last(10th) image I got an error saying: Only Admins can access Ben10!
Figure 3
With this I assumed that I needed to takeover an admin account named admin
and that the 10th image would give me the flag.
Signing up with admin username
But there is no need for a takeover if we can just sign up as an admin right? So I tried to sign up using admin
as a username
but had no luck, so I moved to another feature.
Figure 4
Forgot Password
Trying this feature immediately gives a red flag. It gives us a reset token after giving a username
I want to recover
Figure 5
Then clicking the Reset your password
redirects me to a page that asks for a Username
and a New Password
How it works?
- Asks for a
username
- Gives a
reset token
- Asks again for a
username
and anew password
This was a good indicator for me since it means that I can change whose password
I want to change by giving a username
I don’t own in the last step.
So I did…
Figure 6
What a bummer! I thought it was going to be easy XD
At least this tells me that it’s either there’s no username admin
or the system just gives this response instead of a 403
to mislead players :P
So to test my theory I created a new user and tried to change its password using the Forgot Password
feature
Figure 7
Welp, it seems that there’s a permission check…with this I assumed that we can’t use a different username for the first and third step because of the permission check and that the admin
username doesn’t exist since we didn’t get a user not found
error. But, we are sure that there’s an admin
because of the error we got from trying to access the 10th image (Figure 3) And the flow is still sketchy to me…
Since we were given a source code my next idea is that maybe the admin username
is being leaked there.
WhiteBox approach
Looking for the Admin username
Actually, the reason why I tried doing a Blackbox
approach first is because I was scared to look at the source code because I’m not really good at doing code reviews XD
Continuing…
The first thing I did was to see if the admin username
is being leaked somewhere in the code, and it didn’t take a lot of time to find it. When I looked at home.html
there it was the secret admin username
1
<div style="display:none;" id="admin_data"> {admin_username} </div>
So I checked the page source back in the target site and finally
1
2
3
4
5
<h1> Welcome, dats</h1>
<h2> Do you like the aliens on my Omnitrix?</h2>
<!-- secret admin username -->
<div style="=display:none;" id="admin_data">admin^dats^1c4cdf3c26</div>
so we now have the admin username
–> admin^dats^1c4cdf3c26
. The next step is going to be the last right? I can just request a reset token
using the admin^dats^1c4cdf3c26
username and we’re done, right??? Goodnight and 8 hours of sleep let’s go :)
Yeah…no!!!
Figure 8
At this point, I just said to myself that I have the code so I should just find the issue in the code so I don’t waste more time XD
App.py
Since I’m quite familiar with Python understanding the code was manageable(thankfully)
I started at the top instead of directly going to the function where the resetting of password
is happening. This is because I thought it would be good practice for me to read and understand the code that others wrote :D
I immediately saw the reason why I was getting the errors before
/register (shortened code)
1
2
3
if username.startswith('admin') or '^' in username:
flash("I don't like admins", "error")
return render_template('register.html')
/reset_password (shortened code)
1
2
3
if username.startswith('admin'):
flash("Admin users cannot request a reset token.", "error")
return render_template('reset_password.html')
It checks if the input we give starts with "admin"
if it is then it will trigger the error.
/forgot_password (shortened code)
Please check the comments
in the code for the explanation :)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# It checks if the username doesn't start with admin
if not username.startswith('admin'):
token = get_reset_token_for_user(username) # username = request.form['username']
# If TRUE it checks if the reset_token is valid and will be used for the inputted username
if token and token[0] == reset_token: # reset_token = request.form['reset_token']
# If TRUE then it proceeds to reset the password of the variable username
update_password(username, new_password)
flash(f"Password reset successfully.", "success")
return redirect(url_for('login'))
# If FALSE then it gives out an error "Invalid reset token for user."
else:
flash("Invalid reset token for user.", "error")
# If however the username starts with admin
else:
# It splits the username using `^` as a separator
# Then it gets the second index [1] of the array. Note: the first index is [0]
username = username.split('^')[1]
token = get_reset_token_for_user(username)
# Then it checks if the reset_token is valid and will be used for the inputted username
if token and token[0] == reset_token:
# If TRUE then it proceeds to reset the password however...
# IMPORTANT: instead of using "username" it uses "request.form['username']"
# This means that the code will change the password of the unsplit username
update_password(request.form['username'], new_password)
flash(f"Password reset successfully.", "success")
return redirect(url_for('login'))
else:
flash("Invalid reset token for user.", "error")
It might seem confusing so I’ll try to show what’s going to happen clearly.
Code Flow
Remember: How does Forgot Password
work?
- Asks for a
username
- Gives a
reset token
- Asks again for a
username
and anew password
REMINDER!!! We are already in the third step here(/reset_password
is responsible for the first and second step)
Let’s say that we used dats
in the first step and admin^dats^1c4cdf3c26
for the third step
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
else:
# admin^dats^1c4cdf3c26 --> dats
username = username.split('^')[1]
token = get_reset_token_for_user(username)
# True since the username is now dats and we used dats in the first step
if token and token[0] == reset_token:
# Instead of using the updated value of the username variable --> "dats"
# It uses the value I gave in the third step --> "admin^dats^1c4cdf3c26"
update_password(request.form['username'], new_password)
# And this results in the system changing
# The password of the admin instead of my account :P
flash(f"Password reset successfully.", "success")
return redirect(url_for('login'))
Solution
All in all, to takeover the account of the admin
all we need to do is:
- Leak the
username
of theadmin
in our case it’s –>admin^dats^1c4cdf3c26
- Request for a
reset token
using ausername
we own –>dats
- Get the
reset token
and proceed to the next page - Use
admin^dats^1c4cdf3c26
in theusername form
- Login using the new credentials of the
admin
and check the 10th image.
Flag:srdnlen{b3n_l0v3s_br0k3n_4cc355_c0ntr0l_vulns}
Figure 9
Lessons Learned
Always doubt your assumptions. Because I wasn’t able to change the password of my second account, I assumed that the permission check was there, not thinking to double-check with the username of the admin when I found it. If I did, then I would have already solved the challenge without even looking at the code.
It’s still a win though since I got out of my comfort zone. But in the wild, where we normally don’t have the source code handed to us. Doubting our assumptions is definitely beneficial :D
The result of reviewing the code was better than I expected. At first, I thought there was no way that I would understand what it was doing but thankfully most of the lines are pretty self-explanatory, and with a little bit
(a lot)of Googling, I was able to understand how it was working.For example, I had no idea at first what the method
split
was doing, so I tried it myself and figured out that it separates the string based on what the separator is and puts them in an array1 2 3 4 5 6 7 8 9 10 11 12 13
>>> username = "ttt^ggg^sss" >>> username = username.split("^") >>> username[1] 'ggg' >>> username[2] 'sss' >>> username[3] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range ## I was even reminded that the index starts in [0] lol >>> username[0] 'ttt' >>>
Thanks to the Sardinia Len team for creating the CTF.
I hope you enjoyed this write-up. Thanks for reading and have a nice day :D
-Datsuraku147