이번에는 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")
이쯤에서 광고 한번 클릭해주세용~~^^
↓
↓
이제 이 패치한 코드를 넣고 결과를 확인하면 아래처럼 플래그를 획득할 수 있다.
'워게임' 카테고리의 다른 글
[Dreamhack] - Windows Search(WRITEUP) (1) | 2023.01.30 |
---|---|
[Webhacking.kr] - old 58 (writeup) (0) | 2022.03.03 |
[Dreamhack](writeup) - simple_sqli (0) | 2022.03.01 |
DigitalForensic with CTF(ctfd) - 제 친구의 개가 바다에서…(writeup) (0) | 2021.09.04 |
HackCTF - So easy? (forensics writeup) (0) | 2021.08.24 |