Skip to content

No FA

Files Provided

app.py:

from flask import Flask, render_template, request, flash, redirect, url_for, session, send_file
from dotenv import load_dotenv

import db
import os
import hashlib
import random
import time

load_dotenv()

app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY')

@app.before_request
def initialize():
    app.before_request_funcs[None].remove(initialize)
    db.init_db()

@app.route("/")
def home():
    if 'username' not in session or session['logged'] == 'false':
        flash('Please login to access this page', 'red')
        return redirect(url_for('login'))

    flag = "No flag for you!!"
    if session.get('username') == 'admin':
        flag = os.getenv('FLAG')

    return render_template("index.html", flag=flag)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        user = db.get_user_by_username(username)
        if user and hashlib.sha256(password.encode()).hexdigest() == user['password']:
            if user['two_fa']:
                # Generate OTP
                otp = str(random.randint(1000, 9999))
                session['otp_secret'] = otp
                session['otp_timestamp'] = time.time()
                session['username'] = username
                session['logged'] = 'false'
                # send OTP to mail ---
                return redirect(url_for('two_fa'))
            else:
                session['username'] = username
                session['logged'] = 'true'
                flash('Login successful!', 'green')
                return redirect(url_for('home'))
        else:
            flash('Invalid username or password', 'red')
    return render_template('login.html')

@app.route('/two_fa', methods=['GET', 'POST'])
def two_fa():
    if request.method == 'POST':
        otp = request.form['otp']
        stored_otp = session['otp_secret']
        timestamp = session.get('otp_timestamp')
        if stored_otp and otp == stored_otp and (time.time() - timestamp) < 120:
            session['logged'] = 'true'
            flash('Login successful!', 'green')
            return redirect(url_for('home'))
        else:
            flash('Invalid OTP or OTP expired', 'red')
            return render_template('2fa.html')
    else:
        return render_template('2fa.html')

@app.route('/logout')
def logout():
    flash('You have been logged out.', 'green')
    session.pop('username', None)
    session['logged'] = 'false'
    return redirect(url_for('login'))

users.db:

.
.
adminiamadmin@nfs.comc20fa16907343eef642d10f0bdb81bf629e6aaf6c906f26eabda079ca9e5ab67
.
.

Approach

Authentication:

if user and hashlib.sha256(password.encode()).hexdigest() == user['password']:
    if user['two_fa']:
        # Generate OTP
        otp = str(random.randint(1000, 9999))
        session['otp_secret'] = otp
        session['otp_timestamp'] = time.time()
        session['username'] = username
        session['logged'] = 'false'
        # send OTP to mail ---
        return redirect(url_for('two_fa'))

There's no salt used in hashing, trying with JohnTheRipper and rockyou.txt:

./run/john --format=raw-sha256 --wordlist=./run/rockyou.txt ./hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA256 [SHA256 256/256 AVX2 8x])
Warning: poor OpenMP scalability for this hash type, consider --fork=12
Will run 12 OpenMP threads
Note: Passwords longer than 18 [worst case UTF-8] to 55 [ASCII] rejected
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
apple@123        (?)     
1g 0:00:00:00 DONE (2026-03-15 22:33) 4.545g/s 9830Kp/s 9830Kc/s 9830KC/s bragaparasempre..Angelg7
Use the "--show --format=Raw-SHA256" options to display all of the cracked passwords reliably
Session completed. 

Trying the password "apple@123" works!

OTP is stored in the session cookie, which can be decoded here

Flag got!