Post

Ben 10

My writeup for the Srdnlen CTF 2025 "Ben 10" challenge

Ben 10

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

signup

          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.

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!

onlyadminscanaccess

          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.

idontlikeadmins

          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

tokengenerated

          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?

  1. Asks for a username
  2. Gives a reset token
  3. Asks again for a username and a new 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…

adminnotfound

          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

invalidusertoken

          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!!!

admincantreqforgotpass

          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?
  1. Asks for a username
  2. Gives a reset token
  3. Asks again for a username and a new 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:

  1. Leak the username of the admin in our case it’s –> admin^dats^1c4cdf3c26
  2. Request for a reset token using a username we own –> dats
  3. Get the reset token and proceed to the next page
  4. Use admin^dats^1c4cdf3c26 in the username form
  5. Login using the new credentials of the admin and check the 10th image.

Flag:srdnlen{b3n_l0v3s_br0k3n_4cc355_c0ntr0l_vulns}

flag

          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 array

      1
      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

This post is licensed under CC BY 4.0 by the author.

Trending Tags