워게임

[Dreamhack] - PATCH-1 (writeup)

HackHiJack 2022. 3. 2. 21:14
728x90
반응형

이번에는 dreamhack의 PATCH-1 문제를 풀어볼 것이다. (난이도 : 3)

 

이 문제는 진짜 화이트 해킹의 대표적인 예라고 할 수 있는 문제이다.

여러 가지 취약점이 있는 코드를 분석하고 패치하여 취약점 공격을 막으면 된다.\

 

 뭐 딱히 지금은 중요한 게 없다.

 

먼저 이 기본적으로 어떤 제한이 있는지, 무엇을 패치해야 하는지 등의 배경 정보를 확인해보자.

기본 코드를 submit하거나 /usage에서 정보를 얻었다.

 


 

 - VULN(5)
   - Hard-coded Key
   - SQL Injection
   - Server-Side Template Injection
   - Cross Site Scripting
   - Memo Update IDOR

- 주어진 코드에 존재하는 취약점을 패치해보세요. 

- 코드의 기능이 정상 동작하는지 확인하는 SLA도 동작합니다. 

- 코드 수정 후 Submit 하시면 테스트가 시작됩니다. 

- "✔ 수정 가능"인 코드만 수정 가능합니다. 

- 로컬 환경에서 테스트 후 서버의 검증 과정을 받는 것을 추천드립니다. 

- 30초에 한번 제출할 수 있습니다. 

 

- 주의 사항 

- Python Syntax에 주의하세요. 

- socket, execve, ... 등 외부 서버 접속 및 OS 커맨드 사용 불가. 

- os.environ 등에 저장된 설정 변수 변경 불가. 

- app.run() 임의로 추가 불가.

 

-코드

#!/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")

 

우리는 5가지의 취약점을 패치해야한다.

 

나 같은 경우는 ssti부터 패치했다.(드림핵 질문에 ssti 관련된 것이 있어서.. 흠흠)

일단은 어디서 ssti가 터지는 파악을 해야 한다.

python flask ssti의 경우 render_template 같은 부분에 ssti가 일어난다.

그런 부분은 딱 하나, memoView() 함수이다.

@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")

 

정확히는 다음에서 ssti가 터진다.

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)

일단은 드림핵 질문에 힌트가 있을 줄 알았는데 딱히 도움이 안 됐다.

그래서 구글링을 오지게 시도한 결과 아래 reference에서 해결책을 찾았다.

Reference : https://sl1nki.page/blog/2021/01/24/ssti

 

ssti를 패치하는 법은 format()이 아닌 dictionary형식으로 변수를 만들어서 다음과 같이 만들면 됐다.

if mode == 'html':
      template = ''' Written by {{userid}}<h3>{{title}}</h3>
      <pre>{{contents}}</pre>
      '''
      context = {
        'userid': userid,
        'title' : title,
        'contents' : contents
      }
      return render_template_string(template, **context)

그러면 ssti랑 xss가 동시에 패치됐었다.

 

그리고 다음 마음에 걸린 취약점은 hard-coded key였다.

정확히는 hard-coded key의 개념을 몰라서 아마도 app.secret_key라고 생각했다.

app = Flask(__name__)
app.secret_key = "Th1s_1s_V3ry_secret_key"

평소 드림핵 문제 코드에는 저런 식으로 key가 있었던 적이 없는 것 같아서 아무 문제 코드를 다시 봤는데 os.urandom(32)라고 되어있었다. 그래서 똑같이 문자열을 없애고 그렇게 코드를 적었다.

app = Flask(__name__)
app.secret_key = os.urandom(32)

이렇게 해서 submit을 하니 통과되었다.

이제 sql injection을 패치했다.

다른 건 안 그런데 유독 아래의 sql문만 조금 다르게 넣어서 취약점이 발생한다고 판단했다.

@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")

sql injection은 python sqlite3이라는 모듈에서 발생해서 자료가 많았다.

그래서 아래처럼 바꾸어 주면 된다.

@app.route('/api/login', methods=['POST'])
def login():
  userid = request.form.get('userid', '')
  password = request.form.get('password', '')
  if userid and password:
      password = hashlib.sha256(password.encode()).hexdigest() 
      ret = query_db("SELECT * FROM users where userid = ? and password = ?" , (userid, password), one=True)    
  if ret:
      session['uid'] = ret[0]
      return jsonify(result="success", userid=ret[0])
  return jsonify(result="fail")

 

(나 같은 경우는? 를 ''안에 넣어서 삽질을 했었다...ㅠ)

 

마지막으로 IDOR이다.

문제 설명처럼 memoUpdate()만 수정하면 된다.

이건 생각이 필요한데, idx를 받으면 idx와 같은 row인 userid와 session에서 가져온 userid랑 같은지 확인해야 한다.

이걸 반영해서 아래처럼 코딩할 수 있다.

@app.route('/api/memo/<int:idx>', methods=['PUT'])
def memoUpdate(idx):
  if not session.get('uid'):
    return jsonify(result="no login")

  userid = session.get('uid')
  ret = query_db('SELECT * FROM memo where idx=?', [idx,])[0]
  title = request.form.get('title')
  contents = request.form.get('contents')
  dbuserid = ret['userid']
  if dbuserid == userid :
    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")

이쯤에서 광고 한번 클릭해주세용~~^^

반응형

 

이제 이 패치한 코드를 넣고 결과를 확인하면 아래처럼 플래그를 획득할 수 있다.

 

728x90
반응형