발자취

#07 SQL Injection 공격 본문

3-1/웹 어플리케이션 보안

#07 SQL Injection 공격

해린 2023. 5. 7. 00:25

#01 Structured Query Language (SQL)
1. SQL
SQL이란?
데이터베이스 내에서 다음의 정보를 액세스하거나 관리하는데 사용되는 프로그램 언어
    - 사용자 계정, credentials (패스워드 등과 같은 기밀 정보), 개인 정보
    - 애플리케이션 내의 각각의 사용자 권한
    - 데이터베이스를 구성하는 각각의 요소와 관련된 정보
 
SQL의 특징
- Interpreted 언어: 컴파일된 언어가 아님. C나 C++과 같은 언어는 컴파일된 실행파일을 실행하는데, 인터프리터 언어는 바로 실행함 -> "빠른 실행" (파이썬, PHP가 인터프리터 언어임)
- 사용자가 제공한 입력 데이터 값을 가지고 웹 애플리케이션의 다양한 기능 구현에 이용됨
- 공격자가 조작된 입력 데이터 값을 제공할 경우에 다양한 취약점에 노출됨- Oracle, MS-SQL, MySQL
 
 
2. SQL Injection 공격
- 공격자가 조작된 데이터 값을 입력하여 다음의 공격을 수행
    - 데이터베이스에 저장된 민감한 데이터들을 읽거나 수정함
    - 데이터베이스가 위치한 서버에 대해 허가 받지 않는 방식으로 제어권한 획득함
 
- 공격자는 아래의 구성 요소가 사용자 입력 데이터 값을 처리하는데 있어서 갖는 취약점을 이용함
    - SELECT 문
    - INSERT 문
    - UPDATE 문
    - UNION 연산자
 
 
2-1. SELECT 문
- 데이터베이스에 대한 질의(query)를 할 때 사용됨- SELECT A FROM B WHERE C='D'
    - B 테이블에서 C 항목이 D값과 일치하는 레코드들 중 A 항목들을 추출함
    - A, B, C: 애플리케이션이 생성될 때 프로그래머에 의해 만들어지는 질의의 일부분
    - D: 사용자가 제공한 입력 데이터 값 (두 개의 인용부호 '' 안에 포함됨)
- 사용 예: SELECT author, title, year FROM books WHERE publisher = 'Wiley'
        -> 여기서 'Wiley'가 사용자가 제공한 데이터
 
2-1-1. SELECT 문에서 Injection 공격
[1] Case 1: 사용자가 제공한 조작된 입력 값이 Wiley' OR 1=1-- 인 경우, 데이터베이스는 테이블 내의 모든 레코드를 return 함
SELECT * FROM books WHERE publisher = 'Wiley' OR 1=1--'
는 
SELECT * FROM books WHERE 1=1
와 같다!
(-- 뒤는 모두 주석 처리되기 때문... 1=1은 항상 참이기 때문에 모든 레코드를 return하는 결과가 발생!)
 
[2] Case 2: 사용자가 제공한 조작된 입력 값이 Wiley’ OR ‘a’=‘a 인 경우, 테이블 내의 모든 레코드를 return 함
SELECT * FROM books WHERE publisher = 'Wiley' OR 'a'='a'
이 경우에도 역시 'a'='a'라는 항상 참인 조건이 붙어있기 때문에 모든 레코드를 return하는 결과가 발생함!
다만, 'a'는 문자열이기 때문에 주석문 처리(--)할 필요가 없음
 
[3] 로그인 회피: 정상적이지 않은 정보를 제공하여 로그인을 해내는 공격- 우선, 아래의 SELECT 문에서, users 테이블에서 사용자 이름이 marcus, 패스워드가 secret 인 사용자들을 모두 추출하기 위해선
SELECT * FROM users WHERE username = ‘marcus’ and password = ‘secret’
이렇게 입력하면 됨
 
- 사용자가 관리자일 때 패스워드 입력이 필요없는 시스템이 있다고 가정 (username만 입력해도 로그인 되는 상황)
    - if, 관리자의 username을 아는 경우 (관리자의 username == admin): username에 admin'--을 입력
           -> SELECT * FROM users WHERE username = ‘admin’--’ AND password=‘foo’을 입력하면 뒷부분이 주석처리되어
               SELECT * FROM users WHERE username = ‘admin’ 과 같게 받아들여짐! => 로그인 회피 공격 성공!
 
    - if, 관리자의 username을 모르는 경우 (항상 true인 값을 넣어야 함): username에 'OR 1=1-- 입력
           -> SELECT * FROM users WHERE username = ‘ 'OR 1=1--’ AND password=‘foo’을 입력하면
               SELECT * FROM users WHERE username = ‘ ’ OR 1=1 으로 인식되어
               SELECT * FROM users WHERE 1=1로 받아들여짐 => 로그인 회피 공격 성공!
 
 
2-2. INSERT 문
- 테이블 내에서 새로운 데이터를 생성할 때 사용됨
- INSERT INTO A(B) VALUES (‘C’)
    - C의 값을 테이블 A의 B 항목으로 삽입함
    - C : 사용자가 제공한 입력 데이터 값
    - B에 포함된 원소들의 개수와 각각의 원소들의 데이터 타입이 C에 포함된 입력 값들의 개수와 각각의 원소들의 데이터 타입과 일치해야 함
 
- 예제: username이 foo인 일반 사용자를 데이터베이스의 테이블에 삽입하는 경우
           주로 privs의 값이 1이면 일반 사용자 권한, 0이면 관리자 사용 권한을 나타냄
                 -> 관리자 권한을 사용하기 위해 privs가 0이 되도록 만들어야 함!
 
2-2-1. INSERT 문에서 Injection 공격
[1] privs 값을 0으로 만들기: username 입력 부분에 foo’, ‘bar’, 9999, 0) -- 를 입력
INSERT INTO users (username, password, ID, privs) VALUES (‘foo’, ‘bar’, 9999, 0) --’, ‘secret’, 2248, 1)
이렇게 입력해주면
INSERT INTO users (username, password, ID, privs) VALUES (‘foo’, ‘bar’, 9999, 0)
로 인식해서 privs가 0으로 받아들이므로 관리자 권한을 얻을 수 있음
 
[2] INSERT 문에서 B에 포함된 원소들의 개수를 알지 못하는 경우
- 사용자 입력 값인 C 값의 개수를 1부터 하나씩 증가시켜가면서, 시스템이 사용자 입력에 대해서 정상적으로 처리하는 경우를 찾으면 됨
- 예: username이 foo 인 경우
       - 사용자 입력 값으로 foo’)--, foo’, 1)--, foo’, 1, 1)--, foo’, 1, 1, 1)-- 이런 식으로 입력하면 됨!
 
 
2-3. UPDATE 문
- 테이블 내에서 기존 데이터의 내용을 갱신(변경)하는데 사용됨
- UPDATE A SET B=‘C’ WHERE D=‘E'
    - 테이블 A에서 D 항목이 E값과 일치하는 레코드들의 B 항목을 C값으로 변경함
    - C, E : 사용자가 제공한 입력 데이터 값
 
- 예제: username이 marcus 인 사용자의 패스워드를 secret에서 newsecret 으로 변경
           UPDATE users SET password=‘newsecret’ WHERE user = ‘marcus’ password = ‘secret’
 
2-3-1. INSERT 문에서 Injection 공격
[1] UPDATE 문을 통해 모든 사용자들의 password를 강제적으로 newsecret으로 변경함: username에 admin’ or 1=1-- 입력
UPDATE users SET password=‘newsecret’ WHERE user = ‘admin’ or 1=1--’ password = ‘secret’

UPDATE users SET password=‘newsecret’ WHERE user=‘admin’ or 1=1
로 인식되어
UPDATE users SET password=‘newsecret’ WHERE 1=1
로 받아들여짐!
 
 
2-4. UNION 연산자
- 두 개 또는 그 이상의 SELECT 문 들의 결과들을 하나의 결과로 단일화 시킴
    1) publisher에 대한 정상적인 사용자 입력 값: Wiley
        SELECT author, title, year FROM books WHERE publisher = ‘Wiley
 
    2) Publisher에 대한 조작된 사용자 입력 값 : Wiley’ UNION SELECT username, password, uid FROM users--
        SELECT author, title, year FROM books WHERE publisher = ‘Wiley’ UNION SELECT username, password, uid FROM users--
        -> 조작된 입력값을 넣어 두개의 select 문을 결합함 -> 획득해서는 안 될 정보를 얻어냄
 
- UNION 연산자를 이용해서 두 개의 SELECT 문의 결과를 결합할 때, 두 개의 결과는 같은 구조 (데이터 타입, 원소 개수) 이어야 함
- UNION 연산자를 사용할 때, 두 번째 SELECT 문의 원소의 개수를 알지 못하는 경우, NULL을 이용해서 개수를 파악할 수 있음
   -> 역시, 에러가 나지 않을 때까지 null값을 하나씩 늘려가며 개수 파악하는 방식
- 두 번째 SELECT 문의 원소의 타입을 알지 못하는 경우, NULL과 문자열을 이용해서 타입을 파악할 수 있음
   -> 에러가 나지 않을 때까지 문자로 된 값을 넣어보면서 데이터 타입을 파악하는 방식
 
 
3. 데이터베이스 인식
1) 동작 중인 데이터베이스 인식
2) 동작 중인 데이터베이스의 버전을 확인
3) 공격자의 맞춤형 공격 실시
 
 
4. 특수문자 인코딩
HTTP 요청 내에서 SQL injection 공격을 수행하고자 할 때, 특수 문자 (&, =, , +, ;) 들은 URL 인코딩 방식으로 인코드 되어야 함
& : %26
= : %3d
space : + 또는 %20
+ : %2b
; : %3b
 
 
5. 입력 필터 회피하기
[필터1] 블랙리스트를 작성하여 필터링
    - 회피1: SELECT 키워드가 블랙 리스트에 포함되면, 아래와 같이 입력함으로써 입력 필터링을 회피할 수 있음
                  SeLeCt
                  %53%45%4c%45%43%54
    -> 블랙리스트는 대소문자를 구분하기 때문에 SELECT라고만 포함해두면 필터링하지 않음, 그러나 SQL문을 처리할 때는 대소문자를 구분하지 않기 때문에 정상 처리됨.. => 공격자는 블랙리스트에 포함된 형태가 아닌 살짝 다른 형태로 회피할 수 있음!
 
    - 회피2: admin 키워드가 블랙 리스트에 포함되면, 아래와 같이 입력함으로써 입력 필터링을 회피할 수 있음
                Oracle: ‘adm’||’in’
                MS-SQL: ‘adm’+’in’
                MySQL: concat(‘adm’, ‘in’)
    -> 문자열을 쪼개서 입력하면 회피가 가능
 
[필터2] 애플리케이션이 사용자 입력 데이터에서 space를 제거함
    - 회피: 주석문 (comment) 을 사용해서, 사용자 입력 데이터 내에서 space를 사용하는 것과 같은 효과를 냄
              SELECT/*foo*/username,password /*foo*/FROM/*foo*/users
              SEL/*foo*/ECT username,password FR/*foo*/OM users
    -> 이렇게 중간중간 주석문을 넣어 입력하면 필터링을 회피할 수 있음
 
[필터3] 사용자 입력 값에 인용부호(')가 있을 때, 애플리케이션은 추가적인 인용부호를 기존 인용부호에 덧붙임
- 사용자 입력 값에 두 개의 인용부호가 있을 때, 인용부호 회피 마크로 인식돼서 무시됨
    - username에 admin'--을 입력하면
      SELECT * FROM users WHERE username = ‘admin’ ‘ --’ and password = ‘ ‘
      를 입력하면
      SELECT * FROM users WHERE username = ‘admin
      인용부호가 연속으로 존재하여 회피 마크로 인식하여 무시함 -> 에러
 
- 회피:
username 입력값에 대한 길이 제한(10개 문자)이 있다고 가정 -> username에 aaaaaaaaa’ / password에 or 1=1-- 입력
SELECT * FROM users WHERE username = ‘aaaaaaaaa’’ ’ and password = ‘ or 1 = 1--’
는 
SELECT * FROM users WHERE username = ‘aaaaaaaaa and password = ‘ or 1 = 1
로 인식되어
SELECT * FROM users WHERE 1 = 1
로 받아들여져서 모든 사용자 정보를 추출함
 
 
6. MySQL에서 SQL Injection 공격 예방
1) mysql_real_escape_string 함수
- 사용자 입력값에 대한 필터링 기능 수행
- 인자로 들어온 문자열중에서, \x00, \n, \r, \, ', ", \x1a 문자 앞에 역슬래쉬(\)를 덧붙임
- $username = mysql_real_escape_string($_GET['username']); 이 코드에서 john’이 들어왔다면, $username의 값은 john\’으로 설정됨
 
2) prepare, execute 함수 (지금까지 본 방어책 중 가장 좋은 방어책)
- 사용자가 입력한 데이터 값을 SQL 문의 일부가 아닌 별도의 파라미터로 취급해서 SQL Injection 공격 예방
(지금까지는 password에 대한 값을 입력하면 SQL문에 결합되어 처리되었었는데, 이 방식을 사용하면 전체 SQL문으로 보는게 아니라 password 그 자체의 값으로 보는 것이기 때문에 안전)
 
- 사용법: 
$password = $_GET['password'];
$str = $db->prepare("SELECT * FROM users WHERE username='${username}' AND password=?");
$result = $db->execute( $str, ${password});
*주의
- 반드시 password 뒤에 ?가 들어가는 형식! 일단 password 자체를 ?로 설정해두는 것.
- execute 사용 시 prepare한 결과물을 첫번째 인자, 파라미터로 받아온 password를 두번째 인자로 둬야 함!
 
=> 공격자가 조작된 입력값을 넣어도(주석문, ' 등을 포함하여도) 그 입력값 전체를 password의 "값"으로 인식하여 공격을 막는 방식이다.