#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for, session, g
import sqlite3
import hashlib
import os
import time, random
app = Flask(__name__)
app.secret_key = os.urandom(32)
DATABASE = "database.db"
userLevel = {
0 : 'guest',
1 : 'admin'
}
MAXRESETCOUNT = 5
try:
FLAG = open('./flag.txt', 'r').read()
except:
FLAG = '[**FLAG**]'
def makeBackupcode():
return random.randrange(100)
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
userid = request.form.get("userid")
password = request.form.get("password")
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE id = ? and pw = ?', (userid, hashlib.sha256(password.encode()).hexdigest() )).fetchone()
if user:
session['idx'] = user['idx']
session['userid'] = user['id']
session['name'] = user['name']
session['level'] = userLevel[user['level']]
return redirect(url_for('index'))
return "<script>alert('Wrong id/pw');history.back(-1);</script>";
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('index'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
return render_template('register.html')
else:
userid = request.form.get("userid")
password = request.form.get("password")
name = request.form.get("name")
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE id = ?', (userid,)).fetchone()
if user:
return "<script>alert('Already Exists userid.');history.back(-1);</script>";
backupCode = makeBackupcode()
sql = "INSERT INTO user(id, pw, name, level, backupCode) VALUES (?, ?, ?, ?, ?)"
cur.execute(sql, (userid, hashlib.sha256(password.encode()).hexdigest(), name, 0, backupCode))
conn.commit()
return render_template("index.html", msg=f"<b>Register Success.</b><br/>Your BackupCode : {backupCode}")
@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
if request.method == 'GET':
return render_template('forgot.html')
else:
userid = request.form.get("userid")
newpassword = request.form.get("newpassword")
backupCode = request.form.get("backupCode", type=int)
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE id = ?', (userid,)).fetchone()
if user:
# security for brute force Attack.
time.sleep(1)
if user['resetCount'] == MAXRESETCOUNT:
return "<script>alert('reset Count Exceed.');history.back(-1);</script>"
if user['backupCode'] == backupCode:
newbackupCode = makeBackupcode()
updateSQL = "UPDATE user set pw = ?, backupCode = ?, resetCount = 0 where idx = ?"
cur.execute(updateSQL, (hashlib.sha256(newpassword.encode()).hexdigest(), newbackupCode, str(user['idx'])))
msg = f"<b>Password Change Success.</b><br/>New BackupCode : {newbackupCode}"
else:
updateSQL = "UPDATE user set resetCount = resetCount+1 where idx = ?"
cur.execute(updateSQL, (str(user['idx'])))
msg = f"Wrong BackupCode !<br/><b>Left Count : </b> {(MAXRESETCOUNT-1)-user['resetCount']}"
conn.commit()
return render_template("index.html", msg=msg)
return "<script>alert('User Not Found.');history.back(-1);</script>";
@app.route('/user/<int:useridx>')
def users(useridx):
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE idx = ?;', [str(useridx)]).fetchone()
if user:
return render_template('user.html', user=user)
return "<script>alert('User Not Found.');history.back(-1);</script>";
@app.route('/admin')
def admin():
if session and (session['level'] == userLevel[1]):
return FLAG
return "Only Admin !"
app.run(host='0.0.0.0', port=8000)
코드를 먼저 해석하겠다.
userLevel = {
0 : 'guest',
1 : 'admin'
}
userLevel이 있으며 guest의 경우 0, admin는 1이다.
def makeBackupcode():
return random.randrange(100)
백업 코드가 있고, 1부터 100까지 랜덤 숫자이다.
if user:
session['idx'] = user['idx']
session['userid'] = user['id']
session['name'] = user['name']
session['level'] = userLevel[user['level']]
return redirect(url_for('index'))
컬럼은 idx, userid, name, level이 있다.
@app.route('/admin')
def admin():
if session and (session['level'] == userLevel[1]):
return FLAG
return "Only Admin !"
app.run(host='0.0.0.0', port=8000)
admin페이지에서 level이 1일 때 flag를 얻게 된다.
@app.route('/user/<int:useridx>')
def users(useridx):
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE idx = ?;', [str(useridx)]).fetchone()
if user:
return render_template('user.html', user=user)
return "<script>alert('User Not Found.');history.back(-1);</script>";
파라미터에 useridx를 넣는 포인트가 있다.
1부터 차례대로 넣어봤다.
..... 쭉 계정의 ID, NAME, LEVEL이 나온다.
아까 FALG를 얻을려면 userlevel이 1이어야 하기 때문에 이를 이용하여 구하면 되지 않을까 생각이 들었다.
패스워드를 모르기 때문에 forgot Loing 페이지로 가주었다.
id와 새로운 pw, 백업 코드를 넣으면 새로운 pw로 바꿀 수 있다.
즉, id와 백업코드만 알 수 있으면 로그인을 할 수 있다.
현재 Apple이라는 id는 알고 있기 때문에 백업코드를 알아야내 한다.
한번 임의이 백업코드 1을 넣고 요청을 보내면
현재 Left 카운터가 4개가 남았다고 한다.
코드를 보자.
@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
if request.method == 'GET':
return render_template('forgot.html')
else:
userid = request.form.get("userid")
newpassword = request.form.get("newpassword")
backupCode = request.form.get("backupCode", type=int)
conn = get_db()
cur = conn.cursor()
user = cur.execute('SELECT * FROM user WHERE id = ?', (userid,)).fetchone()
if user:
# security for brute force Attack.
time.sleep(1)
if user['resetCount'] == MAXRESETCOUNT:
return "<script>alert('reset Count Exceed.');history.back(-1);</script>"
if user['backupCode'] == backupCode:
newbackupCode = makeBackupcode()
updateSQL = "UPDATE user set pw = ?, backupCode = ?, resetCount = 0 where idx = ?"
cur.execute(updateSQL, (hashlib.sha256(newpassword.encode()).hexdigest(), newbackupCode, str(user['idx'])))
msg = f"<b>Password Change Success.</b><br/>New BackupCode : {newbackupCode}"
else:
updateSQL = "UPDATE user set resetCount = resetCount+1 where idx = ?"
cur.execute(updateSQL, (str(user['idx'])))
msg = f"Wrong BackupCode !<br/><b>Left Count : </b> {(MAXRESETCOUNT-1)-user['resetCount']}"
conn.commit()
return render_template("index.html", msg=msg)
return "<script>alert('User Not Found.');history.back(-1);</script>";
user가 존재하면 time.sleep(1)함수로 1초 지연한다.
resetcount가 MAXRESETCOUNT, 즉 5이면 리다이렉트한다.
백업코드가 맞으면 1부터 100까지 새로운 백업코드를 만들고 패스워드를 변경한다.
백업코드가 일치하지 않으면 RESETCOUNT를 1증가시킨다.
현재 취약점이 있는 곳은 time.sleep(1)이다.
시간이 1초 지연되는 동안 백업코드를 동시에 요청하면 하나씩 증가되지 않고 5를 넘어버린다.
즉, 동시요청을 통해 프로세스가 서로 요청하게 하여 관리자 권한을 얻는다. 레이스 컨디션이라고도 한다.
공격할 backupcode를 잡아주고
백업코드의 랜덤 값 범위인 1부터 100까지 설정하여 공격을 시작한다.
공격이 끝나면 이제 프로세스 들은 서로 요청을 계속 하여 동시요청상태에 빠져있다.
아무 백업코드를 넣으면 Wrong BackupCode! 문구가 나오면서 백업코드가 맞지 않다고 뜬다.
하지만 Left Count는 음수가 나오면서 공격이 성공한 것을 알 수 있다. 다시 브프공격을 하고
로그인을 해보면
Apple로 로그인이 된다.
이제 admin을 눌러서 admin페이지로 들어가면
플래그를 획득할 수 있다.