
이번 dreamhack 문제는 코드를 패치하는 문제다.
지금까지 취약점 공격만 해봤지 취약점 패치는 해본 적이 없어서 새로웠다.
#!/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.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:
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/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()
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')
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")
주어진 코드의 전체 내용이다.
여기서 취약한 부분을 찾아서 패치하면 된다.
app.secret_key = "Th1s_1s_V3ry_secret_key"
코드를 보자마자 가장 먼저 발견한 취약한 부분은 secret_key 값이 하드코딩 돼있는 것이다.
app.secret_key = os.urandom(32)
이런식으로 바꿔서 패치를 했다.
@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")
그 다음은 /api/login 라우트 부분이다.
이 부분에는 SQLI 취약점이 있다.
코드를 보면 사용자의 입력 값인 userid, password가 SQL Query에 그대로 들어가는 것을 확인할 수 있다.
이 취약점은 prepared statement를 사용하면 SQLI을 막을 수 있다.
ret = query_db("SELECT * FROM users WHERE userid=? AND password=?",
(userid, hashlib.sha256(password.encode()).hexdigest()), one=True)
이런식으로 패치를 하면 SQLI를 막을 수 있다.
원리를 간단히 말하자면 SQL 명령어의 틀과 그 안에 채워질 데이터를 프로토콜 수준에서부터 분리하여 데이터베이스에 전달하는 것이다.
DB 시스템은 이를 위해 Prepare, Execute라는 두 단계의 메커니즘을 사용한다.
Prepare 단계에서는 DB Server에 SQL 템플릿을 먼저 보낸다.
그리고 Execute 단계에서 실제 데이터 값을 별도의 패킷으로 서버에 보낸다.
Execute 단계에서 보내지는 데이터(예를 들어 ' or 1=1 #)는 SQL 명령의 일부가 아니라 순수한 데이터 값으로 전송되는 것이다.
따라서 서버는 데이터(' or 1=1 #)를 컬럼의 값이 아니라 ' or 1=1 #라는 하나의 문자열로 인식하게 된다.
(혹시라도 틀린 설명이라면 아무나 알려주쉐이)
@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")
/api/memo/ 라우트에도 SQLI 취약점이 있었다. 이것도 같은 방식으로 수정하여
ret = query_db("SELECT * FROM memo where idx=?", (idx))[0]
위처럼 수정해준다.
일단 이렇게 패치하고 Submit을 해봤는데 아래와 같이 결과가 나왔다.

SSTI, XSS, IDOR 취약점이 남아있고 친절히 알려줘서 날먹할 수 있게 됐다.
그래서 SSTI, XSS, IDOR 중심으로 취약점이 있는 부분을 찾아봤다.
그래서 일단 IDOR 부분을 찾아봤다.
@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")
IDOR 취약점은 /api/memo/<int:idx> 라우트에 있었다.
로그인만 되어 있는지 검증만해서 ret['userid']와 session.get('uid')가 같은지 비교하는 조건문을 추가해줬다.
if userid != ret['userid']:
return jsonify(result="stop")

이런식으로 추가해줘서 IDOR도 패치를 했다.
그다음으로 SSTI가 있는 곳을 찾았다.
@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")
SQLI 취약점이 있던 /api/memo/ 라우트에 SSTI 취약점도 있었다.
momoView 함수에 return render_template_string(template)가 취약한 줄 알고 render_template_string 대신 render_template를 썼는데.. HTML Mode Error가 떴다.
그래서 조금 더 알아보니 render_template 함수는 인자를 파일 이름으로 받기 때문에 Error가 뜬 것이었다.
그래서 다른 방법을 찾아봤다.
겨우겨우 찾은 자료에서 컨텍스트를 사용해서 렌더링하면 된다고 한다.
context = {
'name': request.values.get('name')
}
return render_template_string('Hello {{ name }}!', **context)
이런식으로 사용하면 된다고 하는데 원리는 아직 모르겠다.
아는 사람 있으면 알려주세요
@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>
'''
context = {
'title': title,
'userid': userid,
'contents': contents
}
return render_template_string(template, **context)
else:
return jsonify(result="success",
userid=userid,
title=title,
contents=contents)
return jsonify(result="error")
그래서 위와 같이 패치를 진행하고 Submit을 해봤다.

그 결과 모든 게 통과되고 Flag 값을 얻을 수 있었다.
그런데 XSS 패치는 따로 안 했는데 이게 왜 된 건지 이것도 나중에 따로 알아봐야할 것 같다.
'Dreamhack > Web' 카테고리의 다른 글
| [Dreamhack] BISC board (0) | 2025.11.25 |
|---|---|
| [Dreamhack] phpmyredis (0) | 2025.11.24 |
| [Dreamhack] chocoshop (0) | 2025.08.13 |
| [Dreamhack] Safe Input (0) | 2025.04.17 |