필자는 데이터베이스를 사용할때 sqlite를 사용하는것을 좋아한다.
그 이유는 주 언어로 파이썬을 사용하기도 하고,
많은 SQL이 있지만, 별도 연결 과정 없이
db file만 import하면 바로 사용할 수 있다보니
제일 애용하는 SQL이 되어버렸다.
sqlite는 파일 기반 데이터베이스기 때문에 서버가 따로 필요 없다는 제일 강력한 장점이 있다고 생각한다.
바로바로 쿼리를 이용해서 사용할 수 있는 DB니까.
하지만 단점도 많다. 서버가 따로 필요 없다는 파일 기반 데이터베이스라는 점이 제일 큰 단점같다.
서버를 사용하지 않기 때문에 접근성이 용이하지 못하다. 공유를 하려면 파일을 직접 복사하여 사용해야 한다.
네트워크를 이용한 접근이 불가능하고, 사용자 권한 로직도 부족하다.
하지만 오늘 풀어볼 워게임에서 SQL Injection을 실습할때는 사용하기 정말 간편하다고 생각한다.
바로 워게임으로 접속을 해보자.
https://dreamhack.io/wargame/challenges/1
baby-sqlite
로그인 서비스입니다. SQL INJECTION 취약점을 통해 플래그를 획득하세요! 해당 문제는 숙련된 웹해커를 위한 문제입니다.
dreamhack.io


로그인 창이 하나 있는데, 웹 페이지에서는 아무런 힌트를 얻지 못할것 같다.
바로 소스코드 부분으로 넘어가보자.
#!/usr/bin/env python3
from flask import Flask, request, render_template, make_response, redirect, url_for, session, g
import urllib
import os
import sqlite3
app = Flask(__name__)
app.secret_key = os.urandom(32)
from flask import _app_ctx_stack
DATABASE = 'users.db'
def get_db():
top = _app_ctx_stack.top
if not hasattr(top, 'sqlite_db'):
top.sqlite_db = sqlite3.connect(DATABASE)
return top.sqlite_db
try:
FLAG = open('./flag.txt', 'r').read()
except:
FLAG = '[**FLAG**]'
@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')
uid = request.form.get('uid', '').lower()
upw = request.form.get('upw', '').lower()
level = request.form.get('level', '9').lower()
sqli_filter = ['[', ']', ',', 'admin', 'select', '\'', '"', '\t', '\n', '\r', '\x08', '\x09', '\x00', '\x0b', '\x0d', ' ']
for x in sqli_filter:
if uid.find(x) != -1:
return 'No Hack!'
if upw.find(x) != -1:
return 'No Hack!'
if level.find(x) != -1:
return 'No Hack!'
with app.app_context():
conn = get_db()
query = f"SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};"
try:
req = conn.execute(query)
result = req.fetchone()
if result is not None:
uid = result[0]
if uid == 'admin':
return FLAG
except:
return 'Error!'
return 'Good!'
@app.teardown_appcontext
def close_connection(exception):
top = _app_ctx_stack.top
if hasattr(top, 'sqlite_db'):
top.sqlite_db.close()
if __name__ == '__main__':
os.system('rm -rf %s' % DATABASE)
with app.app_context():
conn = get_db()
conn.execute('CREATE TABLE users (uid text, upw text, level integer);')
conn.execute("INSERT INTO users VALUES ('dream','cometrue', 9);")
conn.commit()
app.run(host='0.0.0.0', port=8001)
코드를 간단히 해석을 해보자면,
get_db라는 함수는
Flask에서 SQLite 연결을 관리하는 함수이다.
같은 요청 내에서는 같은 DB 연결을 사용하도록 구현되어 있고, 호출을 최소화하여 불필요한 연결 생성을 방지한다.
또한, _app_ctx_stack.top은 Flask의 애플리케이션 컨텍스트(stack)에서 최상위 컨텍스트를 가져오는 코드이다.
Flask에서 데이터베이스를 효율적으로 사용하게 하고, 최상위 컨텍스트를 호출하는 함수다.
문제의 핵심인 login 엔드포인트를 한번 보자.
필자도 정확하지 않을 수 있다. 지적은 언제든지 환영이다.
일단, request method가 get일때는 login.html을 사용자에게 렌더링을 해주는 것을 알 수 있다.
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
로그인 폼에서의 사용자의 입력값을 변수에 바인딩하는것을 볼 수 있다.
uid와 upw는 소문자로 치환하고, 값이 없을때는 빈칸이 들어간다.
level은 따로 설정되어 있지 않으면 9로 설정되어 있다.
uid = request.form.get('uid', '').lower()
upw = request.form.get('upw', '').lower()
level = request.form.get('level', '9').lower()
sqli_filter가 있는데, 이 필터에서는 form에서 입력된 데이터가 저 배열 안에 있으면 No Hack을 반환한다.
싱글쿼터, admin, select와 같은 기본적인 SQL Injection 구문과 '\x08', '\x09', '\x00', '\x0b', '\x0d'와 같은
Control Characters도 filtering하고 있다.
sqli_filter = ['[', ']', ',', 'admin', 'select', '\'', '"', '\t', '\n', '\r', '\x08', '\x09', '\x00', '\x0b', '\x0d', ' ']
for x in sqli_filter:
if uid.find(x) != -1:
return 'No Hack!'
if upw.find(x) != -1:
return 'No Hack!'
if level.find(x) != -1:
return 'No Hack!'
제일 중요한 쿼리를 날리는 로직이다.
conn으로 get_db함수를 호출하고,
쿼리를 상당히 위험하게 사용한다. 저렇게 코딩하면 혼난다.
밑에 if uid == 'admin' return FLAG가 있는데, uid의 값을 admin으로 만들어야 문제가 풀리는것 같다.
쿼리쪽을 보면
uid와 upw는 '{}'로 감싸져 있지만, level은 싱글쿼터 안에 있지 않아서 SQL Injection 공격이 가능하다.
하지만 sqli_filter에서 admin과 같은 문자열을 막고 있기 때문에
admin / admin으로 입력하면 No Hack!이 뜬다.
어떻게 sqli_filter를 우회하며, uid값을 admin으로 만들 수 있을까?
with app.app_context():
conn = get_db()
query = f"SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};"
try:
req = conn.execute(query)
result = req.fetchone()
if result is not None:
uid = result[0]
if uid == 'admin':
return FLAG
except:
return 'Error!'
return 'Good!'
하지만 sqli_filter에서 admin과 같은 문자열을 막고 있기 때문에
admin / admin으로 입력하면 No Hack!이 뜬다.
어떻게 sqli_filter를 우회하며, uid값을 admin으로 만들 수 있을까?
정답은,
| level= /**/union/**/values(char(97)||char(100)||char(109)||char(105)||char(110)) |
level의 값을 이렇게 설정하여 요청한다면
풀리게 된다.
/**/ 으로 공백을 우회하고,
char 97 = a
char 100 = d
char 109 = m
char 105 = i
char 110 = n
admin이라는값을 주고, | 파이프 기호로 이를 이어서 values를 이용하여 값으로 전달해 준다.
values는
이러면 풀린다.

'webhacking' 카테고리의 다른 글
| WebGoat - JWT tokens (0) | 2025.11.09 |
|---|