Brief Tutorial of SQL
SQL Injection에 대해 학습하기 전에 기본적인 SQL문을 어떻게 사용하지는 간단하게 알아보자.
Log in to MySQL
오픈소스 관계형 데이터 베이스 관리 시스템인 MYSQL 데이터베이스를 사용할 떄 로그인 명령어이다.
Create a Database
MYSQL 내부에는 여러 개의 데이터베이스를 생성할 수 있으며 "SHOW DATABASES" 명령어를 통해 현재 존재하는 데이터베이스를 나열할 수 있다. 또한 CREATE DATABASE [이름]; 명령어를 통해 새로운 데이터베이스를 생성할 수 있다.
데이터베이스의 속성(열)을 생성하기 위해서는 위 사진과 같이 각 속성의 이름, 타입 등을 명시해 줄 수 있다.
이때, 어떤 데이터베이스를 사용할 것인지 USE 명령어를 통해 시스템에 알리고, DESCRIBE 명령어를 통해 현재 데이터베이스의 구조를 확인할 수 있다.
Insert a Row
INSERT INTO 명령어를 사용하여 테이블에 새로운 레코드를 삽입할 수 있다. 위 예제에서는 employee라는 데이터베이스에 데이터를 삽입하고 있으며, ID값은 데이터베이스가 자동으로 할당해주는 것이기 떄문에 지정하지 않아도 된다.
SELECT Statement
SELECT 문을 통해 데이터베이스에서 정보를 검색할 수 있다. 이때 *은 모든 속성(열)을 보겠다는 의미이고, 아래와 같이 보고 싶은 속성을 지정해주면 해당하면 데이터만 출력한다.
WHERE Clause
WHERE은 SELECT, UPDATE, DELETE 등 여러 유형의 SQL문을 작성할 떄의 조건으로 설정된다. WHERE 뒤에 조건문이 올 수 있고, 해당 조건이 참인 경우에만 반영한다. AND와 OR을 통해 여러 조건을 결합할 수도 있다.
위 사진에서 첫 번째의 경우 EID가 'EID5001'인 레코드만 반환하도록 하고, 두 번째의 경우 EID가 'EID5001'인 경우와 이름이 "David"인 레코드만 반환하도록 조건을 설정한 모습이다.
이때, WHERE절에 1=1과 같은 항상 참인 조건을 걸면 where절이 없는 것과 동일하지만 SQL Injection 공격에서는 유용하게 사용할 수 있다.
UPDATE Statement
UPDATE문을 이용하여 현재 존재하는 데이터를 변경할 수 있다.
Comments
SQL문에도 주석이 존재한다. "#"과 "--"을 사용하면 one line만 주석처리되며, 해당 기호 뒷부분부터 주석처리된다. "/*", "*/" 기호를 이용하면 c언어처럼 여러 줄을 주석처리 할 수 있다.
Interacting with Database in Web Application
데이터베이스는 웹 페이지와 어떻게 연결되어 있을까?
일반적으로 브라우저를 통해 웹 페이지를 접속한다. 이후 유저가 데이터를 입력한 후 서버에 request를 전송하면 서버는 입력된 정보가 유효한 것인지 판단하기 위해 쿼리문을 작성하여 DB로 쿼리를 전송한다. DB는 쿼리의 결과에 대해 참/거짓을 알려주거나 쿼리된 결과값을 전송한다. 이후 서버에서 다시 검증 결과를 유저에게 반환한다.
이때, SQL Injection은 데이터베이스에 대한 공격으로 데이터베이스에 데미지를 줄 수 있다. 사용자는 웹 서버를 통해서 DB와 통신하지만 직접적으로 DB에 위해를 가할 수 있다.
Getting Data from User
사용자로부터 데이터를 어떻게 가져오는지 확인해보자.
위 사진은 사용자가 데이터를 입력할 수 있는 양식이다. submit 버튼을 클릭하면 데이터가 첨부된 HTTP request가 서버에 전송된다.
위 입력창을 구현한 HTML 코드이다. 입력으로 받은 데이터를 서버의 php 코드인 getdata.php를 통해 GET 메소드로 전송하고 있음을 알 수 있다.
submit 버튼을 클릭하면 전송되는 request의 url이다. www.example.com의 서버에서 getdata.php라는 요청처리 경로를 통해 입력된 데이터들이 파라미터로 들어가고 있는 모습을 확인할 수 있다. 이때, 각 파라미터는 & 기호를 통해 구분되고 있음을 확인할 수 있다.
Getting Data from User
서버는 어떻게 유저로부터 데이터를 가져오는지 살펴보자.
앞서 전송한 요청은 HTTP GET 메소드라고 언급하였다. HTML 코드의 method field가 GET 유형으로 지정했기 때문이다. 이때, GET 요청에서 URL의 물음표(?) 뒤에 파라미터가 첨부된다. 각 파라미터는 "이름 = 값"의 쌍을 가지며 &로 구분된다.
HTTPS로 전송되는 경우 형식은 비슷하지만 데이터가 암호화된 채로 전송된다. 이러한 요청이 php 스크립드에 도착하면 HTTP 요청에 존재하는 파라미터는 $_GET 또는 $_POST 배열에 저장되며, 각 값은 위 사진과 같이 가져올 수 있다.
How Web Applications Interact with Database
php에서 데이터베이스의 쿼리를 사용하기 전에 데이터베이스의 서버와 연결해야 한다.
데이터베이스와의 연결을 위해 new mysqli() api를 사용하여 연결을 생성할 수 있다. 이때, 인자로는 서버의 위치, 데이터베이스의 아이디와 비밀번호, 데이터베이스의 이름까지 총 4개의 인자를 필요로 한다.
php에서 유저의 입력을 어떻게 데이터베이스로 전송하는지 확인해보자.
위 사진에서 붉은 네모박스 안에 SQL 쿼리문이 존재함을 확인할 수 있다. 이때, 유저의 입력이 필요한 eid, epw 부분은 서버로부터 받아온 변수에 할당하고 있음을 확인할 수 있다.
SQL Injection은 커뮤니케이션 채널, 즉 사용자와 데이터베이스 사이의 채널을 통해 발생한다.
SQL Injection Attacks
SQL문은 php에서 생성되고 실행된다. 그런데 SQL Injection은 개발자가 의도한 바와 다른 쿼리문을 사용자가 실행할 수 있기 때문에 발생하는 문제이다. 앞서 php에서 쿼리문을 전송하기 위해 사용자의 입력이 필요한 부분을 위와 같이 대치될 수 있다.
만약 아이디 부분에 유저가 입력으로 EID5002' #를 입력하면 위 사진과 같이 # 뒷 부분이 다 주석처리 되어 무시되기 때문에 비밀번호 검증 부분이 닫혀 아이디 값만으로 데이터베이스에 존재하는 값을 긁어올 수 있다.
아이디로 EID5002' #을 입력한다면 실제로 입력으로 들어가는 쿼리문은 위 사진과 같다. 이 경우 해당 아이디 사용자의 비밀번호응 몰라도 직원의 이름, 급여, SSN을 반환할 수 있으며 이는 보안이 침해되는 방식이다.
데이터베이스에 존재하는 모든 EID 값을 모르는 경우 위 사진과 같이 입력으로 a' OR 1=1을 입력하여 WHERE절이 항상 참이 되도록 만들면, 데이터베이스에 존재하는 모든 레코드(값)를 가져올 수 있다.
SQL Injection Attacks using cURL
우리는 cURL을 사용하여 공격을 진행할 수 있다. 이때, cURL은 url을 기반으로 서비스 명령을 전송 가능한 유틸리티이다. cURL을 통해 스크립트를 생성, 실행이 가능하기 때문에 대량으로 자동 공격이 가능하기 떄문에 편리하다.
cURL을 사용하면 웹 페이지 대신 command-line에서 요청을 전송할 수 있다. 그러나 위 사진의 형태로는 데이터가 전송되지 않는다. 왜냐하면 cURL 방식은 데이터에 존재하는 모든 특수 문자가 인코딩된 상태로 전송되어야 하기 때문이다.
따라서 '(apostrophe)은 %27로, ' '(white space)은 %20으로, #은 %23으로 인코딩하여 전송하면 위 사진과 같이 결과를 얻을 수 있다. 각 인코딩된 값은 웹 통신 시 정해진 값이다.
Modify Database
명령문이 UPDATE 또는 INSERT INTO인 경우 데이터베이스를 변경할 수 있다.
위 사진과 같은 웹 페이지는 이미 존재하는 비밀번호를 수정하는 쿼리문이다. 이때 들어가는 입력 값으로는 아이디, 이전 비밀번호, 새 비밀번호이다.
submit 버튼을 클릭하면 HTTP POST 요청이 전송되고, 해당 요청은 사용자의 비밀번호를 변경하기 위해 UPDATE문을 사용하는 서버 스크립트인 changepasswd.php로 전송된다.
이때 php 파일에서는 $_POST 배열에 저장된 값을 기반으로 SQL statment를 만들 수 있고, query() 명령을 통해 실행 가능하다. 악성 유저가 악의적으로 데이터베이스를 수정하는 예제를 살펴보자.
EID5000 값을 가지고 있는 유저는 자신의 salary 값을 임의로 변경하고자 한다. salary 값을 변경하기 위해 비밀번호 변경 창에 위 사진과 같이 입력하였다.
위와 같이 입력을 작성한 경우 실제로 반영되는 SQL문은 위 사진과 같다. 이때 입력으로 새로운 비밀번호를 입력 후 '를 입력하여 앞에서 열린 '를 잘 닫아 주는 것이 중요하다.
이러한 방식을 이용하여 다른 사람의 EID 값을 통해 다른 사용자의 값도 변경할 수 있다. 이때 EID fleid에 #을 넣어 주석처리하면 비밀번호를 몰라도 공격이 가능하다.
Multiple SQL Statements
우리는 지금까지 php에서 지정한 SELECT 또는 UPDATE 문을 이용하여 공격을 수행하였다. 이때, 유저가 임의의 SQL문을 실행할 수 있다면 더욱 강력한 공격이 가능하다.
사진과 같이 a'; 입력 후 새로운 SQL문인 DROP DATABASE dbtest; #을 입력하면 데이터베이스 전체를 삭제할 수 있다. 위와 같이 명령어를 입력할 경우 서버에 실제로 반영되는 SQL문은 아래와 같다.
기존에 존재하던 SQL문 이후에 새로운 SQL문이 추가되고, 기존의 SQL문이 사라진 것을 확인할 수 있다.
그러나 이러한 공격은 MYSQL에서 성공할 수 없다. 왜냐하면 우리가 사용하고 있는 php의 mysqli extension의 mysql::query() 메서드는 여러 줄의 쿼리를 실행할 수 없기 때문이다.
따라서 위 사진과 같이 $mysqli->query()에 두 개의 SQL문을 넣고 실행해보면,
위 사진과 같이 오류 메세지가 반환된다. 'DROP DATABASE dbtest'라는 명령어를 실행시킬 수 없다는 의미이다.
만약 여러 줄의 쿼리문을 실행하고 싶으면, $mysqli->multi_query() 명령어로 실행하면 된다. 그러나 위험성으로 그다지 추천하지는 않는다.
The Fundamental Cause
이러한 SQL Injection이 발생하는 원인은 데이터와 코드가 혼재되어 있기 때문이다. 서버 관리자의 입장에서 유저로부터 들어오는 입력이 모두 데이터임을 가정하고 코드를 구성하였지만, 악의적인 사용자는 입력으로 악성코드를 넣어 코드처럼 실행시키기 때문이다.
데이터와 코드를 혼재시키면서 발생할 수 있는 여러 유형에 대해 알아보자.
(a) SQL
SQL문이 실행되기 위해서 신뢰성 없는 유저의 입력과 신뢰성 있는 코드를 섞어 SQL Statement를 생성한 후 SQL parser를 통해 데이터와 SQL 코드를 분리한 후 실행한다.
이때, 데이터로 악의적인 코드를 넣으면 SQL 파서가 입력 데이터로 들어온 코드를 실제 코드라고 판단하여 컴파일한 후 실행되어 버린다.
(b) JavaScript
이 경우 XSS 공격과 동일한 경우이다. 유저가 작성할 수 있는 필드에 js를 실행할 수 있는 부분이 존재한다. 마찬가지로 악의적인 유저가 해당 부분에 코드를 집어넣을 수 있다.
악성 코드가 존재하는 게시판을 읽은 사용자는, 악의적인 사용자가 삽입한 코드를 실행하여 의도치 않은 operation을 발생시킬 수 있다.
(c) system()
마찬가지로 system() 함수를 통해서 악의적인 사용자는 ls와 같은 쉘 명령어를 집어넣을 수도 있다. 여러 가지 명령어를 함께 집어넣을 수 있으며, 이 명령어는 쉘에 의해 실행될 수 있다.
프로그램에서 시스템 함수를 이용할 때, 활용하는 데이터가 유저의 입력인 경우 앞선 두 방법과 동일한 방법으로 공격이 가능하다. 마찬기자로 데이터와 코드가 섞여서 발생하는 문제다.
(d) Format String
printf([user's input]와 같이 유저의 입력을 printf를 통해 출력하는 경우, 이 입력 안에 format이 존재하면 메모리에 있는 내용을 읽을 수 있다. 이러한 방식을 이용히여 메모리가 공격자 마음대로 읽고 쓸 수 있게 된다.
공격자 마음대로 메모리의 데이터를 읽을 수 있으면, 프로그램 실행 중에 존재하는 크리티컬 데이터(pw, 해시값) 등이 유출될 수 있다. 또한 악의적으로 메모리의 값을 수정할 수 있는데, 예를 들어 게임에서 코인이나 능력치를 수정할 수 있다.
(e) C program
위 네 가지의 경우 모두 코드와 데이터가 섞여 있기 때문에 발생하는 문제이다. 그런데, c program은 프로그램이 모두 실행가능한 상태로 컴파일된 이후 함수의 파라미터를 유저의 입력 데이터로 처리할 수 있다. 이렇게 프로그램을 실행하는 경우 코드와 데이터가 분리되어 취약점이 발생하지 않을 수 있다.
Countermeasures
우리는 이러한 SQL Injection을 방어할 수 있는 대응책에 대해 알아보자.
Filtering and Encoding Data
사용자가 제공한 데이터와 코드가 섞이기 전에 코드로 해석될 수 있는 문자를 필터링 + 인코딩하여 해당 부분이 실행되지 않도록 막을 수 있다. 일반적으로 유저의 입력에서 특수문자가 들어오는 경우 주로 SQL injection에 사용되므로 이들을 제거하기 위해 인코딩을 진행한다.
위 사진에서 '와 같은 특수문자를 인코딩하면 parser는 해당 부분을 코드가 아닌 데이터로 인식할 수 있도록 하여 코드로 해석되는 것을 방지한다.
또한 php는 mysqli::real_escape_string() 확장을 통해 특수문자를 자동으로 인코딩해줄 수 있다.
Prepared Statement
SQL Injection이 발생하는 근본적인 원인은 데이터와 코드가 섞여있기 때문이다. 따라서 우리는 데이터와 코드를 아예 분리해줄 수 있다. 즉, 코드와 데이터를 다른 채널로 분리하여 서버로 전송하여 코드 실행 시 함수의 파라미터 값을 유저의 입력으로 매칭해준다.
원래 우리가 사용한 방식이다. 원래는 코드가 데이터를 섞어 어떠한 SQL statement로 만든 후 데이터베이스로 전송하였다.
하지만 위와 같이 코드를 미리 컴파일한 후 코드를 전송하고, 이후 bind_param() 함수를 이용하여 유저의 입력을 코드의 파라미터 값으로 넣어줄 수 있다. 즉, 코드 영역을 미리 만들어두고 실행 시 필요한 파라미터를 매핑해주는 방법이다.
이러한 방법은 SQL 쿼리문 실행 시 최적화를 위한 방법이지만 보안적으로도 유용하다.
Prepared Statement가 안전한 이유는 다음과 같다.
신뢰할 수 있는 코드는 코드 채널을 통해 전송되고, 신뢰할 수 없는 데이터는 데이터 채널을 통해 전송된다. 따라서 데이터베이스는 코드와 데이터의 경계를 정확히 알 수 있고, 데이터 채널로 온 신뢰할 수 없는 데이터는 파싱되지 않아 코드로 컴파일되지 않는다. 따라서 공격자가 입력에 아무리 악성코드를 숨겨도 입력이 데이터로만 인식된다.
Blind SQL Injection
HTTP 응답에 결과가 포함되지 않는 경우가 많다. 응답에 결과가 없으면, 공격자가 공격 시 정보를 알아낼 수 없는 경우가 많은데 이를 우회하는 세 가지 방법을 소개하겠다.
Conditional Response
쿼리문에 조건문을 넣어서 결과가 참 또는 거짓일 때 다른 응답이 오는 경우를 이용하는 방식이다. 아래 공격 상황을 가정해보자.
1. client는 서버에 쿠키로 TrackingId를 전송한다.
Cookie: TrackingId=u5YD3PapBcR4lN3e7Tj4
2. 서버는 쿠키를 SQL 쿼리문에 사용한다. 이 쿼리문은 TrackingId에 대해 비밀번호를 반환하는 쿼리문이다.
SELECT TrackingPw FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4‘
3. 위 결과에서 쿼리문의 결과가 참이던 거짓이던 응답에는 비밀번호가 포함되지 않는다. 그런데, 해당 쿼리문이 참인 경우 'Welcome'를 출력하고, 거짓인 경우 아무것도 출력하지 않는다.
위 상황을 이용하여 공격 예제를 짜보자.
xyz' AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1) > 'm
위와 같이 입력을 넣어 공격을 진행할 수 있다. 각 부분에 대해 알아보자.
xyz'
원래 알고 있던 TrackingId이다. 이때 '는 WHERE 절에서 여는 '와 매칭된다.
AND SUBSTRING((SELECT Password FROM Users WHERE Username = 'Administrator'), 1, 1)
AND를 이용하여 앞선 WHERE 조건과 연결하고, substring 함수를 통해 Administrator이라는 이름의 사용자 비밀번호에서 한 글자씩 뽑아낸다.
'm
m이라는 임의의 문자와 크기를 비교하여 비밀번호는 한 문자씩 범위를 좁혀가며 알아낼 수 있다. 이때 '는 WHERE 절에서 닫는 '와 매칭된다.
이때 Administrator 이름의 사용자의 비밀번호에서 첫 번째 글자가 m보다 큰 경우 참이므로 Welcome을 리턴하게 되지만, 거짓인 경우 아무런 변화가 없기 때문에 하나씩 알아낼 수 있다.
SQL error
일부러 데이터베이스에서 오류가 나도록 하는 쿼리를 전송한다. 에러 발생 여부에 따라 응답이 어떻게 달라지는지 구분할 수 있는 경우에 시도할 수 있는 공격법이다. 공격은 아래와 같이 할 수 있다.
(1) SQL 오류로 인해 응답이 달라지는 경우
xyz' AND (SELECT
CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm')
THEN 1/0 ELSE 'a' END FROM Users)='a
위의 경우 마찬가지로 비밀번호를 한 자리씩 대조해가며 비밀번호를 알아내는 방식이다. 참이면 1/0(오류)을, 거짓이면 'a'를 반환하게 하는데 이때, 1/0은 오류가 발생하는 코드이므로 참이면 오류 발생, 거짓이면 'a'를 반환하게 되는데, 위 명령의 경우 뒤에 따라오는 = 'a와 비교하여 그냥 넘어가게 된다.
(2) SQL 오류가 유용한 정보를 반환하는 경우
말 그대로 오류 메세지에 유용한 정보가 있는 경우이다.
“Unterminated string literal started at position 52 in SQL SELECT * FROM tracking WHERE id = '''. Expected char”
예를 들어 입력으로 닫히지 않은 '만 넣었다고 했을 때, 쿼리가 닫히지 않으며 오류 메세지가 위와 같은 경우 우리는 php에서 전송되는 SQL문의 형식을 확인할 수 있다. 따라서 공격 쿼리를 제작할 때 유용하게 사용할 수 있다.
또, 만약 입력으로
CAST((SELECT example_column FROM example_table) AS int)
와 같이 쿼리의 결과를 형변환 시키려고 한다고 가정하자.
“ERROR: invalid input syntax for type integer: "Example data“. 오류메세지가 이와 같이 데이터 값 자체를 포함하며 반환시킬 수 있다.
Time Delays
마지막으로 응답에 아무런 정보도 포함되지 않을 때 사용할 수 있는 방법이다. SQL문 결과가 참인지 거짓인지에 따라 타임 딜레이를 다르게 설정할 수 있다. 이때, SQL문 실행 후 딜레이가 발생하면 HTTP 응답도 마찬가지로 느려지기 때문에 시간 지연으로 해당 SQL문이 참인지 거짓인지 확인할 수 있다.
'; IF (SELECT COUNT(Username) FROM Users WHERE Username = 'Administrator' AND
SUBSTRING(Password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--
입력을 위와 같이 넣었을 때, 참인 경우 delay, 거짓인 경우 바로 결과를 반환한다. 따라서 해당 쿼리의 결과가 참인지 거짓인지 판단할 수 있다.
이러한 방법은 서버에서 딜레이되는 값의 최댓값으로 항상 응답을 주게 설정함으로써 막을 수 있다. 하지만 이러한 방법을 사용할 경우 응답시간이 길어지기 때문에 유저의 불만을 초래할 수 있어 적용하는데에 어려움이 있다.
지금껏 우리가 학습한 CSRF나 XSS는 클라이언트 단에서 실행되는 형태지만, SQL Injection은 서버 사이트에서 실행되는 공격이라는 점에서 차이가 있다.
'Computer Science > 네트워크 및 웹 보안(Network & Web Security)' 카테고리의 다른 글
[네웹보/NWS] XSS 공격 (0) | 2024.04.22 |
---|---|
[네웹보/NWS] CSRF 공격 (0) | 2024.04.22 |