SQL injection이란? (실습편)

2023. 11. 18. 22:07보안, 해킹/웹

1. 정의

1.1 SQL이란?

SQL이란 Structured Query Language의 약자로, 관계형 데이터베이스의 데이터 관리를 위해 설계된 언어이다. 명령을 통해 데이터베이스의 생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)를 수행하며 Oracle, PostgreSQL, MySQL 등에서 사용된다.

1.2 SQL injection이란?

SQL injection이란 공격자가 SQL 구문이 포함된 입력을 통해 악의적인 SQL문을 실행되게 함으로써 데이터베이스를 비정상적으로 조작하는 공격 방법이다. 공격자는 SQL injection을 통해

  • 일반적으로 검색할 수 없는 데이터의 열람
  • 데이터의 수정 및 삭제
  • 서버 데이터 손상
  • 서비스 거부 공격

등의 효과를 기대할 수 있다.

2. SQL injection 예시

2.1 Retrieving hidden data

SQL injection을 통해 일반적으로 검색할 수 없는 데이터의 열람이 가능하다.

  • 예시

아래의 사이트는 Portswigger에서 제공하는 SQL injection에 취약한 쇼핑몰 사이트의 예시다.

이 사이트는 SQL을 이용해 서버 내 데이터베이스에서 데이터를 가져온다. 이 서버에서 사용하는 SQL query는 아래와 같다.

SELECT * FROM products WHERE category = 'Gifts' AND released = 1

이 쿼리를 통해 선택한 category 내에서 released 값이 1인 상품만을 products 테이블에서 가져온다는 것을 추측할 수 있다.

또한 이 category 값은 아래와 같이 URL의 파라미터를 통해 입력받고 있다.

그러므로 공격자는 URL의 parameter 값을 임의로 수정하여 SQL injection을 수행할 수 있다.

가령, released가 1이 아닌 데이터를 포함하여 가져오도록 하려면 Gifts'--와 같은 입력을 할 수 있다. 이때 완성되는 SQL query는 다음과 같다.

SELECT * FROM products WHERE category = 'Gifts'--' AND released = 1

이러한 입력으로 뒤의 released 검증 구문을 주석 처리하여 무력화할 수 있고, 실제로 처음엔 표시되지 않았던 상품까지 아래와 같이 출력되는 것을 확인할 수 있었다.

또한 테이블 내의 모든 데이터를 가져오도록 Gifts'or'1'='1'--와 같은 입력을 할 수도 있다. 이때 완성되는 SQL query는 다음과 같다.

SELECT * FROM products WHERE category = 'Gifts'or'1'='1'--' AND released = 1

이 경우 where 구문의 모든 조건이 참이 되므로 products 테이블의 모든 데이터를 가져오는 것을 아래와 같이 확인할 수 있었다.

2.2 Subverting application logic

SQL injection을 통해 사용자 인증 등의 어플리케이션 로직을 우회할 수 있다.

  • 예시

아래의 사이트는 Portswigger에서 제공하는 SQL injection에 취약한 사이트의 로그인 페이지다.

로그인 기능에서 사용자 인증에 사용하는 일반적인 쿼리는 다음과 같다.

SELECT * FROM users_table WHERE username='guest' and password='guest'

username과 password가 데이터베이스에 저장된 값과 일치하는 경우 테이블에서 유저 정보를 가져온다.

이때 username에 admin’—과 같은 입력을 할 수도 있다. 이때 완성되는 쿼리는 다음과 같다.

SELECT * FROM users_table WHERE username='admin'--' and password=''

이 경우 username이 admin이기만 하면 password가 어떤 값이든 상관없이 admin의 인증 과정을 통과할 수 있다.

또는 admin’or’1’=’1’—과 같은 입력을 할 수도 있다. 이때 완성되는 쿼리는 다음과 같다.

SELECT * FROM users_table WHERE username='admin'or'1'='1'--' and password=''

이 경우 where 구문의 모든 조건이 참이 되므로 모든 유저 정보를 가져올 것이다. 다만, 서버에서 데이터를 처리하는 과정에서 가져온 데이터 중 하나만 사용을 하도록 되어 있기 때문에 테이블 내의 가장 위에 있는 유저 데이터를 처리한다.

두 번째 방법을 통해 관리자 계정으로 로그인이 성공한 것을 아래와 같이 확인할 수 있었다.

3. Blind SQL injection

Blind SQL injection이란 어플리케이션이 SQL 쿼리 결과나 오류 내용을 알려주지 않아 이를 확인할 수 없을 때 사용하는 SQL injection이다.

3.1 조건부 응답을 활용한 Blind SQL injection

SQL injection에는 취약하지만 쿼리 결과가 사용자에게 반환되지 않는 경우가 존재한다. 대신 쿼리가 데이터를 반환했을 때 특정 응답을 보여주는 경우, 이러한 특성을 이용해 Blind SQL injection을 활용할 수 있다.

  • 예시

아래의 사이트는 Portswigger에서 제공하는 SQL injection에 취약한 사이트의 로그인 페이지다.

우측 상단에 ‘Welcome back!’이라는 텍스트가 표시되어 있는 것을 확인할 수 있었다.

이 텍스트가 표시되는 조건을 확인하기 위해 요청 패킷의 쿠키값을 변경해보았다.

그 결과 TrackingId라는 쿠키값을 수정하였을 때 ‘Welcome back!’이라는 텍스트가 표시되지 않는 것을 아래와 같이 확인할 수 있었다.

 

다음으로는 TrackingId라는 쿠키값에 SQL 조건문을 삽입하였을 때 조건문이 유효한지 확인하기 위해 xyz’+or+’1’=’1 이라는 값을 대입하여 확인해보았다. 그 결과 아래와 같이 다시 ‘Welcome back!’이라는 문구가 출력된 것을 확인할 수 있었다.

이를 통해 TrackingId의 값으로 서버 내의 어떤 쿼리를 실행시켰을 때 쿼리 결과가 반환된다면 환영 문구가 출력된다고 추측할 수 있었다.

아래는 기존 TrackingId의 값 뒤에 ‘ AND (SELECT ‘a’ FROM users LIMIT 1)=’a라는 값을 대입하여 응답을 확인하였다. 추가로 삽입한 쿼리는 데이터베이스 내에 users라는 테이블이 존재하면 true를 반환하는 쿼리다. 서버의 응답을 통해 users라는 테이블이 존재한다는 것을 확인할 수 있었다.

다음으로 users 테이블 내에서 관리자 계정을 알아낸다. 기존 TrackingID의 값 뒤에 ‘ AND (SELECT ‘a’ FROM users WHERE username=’administrator’)=’a라는 값을 대입하여 응답을 확인하였다. 삽입한 쿼리는 users라는 테이블 내에 username이 administrator라는 데이터가 존재하면 true를 반환하는 쿼리다. 서버의 응답을 통해 username=administrator라는 데이터가 존재한다는 것 역시 확인할 수 있었다.

다음으로 비밀번호의 길이를 확인하였다. ‘ AND (SELECT ‘a’ FROM users WHERE username=’administrator’ AND LENGTH(password)=1)=’a 라는 값을 대입하고, password의 길이를 확인하기 위해 정수값을 늘려가면서 Welcome 메시지가 출력되는 순간을 확인하였다. 그 결과 비밀번호의 길이가 20인 것을 확인하였다.

마지막 과정은 ' AND (SELECT SUBSTRING(password, 1, 1) FROM users WHERE username='administrator' )=’a라는 값을 대입하여 각 글자를 하나씩 대조해 비밀번호를 알아내는 것이다. 이 과정은 burp suite의 intruder라는 기능을 활용하였다.

여러 응답 중 길이가 다른 한 응답을 확인할 수 있었고, 이를 통해 비밀번호의 첫 번째 글자가 e라는 것을 알 수 있었다. 이 과정을 20번 반복하여 종합해 비밀번호를 알아낸다.

최종적으로 관리자 계정으로 접속에 성공하였다.

3.2 Error-based SQL injection

조건부 응답을 활용한 것처럼 쿼리 결과에 따라 특정 오류 응답을 반환하도록 하여 동일한 방식으로 Blind SQL injection을 진행할 수 있다. 또는 쿼리에서 반환된 데이터를 출력하는 오류 메시지를 트리거하는 방법으로도 오류 기반 SQL injection이 가능하다.

  • 예시

위 사이트의 예시에서 TrackingId 쿠키 값에 다음 쿼리를 삽입하여 요청한다고 가정한다. 이때 xyz는 기존 쿠키값으로 가정한다.

xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a
xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a

첫 번째 예시에서 CASE 문은 ‘a’를 반환하기 때문에 전체 쿼리는 참이 된다. 두 번째 예시에서 CASE문은 1/0을 반환하는데, 이때 divided by zero 오류가 발생한다. 이때 발생하는 오류로 인해 HTTP 응답에 차이가 발생하는 경우 이를 이용하여 삽입된 조건이 참인지 확인할 수 있다.

3.3 시간 지연을 활용한 Blind SQL injection

SQL 쿼리가 실행될 때 어플리케이션이 데이터베이스 오류를 정상적으로 처리하는 경우, 어플리케이션의 응답에는 아무런 차이가 없기 때문에 이전의 방식으로는 쿼리 조건이 참인지 거짓인지 확인할 방법이 없다. 이때 삽입한 조건의 참 거짓 여부에 따라 시간 지연을 유발해 쿼리 결과를 간접적으로 확인할 수 있다.

예를 들어, Microsoft SQL server에서는 다음과 같이 시간 지연을 발생시킬 수 있다.

'; IF (1=2) WAITFOR DELAY '0:0:10'--
'; IF (1=1) WAITFOR DELAY '0:0:10'--

첫 번째 입력은 조건이 거짓이기 때문에 시간지연을 유발하지 않는다. 반면 두 번째 입력은 조건이 참이기 때문에 10초의 지연이 발생한다. 따라서 HTTP 응답 속도가 지연되는 것을 단서로 Blind SQL injection이 가능하다.

4. Second-order SQL injection

1차 SQL injection은 어플리케이션이 사용자 입력을 처리하고 안전하지 않은 방식으로 입력을 SQL 쿼리에 통합할 때 발생한다. 2차 SQL injection은 어플리케이션이 사용자의 입력을 데이터베이스에 저장하고, 다른 요청을 처리하는 과정에서 발생한다.

  • 예시

개발자가 SQL injection 취약점을 인지하고 신규 유저 데이터를 저장하는 과정을 안전하게 구성하였다고 가정한다. 이때 공격자는 다음과 같은 입력을 할 수 있다.

username = guest'; update users set password='letmein' where user='administrator'--

유저 데이터를 저장하는 과정은 안전하게 구성하였다고 가정하였기 때문에 해당 데이터는 데이터베이스에 정상적으로 저장된다. 하지만 웹사이트가 아래와 같은 쿼리로 해당 유저의 데이터를 검색하면서 2차 SQL injection이 발생한다.

SELECT * FROM user_options WHERE username='guest'; update users set password='letmein' where user='administrator'--'

이로써 잘못된 username 데이터를 검색하는 과정에서 admin의 패스워드를 변경하는 쿼리를 실행시킬 수 있게 된다.

5. SQL injection의 예방

  • Prepared statement

Prepared statement는 원래 데이터베이스에서 동일하거나 비슷한 데이터베이스 쿼리를 반복적으로 실행할 때 이를 효율적으로 실행하기 위한 방법이다. 사용자의 입력값을 받아 SQL을 실행하는 경우, Prepared statement의 작동 방식은 다음과 같다.

1. Preparation(준비)

SQL 쿼리의 구조를 정의하고 데이터베이스에 전달한다. 이때 사용자의 입력을 받는 부분은 매개변수로 대체한다. 데이터베이스로 전달하는 쿼리의 예시는 아래와 같다.

PREPARE stmt FROM 'SELECT * from users_table WHERE username = ?';

이 쿼리에서 ?에 해당하는 부분이 매개변수에 해당하는 부분이고, 이는 나중에 사용자의 입력값으로 대체된다.

이후 데이터베이스는 이 쿼리를 컴파일하고 최적화하여 대기한다.

2. Execution(실행)

사용자의 입력값을 받은 이후, 쿼리에 사용자의 입력을 대입한다. 이 과정을 바인딩이라고 한다.

SET @username = 'guest';
EXECUTE stmt USING @username;

이때 쿼리는 사전에 컴파일이 되어있기 때문에 사용자의 입력값은 쿼리로서 동작하지 않는다. 따라서 사용자가 악의적인 입력값을 입력하더라도 개발자가 의도하지 않은 쿼리가 실행되는 것을 막을 수 있다.

  • 화이트리스트

Prepared statement를 활용하면 SQL injection을 차단할 수 있지만 대부분 데이터 값에 해당하는 부분에 사용한다. 테이블 이름이나 column 이름 등 데이터베이스의 구조와 관련한 부분에는 prepared statement가 사용이 불가능하다. 이 경우에는 허용된 입력값에 대해 화이트리스트를 만들어 악의적인 입력을 방지한다.