๐๏ธ 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 |