769 - SQL Injection

2026. 3. 20. 14:00보안/DreamHACK

 

 

우선 코드를 분석해보자!

from flask import Flask, request, render_template, g
import sqlite3
import os
import binascii

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

 

Flask 웹앱을 생성한다.

 

try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'

 

현재 폴더의 flag.txt.를 읽어서 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), userlevel integer);')
    db.execute(f'insert into users(userid, userpassword, userlevel) values ("guest", "guest", 0), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}", 0);')
    db.commit()
    db.close()

 

DB가 없으면 새로 만들고 users 테이블을 생성한다.

컬럼은 userid, userpasswor, userlevel 총 3개를 생성한다.

초기 데이터는 ("guest", "guest",0), ("admin", 랜덤비밀번호, 0) 총 2개를 넣는다.

 

admin 비밀번호는 랜덤이라 정상 로그인으로는 맞출 수 없다.

그런데 admin의 userlevel도 0인 것을 보아 SQL Injection으로 admin 행을 끌어오게 만드는 문제일 가능성이 높다.

 

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 문자열을 그대로 실행한다. 파라미터 바인딩이 없기에 

사용자 입력이 SQL문 안으로 그대로 들어가면 SQL Injection이 가능하다.

 

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    else:
        userlevel = request.form.get('userlevel')
        res = query_db(f"select * from users where userlevel='{userlevel}'")
        if res:
            userid = res[0]
            userlevel = res[2]
            print(userid, userlevel)
            if userid == 'admin' and userlevel == 0:
                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>'

 

request.form.get() 는 사용자가 웹에서 입력한 값을 가져오는 함수다.

⭐ 사용자가 POST로 보낸값은 userlevel = request.form.get('userlevel') 뿐이다.

이 값을 SQL에 그대로 넣는다.

 

보통 로그인이라면 아이디, 비밀번호도 받아야 하는데 해당 문제는 userlevel만 받는다.

이를 통해 이 문제는 특정 조건을 만족하는 행을 DB에서 뽑아오게 하는 문제임을 알 수 있다.

 

 

⭐  취약점

res = query_db(f"select * from users where userlevel='{userlevel}'")

 

사용자 입력을 SQL 문자열에 직접 이어붙였기 때문에 취약하다.

예를 들어 입력값에 따옴표를 끊고 SQL 문법을 섞으면 원래 개발자가 의도한 조건을 바꿀 수 있다.

해당 구조가 전형적인 SQL Injection 이다.

 

 

⭐  플래그가 나오는 조건

if userid == 'admin' and userlevel == 0:
	return f'hello {userid} flag is {FLAG}'

 

플래그를 얻으려면 res에 들어온 첫번째 행이 userid == 'admin', userlevel == 0 이어야 한다.

DB에 이미 ("admin", 랜덤비밀번호, 0) 행이 존재하므로

쿼리 결과의 첫번째 행을 admin으로 만들면 된다!

 

 

 

 


 

주어진 주소에 들어간다.

 

코드를 해석했던대로 입력값은 userlevel만 존재한다.

 

 

정상값인 0 을 입력할 경우, guest로 정상 로그인된다.

 

우리는 플래그 조건을 참고하여 admin 행이 나오게 해야 한다.

 

⭐ SQL Injection 공격

select * from users whrere userlevel='입력값'

 

원래 쿼리에서 입력값으로 WHERE 조건을 바꿔야한다.

 

0' AND userid='admin' --

 

우리의 목표는 admin이 첫번째로 출력되게 하는 것이다.

해당값을 입력하면 select * from users wherer userlevel='0' AND userid='admin' -- 쿼리가 실행된다.

 

0'을 입력하여 문자열을 강제로 닫고 AND 조건을 추가하여

"userlevel이 0이면서 admin" 인 데이터만 가져오게 된다. 

기존에 '가 남아있으므로 -- 을 통해 뒤 코드를 다 무시하게 하여 문법 에러를 방지한다.

 

--을 빼면 Ineternal Server Error가 뜨는 것을 확인할 수 있다.

 

플래그값을 확인할 수 있다. 😁

 

 

문제 해결 성공!

'보안 > DreamHACK' 카테고리의 다른 글

1401 - Bypass  (0) 2026.03.20
768 - Command Injection  (0) 2026.03.20