본문 바로가기

Dreamhack/Web

[Dreamhack] PATCH-1

반응형

문제 설명

이번 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")

 

중간 결과 확인 2

이런식으로 추가해줘서 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 값 추출 성공

 

그 결과 모든 게 통과되고 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