[Dreamhack] simple_sqli

sy46·2023년 4월 29일
0

dreamhack

목록 보기
2/20

Exercise: SQL Injection


SQL Injection을 배우고 실행해보는 첫 문제
문제 코드를 해석해보자
#!/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)




먼저 데이터베이스 구성 코드이다.

DATABASE = "database.db" // 데이터베이스 파일명을 database.db로 설정
if os.path.exists(DATABASE) == False: // 데이터베이스 파일이 존재하지 않는 경우,
    db = sqlite3.connect(DATABASE) // 데이터베이스 파일 생성 및 연결
    db.execute('create table users(userid char(100),
               userpassword char(100));') // users 테이블 생성
    // users 테이블에 관리자와 guest 계정 생성
    db.execute(f'insert into users(userid, userpassword) 
               values ("guest", "guest"), 
      		("admin", "binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    db.commit() // 쿼리 실행 확정
    db.close() // DB 연결 종료

db라는 이름의 데이터베이스 파일을 생성해주고, db에 users라는 테이블을 만든다. users 테이블 안에는 userid와 userpassword가 칼럼으로 들어가 있으며, userid가 guest인 경우 userpassword는 guest로 바로 알 수 있으나, userid가 admin인 경우 userpassword는 랜덤하게 생성된 16바이트 문자열이므로 알기 어렵다.



다음은 로그인 관련 코드이다.

@app.route('/login', methods=['GET', 'POST']) 
// Login 기능에 대해 GET과 POST HTTP 요청을 받아 처리함
def login(): // login 함수 선언
    if request.method == 'GET': // 이용자가 GET 메소드의 요청을 전달한 경우,
        return render_template('login.html') 
		// 이용자에게 ID/PW를 요청받는 화면을 출력
    else: // POST 요청을 전달한 경우
        userid = request.form.get('userid') 
		// 이용자의 입력값인 userid를 받은 뒤,
        userpassword = request.form.get('userpassword') 
		// 이용자의 입력값인 userpassword를 받고
        // users 테이블에서 이용자가 입력한 userid와 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}' // flag를 출력
            // 관리자 계정이 아닌 경우, 웰컴 메시지만 출력
            return f'<script>alert("hello {userid}");history.go(-1);</script>'
        // 일치하는 회원 정보가 없는 경우 로그인 실패 메시지 출력
        return '<script>alert("wrong");history.go(-1);</script>'

문제의 Login method가 POST이므로 else 부분을 살펴보면,
이용자의 입력값을 각각 userid와 userpassword로 저장을 해주고,
SQL을 통해 users 테이블에서 userid와 userpassword가 모두 일치하는 계정이 있는지 탐색한다.

그럼 여기서 res를 얻기 위한 query_db 함수를 알아보자.

def query_db(query, one=True): // query_db 함수 선언
    cur = get_db().execute(query) // 연결된 데이터베이스에 쿼리문을 질의
    rv = cur.fetchall() // 쿼리문 내용을 받아오기
    cur.close() // 데이터베이스 연결 종료
    return (rv[0] if rv else None) if one else rv
    // 쿼리문 질의 내용에 대한 결과를 반환

역시 cur을 얻기 위해 get_db함수를 실행하므로 재귀적으로 get_db함수를 분석해보자.

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

getattr() 구문

먼저 gettattr 구문은 getattr(object, attribute, default)로 이루어져 있다.
  • object : 필수. 객체.
  • attribute : 필수. 값을 얻을 속성명.
  • default : 선택. 속성이 없는 경우 반환할 값.

row_factory 구문

row_factory는 값을 딕셔너리 형태로 바로 받을 수 있도록 도와주는 구문이다.

해석해보면, getattr(g, '_database', None) 함수를 사용하여 변수 g에 _database 속성이 있는지 확인한다. 이 변수는 Flask에서 request context에 대한 전역 변수이다. 만약 _database 속성이 없다면, 새로운 SQLite 데이터베이스 연결 객체를 생성하고, _database 속성에 저장한다.

그리고 row_factory 속성을 sqlite3.Row로 설정하여, 쿼리 결과를 딕셔너리 형태로 받아올 수 있도록 한다.

마지막으로, db 변수를 반환하여, 이 함수를 호출하는 곳에서 SQLite 데이터베이스 연결 객체를 얻을 수 있도록 한다.



SQL Injection으로 문제 해결


SELECT * FROM users WHERE userid="{userid}"
AND userpassword="{userpassword}";

로그인 쿼리가 위와 같은 구문으로 되어있으므로 이용자가 입력한 userid와 userpassword가 그대로 들어가게 된다.
그러므로 userid를 입력해주고, 뒷 부분을 주석처리해주면 문제를 해결할 수 있다.

0개의 댓글