dreamhack.io - PATCH-1

2021. 8. 8. 11:17Web

소개딩 대회 연관해서 횸니형이 추천해서 풀어보고자한다.

전에 비슷한 유형 진짜 본거같은데 어디에서 본지 기억이 안나서...

먼저 제공된 코드는 아래와 같다.

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

위 코드들 중에서 취약한 코드를 찾아서 안전하게 수정하면 된다.

먼저 막돌려봤더니 어떤 취약점들이 발생하는지 친절하게 알려준다.

[FAIL]

 - SLA(7/7): SLA PASS
 - VULN(5)
   - Hard-coded Key
   - SQL Injection
   - Server-Side Template Injection
   - Cross Site Scripting
   - Memo Update IDOR

Hard-coded Key

같은 경우는 초반에 key를 바꿔 주면 된다.

해당 사이트에서 import된 os를 이용한 난수 해결법을 찾았다.

7번째줄

before
> app.secret_key = "Th1s_1s_V3ry_secret_key"
after
> app.secret_key = os.urandom(32)

해당 코드를 수정하니 4개로 줄었다

[FAIL]

 - SLA(7/7): SLA PASS
 - VULN(4)
   - SQL Injection
   - Server-Side Template Injection
   - Cross Site Scripting
   - Memo Update IDOR

XSS

Xss도 친숙하지만 어케 막아야 할지 고민하다가

두개 사이트에서 정보 조합해서 아래와 같이 수정해줬다.

원본
@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/<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> #
      '''
      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")

근데 신기하게 ssti랑 xss둘다 한번에 잡혔다.

[FAIL]

 - SLA(7/7): SLA PASS
 - VULN(2)
   - SQL Injection
   - Memo Update IDOR

Memo Update IDOR

memo update 에 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을 발생시킬만한 것이 idx인데

idx값은 이미 넘어온 상태에서 visual_studio의 userid가 참조되지 않은 객체로 보여서

해당 부분이 존재하는 값인지 확인하는 조건을 추가해줬다.

@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 and (ret['userid'] == userid)) :
    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")

이렇게 하니 sqli만 방어하면 된다고 한다.

[FAIL]

 - SLA(7/7): SLA PASS
 - VULN(1)
   - SQL Injection

SQL Injection —> 삽질하다 문법오류 keep

sqli경우 비교적 친숙한 웹 취약점인데 sql구문들을 주로 사용하는 부분을 살피면 될 것 같다.

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])

42번째 해당 줄이 다른 sqli구문하고 다르게 생겼다.

다른 구문들은

cur.execute("Insert into users values(?, ?);", (userid, hashlib.sha256(password.encode()).hexdigest()))

와 같이 앞에 ?를 넣은 후에 뒤에 괄호로 덮어서 넣어줬는데 해당 구문은 그냥 넣어줬다.

그래서 해당 부분을 아래와 같이 수정하고

if userid and password:
    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])

하단에 취약해 보이는 부분이 하나 더 보여서

ret = query_db("SELECT * FROM memo where idx=" + idx)[0]

해당 코드 또한

ret = query_db("SELECT * FROM memo where idx=?",(idx))[0]

로 변환해보았다.

그랬더니 
ALL PASS

를 받을 수 있었다!!

어디서 틀리는지 모르는데 계속 문법 오류 나서 고생했다..

최종 코드

#!/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 = os.urandom(32)

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("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()
    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> #
      '''
      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,])[0]
  userid = session.get('uid')
  title = request.form.get('title')
  contents = request.form.get('contents')

  if (ret and title and contents and (ret['userid'] == userid)) :
    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")

'Web' 카테고리의 다른 글

dreamhack.io - web-HTTP-CLI  (0) 2021.09.02
dreamhack.io - file-csp-1  (0) 2021.08.24
dreamhack.io - mongoboard  (0) 2021.08.23
Dreamhack - funjs  (0) 2021.08.19
dreamhack.io - web-blind-command  (0) 2021.07.30