SQL 구조
SQL 문법 - DML을 중심으로
SELECT, FROM, WHERE 외에 또 다른 조건 설정문
-GROUP BY: 데이터 그룹화
-HAVING: GROUP BY에 조건을 추가할 때 사용
-ORDER BY: 정렬
SQL 인젝션
SQL Injection 공격 사례
SQL Injection 공격 방법
예) 회원 ID를 입력하여 모든 회원정보를 조회하는 것
로그인 화면에서 사용자가 ID가 '1'인 사용자 정보를 요청하면 웹 애플리케이션이 내부의 데이터베이스로 WHERE 조건문이 있는 SQL 쿼리문
SELECT name, email FROM users WHERE ID='1'을 전송한다. 이 쿼리가 실행되면 데이터베이스 users라는 사용자 테이블에서 ID가 1인 사용자 정보를 반환하여
웹 어플리케이션을 통해 클라이언트에 전달한다.
공격대상: select * from users where userid='ID' and userpassword='PASSWORD'
공격예시: select * from users where userid=' ' or 1=1-- and userpassword='PASSWORD'
‘ OR 1=1 -- 을 추가
=> WHERE 절이 모두 참('1'='1')이 되고, -- 를 통해 뒤의 구문을 모두 주석 처리
=> users 테이블에 있는 모든 정보를 조회 가능
UNION: 합집합으로 두 개의 SELECT구문의 결과를 모두 포함시키는 키워드
정상적인 쿼리문에 하나의 추가 쿼리를 삽입하여 원하는 정보를 획득하는 방법으로, 공격자가 or 이라는 구문 대신 UNION 키워드를 삽입한다.
UNION Injection이 성공하기 위해서는 UNION하는 두 테이블의 컬럼 수가 같아야 하고, UNION하는 두 테이블의 데이터 형이 같아야 한다.
공격예시: SELECT * FROM users WHERE id = '1' UNION SELECT name, pw FROM users--
=> 원래 실행되어야 하는 ID가 1인 사용자 정보 이외에도 WHERE 조건문이 없는 SELECT 구문 결과를 포함하게 되어 모든 사용자의 이름과 비밀번호가 반환
데이터베이스 조회 후 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법
한 바이트 씩 비교하여 공격하는 방식으로 다른 공격에 비해 많은 시간 소요
예) Question #1. dreamhack 계정의 비밀번호 첫 번째 글자는 'x' 인가요?
-Answer. 아닙니다
Question #2. dreamhack 계정의 비밀번호 첫 번째 글자는 'p' 인가요?
-Answer. 맞습니다 (첫 번째 글자 = p)
Question #3. dreamhack 계정의 비밀번호 두 번째 글자는 'y' 인가요?
-Answer. 아닙니다.
Question #4. dreamhack 계정의 비밀번호 두 번째 글자는 'a'인가요?
-Answer. 맞습니다. (두 번째 글자 = a)
이렇게 스무고개 게임이나 up-down 게임처럼 질문을 하고 이에 대한 답을 얻어서 데이터베이스의 내용을 알아낼 수 있다.
동물 보호소에 들어온 모든 동물의 정보를 ANIMAL_ID순으로 조회하는 SQL문을 작성해주세요.
동물 보호소에 들어온 모든 동물의 이름과 보호 시작일을 조회하는 SQL문을 작성해주세요.
ORDER BY: SELECT문으로 검색된 데이터를 오름차순(ASC)이나 내림차순 (DESC)으로 정렬시킬 때 사용된다. 디폴트 값은 ASC로, 생략 가능하다.
동물 보호소에 들어온 동물 중 아픈 동물의 아이디와 이름을 조회하는 SQL 문을 작성해주세요. 이때 결과는 아이디 순으로 조회해주세요.
동물 보호소에 들어온 동물 중 젊은 동물1의 아이디와 이름을 조회하는 SQL 문을 작성해주세요. 이때 결과는 아이디 순으로 조회해주세요.
동물 보호소에 들어온 모든 동물의 아이디와 이름을 ANIMAL_ID순으로 조회하는 SQL문을 작성해주세요.
동물 보호소에 들어온 모든 동물의 아이디와 이름, 보호 시작일을 이름 순으로 조회하는 SQL문을 작성해주세요. 단, 이름이 같은 동물 중에서는 보호를 나중에 시작한 동물을 먼저 보여줘야 합니다.
동물 보호소에 가장 먼저 들어온 동물의 이름을 조회하는 SQL 문을 작성해주세요.
LIMIT 숫자: 지정한 갯수만큼의 자료를 보여준다.
LIMIT 시작위치, 반환갯수: 시작 위치의 바로 다음부터 지정한 갯수만큼의 자료를 보여준다.
가장 최근에 들어온 동물은 언제 들어왔는지 조회하는 SQL 문을 작성해주세요.
동물 보호소에 가장 먼저 들어온 동물은 언제 들어왔는지 조회하는 SQL 문을 작성해주세요.
동물 보호소에 동물이 몇 마리 들어왔는지 조회하는 SQL 문을 작성해주세요.
count(*) : 조회되는 데이터의 갯수를 리턴해주는 명령어이다.
동물 보호소에 들어온 동물의 이름은 몇 개인지 조회하는 SQL 문을 작성해주세요. 이때 이름이 NULL인 경우는 집계하지 않으며 중복되는 이름은 하나로 칩니다.
DISTINCT 컬럼명: 조회하려는 컬럼의 중복된 값은 삭제한 후 결과가 나온다. COUNT(DISTINCT 컬럼명): 중복되지 않은 서로 다른 자료갯수를 확인 할 수 있다.
WHERE 칼럼명 IS NULL: 칼럼의 값이 NULL인 경우만 값을 불러온다.
WHERE 칼럼명 IS NOT NULL: 칼럼의 값이 NULL이 아닌 경우만 값을 불러온다.
문제 소스코드
#!/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)
코드를 보다보면
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
guest 계정은 비밀번호가 나와있지만, admin 계정 비밀번호는 숨겨져있다는 것을 알 수 있다.
우리가 로그인을 시도할 때 실행되는 SQL query문을 보면
res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
users 테이블에 입력한 userid와 userpassword가 일치하는 데이터를 조회한다.
현재 우리는 admin 계정의 password를 모르는 상태이기 때문에 주어진 ID admin으로만 로그인을 해야 한다. 따라서 query문을 이렇게 바꿔주면
select * from users where userid="admin"-- " and userpassword="{userpassword}"'
ID 검색 조건만을 처리하도록 두고 뒤의 userpassword 조회 부분은 주석처리가 돼서 WHERE 절이 참이 된다.
따라서 로그인을 할 떄 저러한 query문을 날려줄 수 있게 userid를 admin"--으로 입력해봤더니
로그인에 성공했다!!
문제링크 Lord of SQL Gremlin
query문을 이용해야 겠다는 생각이 들어 처음에 id=' or 1=1을 뒤에 추가해줬는데 아무런 변화가 없었다.
id=' or 1=1 뒷 부분을 주석 처리를 해 주지 않은게 생각나 다시 주석처리를 하고 id=' or 1=1--을 추가해주었는데
여전히 실패했다.
그래서 이번에는 %23(MySQL의 주석)을 추가해서 id=' or 1=1%23이라고 작성했다. 그랬더니 성공했다!!
+) 주석처리를 해주지 않으려면
id가 아니라 pw부분에 pw=' or '1'='1을 추가해주면 된다.
QnA)
마지막 문제에서 왜 --은 주석처리가 안 될까??