Wargame/Web

[Dreamhack] login-1

월루이 2023. 6. 20. 20:00

1. 문제

admin 권한을 가진 사용자로 로그인하여 FLAG를 획득하는 문제

 

 

힌트

더보기
  • 브루트 포스 공격
  • 컨디션 레이스 공격

 

 

코드

#!/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)

 

 

 

 

풀이

더보기

 

아래 코드를 보면, /register 페이지에서 회원가입을 하면 무조건 guest로 가입되는 모습을 볼 수 있다.

이때, PreparedStatement 구현되어 SQL Injection 공격 시도는 힘들 것으로 보인다...

 

@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}")

 

 

 

 

/user/idx 경로에 들어가면, 각 해당하는 사용자의 정보를 볼 수 있다.

 

 

 

그러므로 Apple 사용자로 로그인하게되면 FLAG를 얻을 수 있다는 점을 알 수 있다.

 

하지만, Apple의 패스워드를 모르기때문에 /forgot_password 페이지에서 비밀번호를 변경해줘야 한다.

 

 

 

 

forgot_password 페이지에 접속 시, backupCode 입력값이 존재한다.

 

 

backupCode는 회원가입 시, 생성되는 코드로 아래와 같이 0~100까지의 랜덤 값을 가진다.

즉, Apple 계정의 backupCode는 모른다.

 

def makeBackupcode():
    return random.randrange(100)

 

 

 

 

forgat_password 코드를 보면, backupCode가 틀릴시, resetCount가 증가하며 최대 5번까지 가능하다고 한다.

브루트 포스 공격 방지를 위해 0~100까지를 연속으로 넣지는 못하게 한다는 얘기다.

 

하지만, time.sleep(1) 이 부분에서 1초간 멈추는 것을 알 수 있다.

이때, 이 1초동안 멈추는 사이 1부터 100까지 대입해본다면…? 

그러니, 레이스컨디션 공격이 가능하므로 브루트 포스 공격이 추가로 가능해진다.

 

 

Burp > Intruder

 

 

참고로 코드에서 == 부분 때문에 현재 쓰고 있는 버전이 프로가 아니라, 1초를 넘었음에도 불구하고 성공한 것을 알 수 있다.

 

MAXRESETCOUNT=5지만, resetCount가 6 이상으로 넘었음에도 거부하지 않으므로 100까지 할 수 있다 !!

=> 이걸 보완해주기위해 계정 잠금 등의 코드를 추가해주면 좋다 ~

 

        if user:
            # security for brute force Attack.
            time.sleep(1)
	
            if user['resetCount'] == MAXRESETCOUNT:
                return "<script>alert('reset Count Exceed.');history.back(-1);</script>"