문제
https://dreamhack.io/wargame/challenges/24/
문제코드
#!/usr/bin/python3
from flask import Flask, request, render_template, g
import sqlite3
import os
import binascii
app = Flask(__name__)
app.secret_key = os.urandom(32)
try:
FLAG = open('./flag.txt', 'r').read()
except:
FLAG = '[**FLAG**]'
DATABASE = "database.db"
if os.path.exists(DATABASE) == False:
db = sqlite3.connect(DATABASE)
db.execute('create table users(userid char(100), userpassword char(100));')
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
db.commit()
db.close()
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
def query_db(query, one=True):
cur = get_db().execute(query)
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 render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
userid = request.form.get('userid')
userpassword = request.form.get('userpassword')
res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
if res:
userid = res[0]
if userid == 'admin':
return f'hello {userid} flag is {FLAG}'
return f'<script>alert("hello {userid}");history.go(-1);</script>'
return '<script>alert("wrong");history.go(-1);</script>'
app.run(host='0.0.0.0', port=8000)
코드 분석
우선 url부분부터 살펴보자
@app.route('/')
def index():
return render_template('index.html')
'/' 로 가는 이 부분은 그냥 index.html 템플릿으로 가는 부분이고
핵심은 아래 /login 부분인 것 같다.
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
userid = request.form.get('userid')
userpassword = request.form.get('userpassword')
res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
if res:
userid = res[0]
if userid == 'admin':
return f'hello {userid} flag is {FLAG}'
return f'<script>alert("hello {userid}");history.go(-1);</script>'
return '<script>alert("wrong");history.go(-1);</script>'
app.run(host='0.0.0.0', port=8000)
GET 요청이면 login.html 템플릿으로 가져오는 것이다.
POST 요청일시..
폼에서부터 userid, userpassword 를 입력받고(아이디와 비밀번호), res 변수에 userid와 userpassword를 넣고 존재하는지 확인하여, admin이면 flag 출력하는 코드가 작성되어 있다
query_db는 어떻게 쓰이는 건지 한 번 찾아보면
def query_db(query, args=(), one=False):
cur = g.db.execute(query, args)
rv = [dict((cur.description[idx][0], value)
for idx, value in enumerate(row)) for row in cur.fetchall()]
return (rv[0] if rv else None) if one else rv
대충 쿼리 구문 작성하는 것 같다. 실제 코드에서도 저렇게 나와있고..
for user in query_db('select * from users'):
print user['username'], 'has the id', user['user_id']
이것들은
https://flask-docs-kr.readthedocs.io/ko/stable/patterns/sqlite3.html
여기서 확인하였다.
아무튼 login 까지 알아봤고 다음으로는 위쪽코드를 살펴보면
DATABASE = "database.db"
if os.path.exists(DATABASE) == False:
db = sqlite3.connect(DATABASE)
db.execute('create table users(userid char(100), userpassword char(100));')
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
db.commit()
db.close()
DATABASE 는 "database.db"이고
저 DATABASE에 파일이 존재하는지 확인하고, 데잍어가 존재하지 않는다면, sqlite3와 연결한 변수를 db로 두고 생성해주는 것이다. 기존에 사용할 때는
users {
'guest' : 'guest'
'admin' : FLAG
}
이런식으로 사용되었었는데, 이걸 데이터베이스화 시킨 것 같다.
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
def query_db(query, one=True):
cur = get_db().execute(query)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
query_db 부분에서는 아까
def query_db(query, args=(), one=False):
cur = g.db.execute(query, args)
rv = [dict((cur.description[idx][0], value)
for idx, value in enumerate(row)) for row in cur.fetchall()]
return (rv[0] if rv else None) if one else rv
여기서 args=() 부분이 빠진 걸로 오버라이딩 한 것 같다.
get_db로 query문을 이용하여 데이터를 가져오는 것이 query_db 함수의 역할이다.
마지막으로 처음보는 형식인 코드는
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
이 부분인데,
@app.teardown_appcontext 는 app에 연결된 특정 컨텍스트가 해제될때 실행되는 함수 등록으로써, 컨텍스트가 종료될 때 등록된 함수가 호출되는 것이다.
무엇이 실행되는 함수인가는 close_connection으로, 데이터베이스를 닫는 역할을 한다.
데이터베이스의 연결이 존재하면 닫는 역할이다. 이 문제를 푸는데 필요없는 코드인 것 같다.
풀이 전략
sql의 주석처리는 -- 아니면 #이다.
핵심 부분은
userid = request.form.get('userid')
userpassword = request.form.get('userpassword')
res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
이 부분이다. 폼에서부터 id와 비밀번호를 받는데, admin으로 접속할 때만 flag 가 나오므로, admin 아이디로 로그인해야한다.
그래서 아이디에
admin"--
를 입력하고 비밀번호에 아무거나 입력하고 로그인 하면 flag 가 출력이 되지 않을까 싶다
실행
메인페이지에서 Login 버튼을 눌러서 로그인창으로 넘어가고
아이디에는 admin" --, 비밀번호는 아무거나 입력을 해서 Login 을 했더니
플래그가 정상적으로 나왔다.
플래그는 DH{c1126c8d35d8deaa39c5dd6fc8855ed0} 이다.
에필로그
처음으로 풀이 하나도 안 보고 내 스스로 생각해서 풀은 문제이다.좀 뿌듯하지만 그만큼 난이도가 쉽다고 생각한다.SQL과 관련해서 공부할려고 MySQL 책도 샀는데, 한 번 공부해야겠다.
+ 추가 내용
동적으로 쿼리문 생성한 뒤 query_db 함수에서 SQLite 질의 방식 : RawQuery
이 RawQuery 를 생성할 떄, 이용자의 입력값이 쿼리문에 포함되면 SQL Injection 취약점에 노출
'보안 스터디 > 웹 해킹' 카테고리의 다른 글
[드림핵/웹해킹] ServerSide: NoSQL Injection (3) | 2024.01.11 |
---|---|
[드림핵/웹해킹] Background: Non-Relational DBMS (1) | 2024.01.11 |
[드림핵/웹해킹] ServerSide: SQL Injection (0) | 2024.01.07 |
[드림핵/웹해킹] Background: Relational DBMS (0) | 2024.01.07 |
[드림핵/워게임] CSRF-2 (웹해킹) (1) | 2024.01.07 |