๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
[Dreamhack]WebHacking/Wargame&CTF

[Dreamhack] Level2: login-1

by Yun2๐Ÿ‘ 2024. 2. 25.
๋ฐ˜์‘ํ˜•

๐Ÿ›Ž๏ธ Access

python์œผ๋กœ ์ž‘์„ฑ๋œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.
"admin" ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ ํ”Œ๋ž˜๊ทธ๋ฅผ ํš๋“ํ•˜์„ธ์š”.

 

 

๐Ÿ‘พ Exploit Algorithm & Payload

> app.py

๋”๋ณด๊ธฐ
#!/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)

 

 

#1


: ์ฝ”๋“œ๋ฅผ ํ™•์ธํ–ˆ๋‹ค. ('/login' ํŽ˜์ด์ง€)

: POST ์š”์ฒญ์ด๋ฉด ์‚ฌ์šฉ์ž ID์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ ธ์™€์„œ DB์—์„œ ํ•ด๋‹น ์ •๋ณด๋ฅผ ์ฐพ๋Š”๋‹ค.

: ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ DB์— ์žˆ์œผ๋ฉด ์„ธ์…˜ ์ •๋ณด๋ฅผ ์„ค์ •ํ•˜๊ณ  ์ธ๋ฑ์Šค ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅ๋œ๋‹ค.

 

: ์ฝ”๋“œ๋ฅผ ํ™•์ธํ–ˆ๋‹ค. ('/register' ํŽ˜์ด์ง€)

: POST ์š”์ฒญ์ด๋ฉด ์‚ฌ์šฉ์ž ID, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฆ„์„ ๊ฐ€์ ธ์™€์„œ DB์— ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž๊ฐ€ ๋“ฑ๋ก๋œ๋‹ค.

: ๋งŒ์•ฝ ์ด๋ฏธ ๊ฐ™์€ ID๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.

: ์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž๋ฅผ ๋“ฑ๋กํ•œ ํ›„์—๋Š” ๋ฐฑ์—… ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ , DB์— ์ €์žฅ๋œ๋‹ค.

: ๋ฐฑ์—… ์ฝ”๋“œ๊ฐ€ ๋žœ๋คํ•˜๊ฒŒ 0~100 ๋ฏธ๋งŒ ์ •์ˆ˜์ž„์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

 

: ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ–ˆ๋‹ค. ('/forgot_password' ํŽ˜์ด์ง€)

: ์‚ฌ์šฉ์ž ID, ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ, ๋ฐฑ์—… ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๊ฐ€์ ธ์˜จ userid์™€ backupcode๊ฐ€ ์ผ์น˜ํ•˜๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ์ด ๋จ๊ณผ ๋™์‹œ์— ์ƒˆ๋กœ์šด ๋ฐฑ์—… ์ฝ”๋“œ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ์„ ํ™•์ธํ–ˆ๋‹ค.

: ์‹œ๋„ ์ตœ๋Œ€ ํšŸ์ˆ˜๊ฐ€ 5๋กœ ์ง€์ •๋˜์–ด ์žˆ์Œ๋„ ํ™•์ธํ–ˆ๋‹ค.

 

'/user/<int:useridx>' ํŽ˜์ด์ง€

: ์‚ฌ์šฉ์ž์˜ DB ์ธ๋ฑ์Šค, DB์—์„œ ํ•ด๋‹น ์ธ๋ฑ์Šค๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜๋ฉด user.html ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ํ›„ ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค.

 

'/admin' ํŽ˜์ด์ง€

: ํ˜„์žฌ ์„ธ์…˜์ด ์žˆ๊ณ , ์„ธ์…˜์˜ ์‚ฌ์šฉ์ž ๋ ˆ๋ฒจ์ด ๊ด€๋ฆฌ์ž(admin) ์ฆ‰, level 1์— ํ•ด๋‹นํ•˜๋ฉด ํ•ด๋‹น ํŽ˜์ด์ง€์—์„œ FLAG๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

 

 

#2


'/register' ํŽ˜์ด์ง€
userid : password : name
> test1 : test123 : test_name

: ๋‹ค์Œ๊ณผ ๊ฐ™์ด userid, password, name์„ ์ž‘์„ฑํ•˜๊ณ  ์•„์ด๋””๋ฅผ ์ƒ์„ฑํ–ˆ๋‹ค.

: BackupCode(42)๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

: ์ƒ์„ฑํ–ˆ์—ˆ๋˜ userid์™€ password๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํšŒ์› ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

: '/user/17'์— ํ•ด๋‹น ํšŒ์› ์ •๋ณด๊ฐ€ ์žˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

: ์—ฌ๊ธฐ์„œ '/user/[1-17]' ๊นŒ์ง€ ํ™•์ธํ•˜์˜€๋Š”๋ฐ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋„ ํ™•์ธ๋˜๊ธฐ ๋•Œ๋ฌธ์— IDOR ์ทจ์•ฝ์ ์ด ์˜์‹ฌ๋œ๋‹ค.

 

'/user/<int:useridx>' ํŽ˜์ด์ง€(Level์ด 1์ด ๊ด€๋ฆฌ์ž ๊ณ„์ •์ด๋ผ ์ฝ”๋“œ์—์„œ ํ™•์ธํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ž‘์„ฑ)
'/user/1' → UserID: Apple UserName: Apple UserLevel: 1
'/user/5' UserID: Dog UserName: Dog UserLevel: 1
'/user/8' UserID: coconut UserName: coconut UserLevel: 1
'/lemon/9' UserID: lemon UserName: lemon UserLevel: 1
'/user/10' UserID: potato UserName: potato UserLevel: 1
'/user/13' UserID: peach UserName: peach UserLevel: 1
'/user/14' UserID: orange UserName: orange UserLevel: 1

- IDOR(Insecure direct object reference) ์ทจ์•ฝ์  -

: ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๊ฐ์ฒด ์‹๋ณ„์ž(ID, ์ด๋ฆ„, ์ธ๋ฑ์Šค ๋“ฑ)๋ฅผ ๋ณ€๊ฒฝํ•˜์—ฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†๋Š” ์ž์›์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๋ณด์•ˆ ์ทจ์•ฝ์ 

: ์„œ๋ฒ„๊ฐ€ ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ์ œ๋Œ€๋กœ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์•„ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ

+ ์ถ”๊ฐ€๋กœ ๊ด€๋ฆฌ์ž๊ฐ€ ํ˜„์žฌ ์—ฌ๋Ÿฌ๋ช… ์ง€์ • ๋˜์–ด ์žˆ์–ด ์นจํ•ด ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Œ์„ ์œ ์ถ”ํ•  ์ˆ˜ ์žˆ์Œ

 

 

#3


: ๊ด€๋ฆฌ์ž userid๋ฅผ ์•Œ์•˜์œผ๋‹ˆ '/forgot_password'์—์„œ ์ƒˆ๋กœ์šด ํŒจ์Šค์›Œ๋“œ๋ฅผ ๋ณ€๊ฒฝ์‹œ๋„ ํ•˜์˜€๋‹ค.

: ๊ทธ๋Ÿฌ๋‚˜ bacupCode๊ฐ€ ํ‹€๋ ธ๊ธฐ ๋•Œ๋ฌธ์— ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ฐจ๊ฐ๋œ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

: ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ ‘์†์„ ์‹œ๋„ํ•˜๋‹ค๊ฐ€ ๋‹ค๋ฅธ ๋ฐ˜์‘์ด ์กด์žฌํ•˜๋Š” ์ง€์ ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค.

: useid๊ฐ€ potato ์ดํ›„๋กœ backupCode๊ฐ€ ํ‹€๋ฆฌ๋ฉด Internal Server Error(status code:500)๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

: Race condition ๋ฌธ์ œ๊ฐ€ ์˜์‹ฌ๋œ๋‹ค. ๋˜ํ•œ ์ทจ์•ฝํ•œ ํŒจ์Šค์›Œ๋“œ ๋ณต๊ตฌ ์ทจ์•ฝ์ ์ด ์˜์‹ฌ๋œ๋‹ค.

- Race condition - 

: ์—ฌ๋Ÿฌ ํ”„๋กœ์„ธ์Šค๋“ค์ด ๊ณต์œ ๋œ ์ž์›์— ๋™์‹œ์— ์ ‘๊ทผ์„ ์‹œ๋„ํ•  ๋•Œ ์ ‘๊ทผ์˜ ํƒ€์ด๋ฐ์ด๋‚˜ ์ˆœ์„œ ๋“ฑ์ด ๊ฒฐ๊ณผ ๊ฐ’์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ๋น„์ •์ƒ์  ์ƒํƒœ๋ฅผ ๋งํ•œ๋‹ค.

: ์—ฌ๊ธฐ์„œ idx์˜ ํ›„์ˆœ์œ„๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋น„์ •์ƒ์ ์ธ ์ƒํƒœ๊ฐ€ ์ถœํ˜„ํ•จ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

-์ทจ์•ฝํ•œ ํŒจ์Šค์›Œ๋“œ ๋ณต๊ตฌ(PR)-

: ํŒจ์Šค์›Œ๋“œ ์ฐพ๊ธฐ ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค๋กœ ์ธํ•ด ๊ณต๊ฒฉ์ž๊ฐ€ ๋ถˆ๋ฒ•์ ์œผ๋กœ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์˜ ํŒจ์Šค์›Œ๋“œ๋ฅผ ํš๋“, ๋ณ€๊ฒฝํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ์ทจ์•ฝ์ ์ด๋‹ค.

 

๐Ÿ”‘Analysis and results for obtaining the Flag DH{…}


: BurpSuite ๋„๊ตฌ๋ฅผ ์ด์šฉํ•˜์—ฌ backupcode๋ฅผ 0~100๊นŒ์ง€ Brute Force๋ฅผ ์‹œ๋„ํ–ˆ๋”๋‹ˆ ์ƒํƒœ์ฝ”๋“œ 200์ด ๋ฐœ์ƒํ•˜๋Š” ๊ตฌ๊ฐ„์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ณ , FLAG๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฐ˜์‘ํ˜•

'[Dreamhack]WebHacking > Wargame&CTF' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Dreamhack] Level1: random-test  (2) 2024.03.04
[Dreamhack] Level1: [wargame.kr] strcmp  (1) 2024.02.25
[Dreamhack] CTF Season 5 Round #4 - BypassIF  (1) 2024.02.25
[Dreamhack] Level2: baby-sqlite  (0) 2024.02.23
[Dreamhack] Level4: KeyCat  (0) 2024.02.23