This little project is about API developed purposely with three vulnerabilities from OWASP TOP 10 list year 2017. I wrote a back-end script in Python 3.10 on Ubuntu server version 22.04. The ARU Dev Solutions LLC is a fictional company requires to test them and fix them.
The vulnerabilities i went with are:
- A2 Broken Authentication
- A3 Sensitive Data Exposure
- A6 Security Misconfiguration
The completely fixed code is below.
A2 Broken Authentication
OWASP also states that the most common causes of broken authentication include weak password policies, session fixation attacks, and insufficient protection of session IDs. Attackers can also exploit common vulnerabilities in authentication protocols such as credential stuffing, brute-force attacks, and password spraying as stated.
There is also a violation that the system stores passwords in plaintext form which allows attackers get access to their accounts which falls into Broken Authentication OWASP vulnerability. This is also partially falls into A3 Sensitive Data Exposure.
Image illustrates attack using “Dirp” tool on Kali Linux, author used to web crawl pages and directories on target web server. Figure displays target’s IP address and wordlist of more than 200 names in fasttrack.txt file which is publicly available and perform brute-force on all the names in the list.
Something about dirb tool here https://www.kali.org/tools/dirb/
Admin function renders admin page which should be restricted for chosen user or users only. This page is restricted to users theirs name is in login the session. @login_required Python decorator from Flask library at line 77 ensures it is that way.
When the admin function is properly configured with specified Python decorator, the browser throws an unauthorized access error as illustrated below
And this is how it looks like when username is in the session. User can access admin page and can add users and refresh database.
A2 Sensitive Data Exposure
Second vulnerability author found, and third one on the OWASP TOP 10 2017 list is that web server does not use HTTPS. Server is not configured with SSL certificate or TLS 1.2 or 1.3 which encrypts web traffic between client’s browser and web server.
Figure illustrates initiation of MITM attack between API server and the victim sitting on the same LAN. Attacker sends the poisoned ARP message to ARP tables of both of them where it changes the original MAC address to the attacker’s one. After that all the traffic on the LAN goes the attacker’s computer.
Figure illustrates how encryption that scrambles data looks like. This is because of implementation SSL certificate that contains security features that server uses for ensuring authentication and encryption.
A6 Security Misconfiguration
Third vulnerability from OWASP 2017 list there is A6:2017 Security Misconfiguration. OWASP states application is vulnerable if unnecessary features are enabled or installed (e.g., unnecessary ports, services, pages, accounts, privileges) OWASP also states specifically that this includes databases.
Statement from OWASP corresponds to what author found. In this case database API uses does not need to be opened to public. API uses it only locally and attacker scanned the ports on the server and port 3306 used by MySQL is accessible over the internet. Password for root account is default or weak it is easily breakable by dictionary attack over internet. Attackers can use custom scripts or there are many tools publicly available for such an attack.
Security Misconfiguration can encompass anything and everything related to configurations. This statement matches with the vulnerability found because it is not API python code related int is misconfiguration in MariaDB settings.
Figure illustrates the change that reconfigures the security access to server for local interfaces only. That means the Python application can communicate with the DB but it will not be open to the internet.
In case client for some reason has to keep the database opened to public, it is generally recommended to dedicate one user except root to be able log in to database remotely with only those privileges in case that user gets compromised. Root should stay be able to connect locally only. First developer has to execute MySQL command that creates user in the database with rights to connect from all hosts and specify the database. Author recommends restricting the rights to only the necessary ones.
The program is using threading. Thread makes the Apache2 to run on two ports (80, 443) simultaneously. There is also a self-created SSL certificate with public key.
app.config['SECRET_KEY'] = 'secret_key'
This line of code sets the secret key for a Flask application. The secret key is used to keep the client-side sessions secure. It is a random key used to encrypt cookies and save them to the browser. It is important to keep this value secure, or attackers could use it to generate their own signed values. Otherwise the program won’t properly start.
engine variable contains a connection string to create an object later used for queries to Mariadb database. There is a number of databse that can be used for such a string like here. Talisman class adds a security features while running the “app” through itself, which activates the HTTPS with SSL certificate while starting the API.
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-neve-link-hover-color-color">########################## # ARU Dev Solutions LLC. ########################## from flask import Flask, render_template, redirect, url_for, request, session from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, PasswordField from wtforms.validators import DataRequired from sqlalchemy import create_engine, Column, String, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from talisman import Talisman import threading, hashlib from flask_login import LoginManager, UserMixin, login_required, login_user app = Flask(__name__) engine = create_engine("mariadb+pymysql://root:email@example.com:3306/arudevsol") base = declarative_base() app.config['SECRET_KEY'] = 'secret_key' Talisman(app) Session = sessionmaker(bind=engine) session = Session() login_obj = LoginManager(app) login_obj.init_app(app) class Users(base): __tablename__ = 'users' id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String(20), nullable=False) password = Column(String(100), nullable=False) def __init__(self, id, username, password): self.id = id self.username = username self.password = password class RegisterForm(FlaskForm): new_username = StringField('New username', validators=[DataRequired(20)]) new_password = PasswordField('New passord', validators=[DataRequired(100)]) submit = SubmitField('Submit') class Admin(UserMixin): def __init__(self, user_id): self.id = user_id class LoginForm(FlaskForm): username = StringField('Username:') password = PasswordField('Password:') submit = SubmitField('Login') @login_obj.user_loader def load_user(username): return Admin(username) @app.route("/", methods=['GET', 'POST']) def index(): form = LoginForm() if request.method == 'POST' and form.validate_on_submit(): username = form.username.data password = form.password.data record = session.query(Users).filter_by(username=username).first() if username is not None and (record.password == hashlib.sha256(password.encode()).hexdigest()): admin = Admin(username) login_user(admin) return redirect(url_for('admin')) return render_template('index.html', form=form) @app.route("/admin", methods=['POST', 'GET']) @login_required def admin(): form = RegisterForm() listofusers = session.query(Users).all() if request.method == 'POST' and form.validate_on_submit(): session.add(Users(id=None, username=form.new_username.data, password=form.new_password.data)) session.commit() return render_template("admin.html", username=None, password=None, form=form, result='User created') return render_template("admin.html", form=form, result="", listofusers=listofusers) def http(): app.run(host="192.168.10.20", port=80) def https(): app.run(host="192.168.10.20", port=443, ssl_context=('cert.pem', 'key.pem')) if __name__ == '__main__': threading.Thread(target=http).start() threading.Thread(target=https).start()</mark>