Post

Broken Access Control - BreakTheFlask

Broken Access Control - BreakTheFlask

Explaining and Exploiting Broken Access Control Vulnerability

Using the vulnerable flask code from ==> https://github.com/DghostNinja/BreakTheFlask.git

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from flask import Flask, request, redirect, session, send_from_directory, abort
import os

app = Flask(__name__)
app.secret_key = "devkey"

users = {
    "admin": {"password": "adminpass", "role": "admin", "id": 1},
    "alice": {"password": "alice123", "role": "user", "id": 2},
    "bob": {"password": "bob123", "role": "user", "id": 3}
}

UPLOAD_FOLDER = "uploads"
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

@app.route('/')
def home():
    if 'username' in session:
        return f"Logged in as {session['username']} (Role: {session['role']})"
    return "Welcome! Please login at /login"

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        u = request.form['username']
        p = request.form['password']
        if u in users and users[u]['password'] == p:
            session['username'] = u
            session['role'] = users[u]['role']
            session['user_id'] = users[u]['id']
            return redirect('/dashboard')
        return "Invalid credentials"
    return '''
        <form method="post">
            Username: <input name="username"><br>
            Password: <input name="password" type="password"><br>
            <input type="submit" value="Login">
        </form>
    '''

@app.route('/dashboard')
def dashboard():
    if 'username' not in session:
        return redirect('/login')
    return f'''
        <h2>Welcome, {session['username']}</h2>
        <ul>
            <li><a href="/profile?id={session['user_id']}">View Profile (IDOR)</a></li>
            <li><a href="/files/{session['username']}.txt">Download Your File</a></li>
            <li><a href="/admin_panel">Admin Panel</a></li>
        </ul>
    '''

@app.route('/profile')
def profile():
    uid = int(request.args.get('id'))
    for user, data in users.items():
        if data['id'] == uid:
            return f"User: {user}, Role: {data['role']}"
    return "User not found"

@app.route('/files/<path:filename>')
def download_file(filename):
    return send_from_directory(UPLOAD_FOLDER, filename)

@app.route('/admin_panel')
def admin_panel():
    return "Welcome to Admin Panel! You should not be here unless you're admin."

@app.route('/change_role', methods=['POST'])
def change_role():
    if 'username' not in session:
        return redirect('/login')
    new_role = request.form.get('role')
    session['role'] = new_role
    return f"Your role has been changed to {new_role} (Insecure!)"

if __name__ == '__main__':
    open(os.path.join(UPLOAD_FOLDER, "alice.txt"), "w").write("Alice's Secret Data")
    open(os.path.join(UPLOAD_FOLDER, "bob.txt"), "w").write("Bob's Notes")
    open(os.path.join(UPLOAD_FOLDER, "admin.txt"), "w").write("Top Secret Admin Data")

    app.run(debug=True, host='0.0.0.0', port=5000)

1. Hardcoded Credentials

1
2
3
4
5
6
7
8
app = Flask(__name__)
app.secret_key = "devkey"

users = {
    "admin": {"password": "adminpass", "role": "admin", "id": 1},
    "alice": {"password": "alice123", "role": "user", "id": 2},
    "bob": {"password": "bob123", "role": "user", "id": 3}
}
  • From the code we can see the harcoded credentials of Admin, Alice and Bob. This is an unsafe coding practice, as we can use the credentials to login into victim’s account and gain access to their information.

2. Insecure Direct Object Reference (IDOR)

Vulnerable:

1
2
3
4
5
6
@app.route('/profile')
def profile():
    uid = int(request.args.get('id'))
    for user, data in users.items():
        if data['id'] == uid:
            return f"User: {user}, Role: {data['role']}"

  • Log in into user B’s account(Alice) since we already got the credentials from the source code. Then, try to view the user’s profile from the dashboard and from there we see a GET request being sent with the user’s ID in the request.

GET request

  • We can se admin has an ID of 1. We can change the request id to that and by doing this we have access to information on the admin profile.
1
GET /profile?id=1  → View admin’s profile

Admin profile

Fix:

Validate that the requested ID matches the currently logged-in user:

1
2
3
4
5
6
7
8
9
10
11
@app.route('/profile')
def profile():
    if 'user_id' not in session:
        return redirect('/login')
    requested_id = int(request.args.get('id'))
    if requested_id != session['user_id']:
        abort(403)
    user = session['username']
    role = session['role']
    return f"User: {user}, Role: {role}"

3. Unauthorized Access to Admin Panel

Vulnerable Code:

1
2
3
4
@app.route('/admin_panel')
def admin_panel():
    return "Welcome to Admin Panel! You should not be here unless you're admin."

  • Log in as user Alice and try accesing the admin_panel from the href tag.
<a href="/admin_panel">

The server will show the admin panel content, even though the user isn’t an admin.

admin_panel

Fix:

Add a role-based access control check to ensure that only admins can access the admin panel:

1
2
3
4
5
6
@app.route('/admin_panel')
def admin_panel():
    if session.get('role') != 'admin':
        return "Access denied"
    return "Welcome to the Admin Panel!"

4. Privilege Escalation via Session Tampering

Vulnerable code:

1
2
3
4
5
6
7
8
@app.route('/change_role', methods=['POST'])
def change_role():
    if 'username' not in session:
        return redirect('/login')
    new_role = request.form.get('role')
    session['role'] = new_role
    return f"Your role has been changed to {new_role} (Insecure!)"

  • If the unauthorized access to the admin panel is fixed from the code. Try navigating to the /change_role path. Capture and analyse the request. You will notice the GET request is not allowed to change user’s role.

method-not-allowed

  • Change the GET method to POST in the request. This should tamper the role of user Alice.

acces-not-granted

  • Alice role is still unchanged(none) since there is no parameter in the request body to pass the admin role to Alice. So let’s pass an header known as the Content-Type: application/x-www-form-urlencoded

Content-Type: application/x-www-form-urlencoded: this header indicatesheader indicates that the body of the HTTP request contains form data that has been URL-encoded. This encoding is typically used when submitting data via HTML forms. that the body of the HTTP request contains form data that has been URL-encoded. This encoding is typically used when submitting data via HTML forms.

  • Pass the header into the request body along with a role=admin

acces-granted

  • We have a successful request. Alice should now be able to visit admin panel, files and dashboard.

  • Alternative way to do this is passing the role through curl

    1
    2
    3
    
    curl -X POST http://127.0.0.1:5000/change_role \
    -d "role=admin" \
    -b "session=your_session_cookie_here"
    

Fix:

  • We added a check: if session.get(‘role’) != ‘admin’. This ensures that only users who are logged in as admin can change the role of others.

  • If the user is not an admin, the server will return “Access denied”, preventing unauthorized role changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route('/change_role', methods=['POST'])
def change_role():
    if 'username' not in session:
        return redirect('/login')
    
    # Ensure that only an admin can change roles
    if session.get('role') != 'admin':
        return "Access denied"
    
    new_role = request.form.get('role')
    session['role'] = new_role
    return f"Your role has been changed to {new_role} (Secure!)"


Understanding and exploiting Broken Access Control is crucial not only for penetration testers but also for developers aiming to secure their applications, because knowing how it’s broken is the first step toward building it right

You can’t fix what you don’t know is broken—and sometimes, the best way to learn security is to break things… responsibly :)

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