보이지 않는 손 같이 멋지고 섹시한 네이밍센스는 이과, 공대에서 찾기 힘들다. 대부분 이름이 내용이거나 만든사람 이름이다.
SQL Injection이란 악의적인 사용자가 보안상의 취약점을 이용하여, 임의의 SQL 문을 주입하고 실행되게 하여 데이터베이스가 비정상적인 동작을 하도록 조작하는 행위이다.
-- tmi
OWASP(Open Web Application Security Project)라는 프로젝트가 있다.
애플리케이션 보안에만 전념하는 여러 커뮤니티 그룹의 조직이 있는데 이중 가장 큰 그룹이 OWASP이다.
주로 웹에 관한 정보노출, 악성 파일 및 스크립트, 보안 취약점 등을 연구하며, 2013년, 2017년, 2021년 10대 웹 애플리케이션의 취약점 OWASP TOP 10을 발표했다. 선정 기준은 웹 애플리케이션 취약점 중에서 공격 빈도가 많고, 보안상 영향을 크게 줄 수 있는 가성비 좋은 취약점들이다. 13년과 17년에선 1위, 21년엔 3위를 기록한 취약점이 SQL Injection이다.
실제로 2017년 여기어때도SQL Injection으로 인한 개인정보 유출사건이 발생했고 피해가 엄청났다고 한다.
아무리 가성비 좋은 SQL Injection이라고 해서 아무거나 생각나는데로 쿼리를 막 넣는게 아니다.
여러가지 방법, 종류가 있는데 종류에 대해 알아보고 대응 방안에 대해 알아보자
SQL Injection 종류
Error based SQL Injection (논리적 에러를 이용한 SQL Injection)
논리식에 허점을 이용한 SQL Injection이다. 가장 많이 쓰이며, 대중적인(?) 공격기법이다.
아래는 일반적으로 로그인할 때 가장 많이 사용되는 sql 구분이다.
SELECT * FROM Useres WHERE id='input1' and password = 'input2'
여기서 사용자가 input1에 아래와 같은 인풋을 넣는다고 가정해보자.
' OR 1=1 --
input1을 처리한 쿼리는 아래와 같다.
SELECT * FROM Users WHERE id='' OR 1=1 -- ' AND password = 'input2'
--를 이용하여 input1 뒤의 문장을 전부 주석처리하고, OR 1=1을 넣음으로서 해당 조건은 모두 True가 된다.
따라서 위의 쿼리는 아래의 쿼리와 결과가 항상 같다.
SELECT * FROM Users
매우 간단한 input1인데 결론적으로 Users 테이블에 있는 모든 정보를 조회하게 되고, 가장 먼저 만들어진 계정으로 로그인에 성공하게 된다. 심지어 대부분 관리자 계정을 가장 처음 만들기 때문에 계정에 관리자 계정으로 로그인 될 확률이 크다.
Union based SQL Injection (Union 명령어를 이용한 SQL Injection)
SQL에서 Union 키워드는 두개의 쿼리문에 대한 결과를 통합해서 하나의 테이블로 보여주게 하는 키워드이다.
정상적인 쿼리문에 Union 키워드를 사용하여 인젝션하면, 원하는 쿼리문을 실행할 수 있게 된다.
설명보다 쿼리부터 확인하는게 이해하기 쉬워보인다.
-- 게시글 조회
SELECT * FROM Board WHERE title LIKE '%INPUT%' OR contents '%INPUT%'
위와 같은 쿼리가 있다. 게시글의 제목이나 내용에 input과 같은게 있으면 보여달라는 검색하는 쿼리이다.
여기서 사용자가 INPUT으로 아래와 같은 쿼리를 넣는다고 가정해보자.
'
UNION
SELECT null,id,passwd FROM Users --
INPUT을 처리한 쿼리는 아래와 같다.
SELECT * FROM Board WHERE title LIKE '%'
UNION
SELECT null,id,passwd FROM Users -- %' OR contents '%INPUT%'
이 쿼리문의 수행 결과는 보드의 타이틀과 아무거나 매치되는 결과와 유저의 id와 passwd를 함께 보여주는 테이블이 보여지게 된다. 인젝션이 성공하게 된다면, 사용자의 개인정보가 게시글과 함께 화면에 보여지게 될 것이다.
물론 패스워드를 평문으로 데이터베이스에 저장하지는 않겠지만 인젝션이 가능하다는 점에서 보안 위험에 노출되어 있다.
예시에서 null을 넣어주었는데 UNION 연산은 컬럼 수를 맞춰줘야하기때문에 null을 넣었다.
컬럼수, 테이블의 컬럼 이름 등 테이블의 구조를 필요로하는 경우가 많은데 이또한 SQL Injection으로 획득이 가능하다.
Blind SQL Injection (Boolean based SQL)
데이터베이스로부터 특정한 값이나 데이터를 전달받지 않고, 단순히 참과 거짓 정보만 알 수 있을때 사용한다.
로그인 폼에 SQL Injection이 가능하다고 가정했을때 서버가 응답하는 로그인 성공과 로그인 실패 메세지를 이용하여, DB의 테이블 정보 등을 추출할 수 있다. 즉, 사용자의 데이터보단 데이터베이스, 테이블의 정보를 알아내는 방법이다.
다시 한 번 로그인할때 자주 사용되는 SQL을 보자
SELECT * FROM Users WHERE id = 'input1' AND password = 'input2'
그리고 역시 input1에 내가 원하는 sql을 삽입한다.
abc123' AND ASCII(SUBSTR(SELECT name FROM information_schema.tables WHERE table_type='base table' limit 0,1)1,1)) > 100 --
위와같은 긴 인풋을 넣는다. (MySQL 가정)
사용자는 임의로 회원가입한 abc123이라는 아이디와 함께 주입한다. AND 뒤에 오는 SELECT 구문은 MySQL에서 테이블명을 조회하는 구문으로 limit키워드를 통해 하나의 테이블만 조회하고, SUBSTR 함수로 첫글자만, 마지막으로 ASCII를 통해서 ascii값으로 변환해준다. 만약에 조회되는 테이블명이 Users라면 'U'자가 ascii값으로 조회될 것이고, 뒤의 100이라는 숫자 값과 비교하게 된다. 거짓이면 로그인 실패가 될 것이고 참이라면 로그인에 성공할 것인데, 참이 될 때 까지 뒤의 100이라는 숫자를 변경해가며 비교한다. 공격자는 이 프로세스를 자동화 스크립트를 통해 단기간내에 테이블 명을 알아낼 수 있다.
Blind SQL Injection (Time based SQL)
얘도 마찬가지로 서버로부터 특정한 응답대신 참, 거짓의 응답을 통해서 데이터베이스의 정보를 유추하는 기법이다.
사용되는 함수는 MySQL 기준 SLEEP과 BENCHMARK이다.
다시 로그인하는 SQL
SELECT * FROM Users WHERE id = 'input1' AND password = 'input2'
삽입할 쿼리
abc123' OR (LENGTH(DATABASE())=1 AND SLEEP(2)) --
-- SLEEP이 치환처리 되어있다면 BENCHMARK() 함수 사용
abc123' OR(LENGTH(DATABASE())=1 AND BENCHMARK(1000000,AES_ENCRYPT('hello','goodbye'));
위의 쿼리는 데이터베이스의 길이를 알아내는 방법으로 SLEEP(2)가 동작할때까지 (LENGTH(DATABASE())=1에서 숫자 1을 변경해가며 시도한다. (AND연산 특성상 LENGTH(DATABASE())가 1이라면 SLEEP(2)가 동작할것이고 아니라면 동작하지 않는다.)
만약 SLEEP이라는 단어가 치환처리 되어있다면 또다른 방법으로 BENCHMARK나 WAIT함수를 사용할 수 있다.
저장 프로시저(Stored Proceduere)은 일련의 쿼리들을 모아 하나의 함수처럼 사용하기 위한 것이다.
공격에 사용되는 대표적인 저장 프로시저는 MS-SQL에 있는 xp_cmdshell로 윈도우 명령어를 사용할 수 있게 된다. 단, 공격자가 시스템 권한을 획득해야 하므로 공격난이도가 높으나, 공격에 성공한다면 서버에 직접적인 피해를 입힐 수 있는 공격이다.
Mass SQL Injection (다량의 SQL Injection 공격)
기존 SQL Injection과 달리 한번의 공격으로 다량의 데이터베이스가 조작되어 큰 피해를 입히는 것을 의미한다.
데이터베이스에 악성스크립트를 삽입하고, 사용자들이 변조된 사이트에 접속 시 좀비 PC로 감염되게 하며 해당 좀비 PC들을 DDos공격에 사용한다.
대응 방안
입력값에 대한 검증
SQL Injection에서 사용되는 기법과 키워드는 엄청나게 많다.
사용자의 입력값에 대한 검증이 필요한데, 서버단에서 화이트리스트 기반으로 검증해야한다. 즉, 가능한 키워드를 리스트로 만들어 검증해야한다. 불가능한 키워드를 리스트로(블랙리스트) 만들어 검증할 경우 보다 많은 키워드를 등록해야하며, 하나라도 빠지면 공격 당할 수 있기 때문이다.
여기서 화이트리스트에 포함되지 않거나, 블랙리스트에 포함된다면, 문자를 치환하는 방법을 많이 쓴다. 이때 공백으로 치환하는 경우가 종종있는데 공백으로 치환하는 방법은 취약한 검증 방법이다.
예를들어 공격자가 SESELECTLECT 를 입력한다면 중간의 SELECT가 공백으로 치환되며 다시 SELECT가 완성된다. 이러한 경우의 수를 전부 처리하기보다 공백대신 공격키워드와는 의미없는 단어로 치환하는 방법이 적절하다.
Prepared Statement 구문 사용
Prepared Statement 구문을 사용하게 되면, 사용자의 입력 값이 데이터베이스의 파라미터로 들어가기 전에 DBMS가 미리 컴파일 하여 실행하지 않고 대기한다.
그 후 사용자의 입력값을 문자열로 인식하게 하여 공격쿼리가 들어간다하더라도, 사용자의 입력은 의미 없는 단순 문자열이 된다.
SELECT * FROM Users WHERE id = 'input1' AND password = 'input2'
에서 input1과 input2를 처리한 쿼리문을 입력받은 뒤 컴파일하는것이 아니라, 쿼리문을 미리 컴파일해놓고, input1과 input2를 처리한다면 뒤의 내용이 -- 를 통해 주석이 될 일도, 다른 쿼리문이 삽입 될 일도 없다.
괜히 교수님이 prepared statement쓰라는게 아니다. 이내용과 아래 Error Message 노출금지 내용이 없었으면 아마 SQL Injection말고 주제 하자고 했을 수도 있다.
Error Message 숨기기
공격자가 SQL Injection을 수행하기 위해서는 데이터베이스의 정보(테이블 명, 컬럼명 등)가 필요하다. DB에서 에러발생시 따로 처리해주지 않았다몀ㄴ, 에러가 발생한 쿼리문과 함께 에러에 관한 내용을 반환해주는데 이 내용을 바로 보여준다면 테이블 명, 컬럼명, 쿼리문이 노출될 수 있다. 따라서 사용자에게 보여줄 수 있는 페이지를 별도로 만들거나, 메세지박스를 따로 만드는것이 좋다.
트랜잭션이란 ACID를 만족하여 무결성을 보장하며 데이터의 상태를 바꾸는 작업의 단위이다. 예를들어 INSERT, DELETE, UPDATE 등 DML은 트랜잭션으로 볼 수 있다. DML은 데이터의 상태를 변경하고, DB는 자동으로 Commit을 실행하여 변경된 내역을 데이터베이스에 반영한다. 실패한다면 데이터의 상태가 변경되기전으로 돌아가는 작업인 Rollback을 수행한다.
트랜잭션의 이기적인 부분은 ACID의 Isolation이다.
Isolation 조건을 만족하기 위해 트랜잭션은 동시에 접근하는것을 제어하고 여기서 데드락이 발생할 수 있다.
트랜잭션하면 흔히 나오는 예시로 입출금이 있다. 내 계좌에서 돈을 빼고(UPDATE) 뺀만큼 상대방 계좌에 돈을 넣는(UPDATE) 작업은 UPDATE 두 개가 모두 성공한 뒤 Commit해야한다. 둘 중 하나라도 실패할 경우 Commit되지 않고 트랜잭션이 시작하기전으로 Rollback한다. 반드시 둘 다 성공해야만 Commit이 진행된다.
여기서 추가로 Isolation(고립성, 독립성)을 위한 예시를 들어보면,
동언이형은 전재산이 1000원이다. 난 동언이형한테 3000원을 송금했다. 그리고 안채는 데이터베이스에 내 송금작업이 Commit되기전에 동언이형한테 5000원을 송금했다.
내 돈을 입금하며 동언이형의 계좌에 잔고를 (1000원 + 3000원)으로 UPDATE한다.
그리고 Commit되기 전에 안채가 동언이형한테 5천원을 송금했다.
이때 안채가 송금작업을 수행할 당시 바라본 동언이형의 잔고는 천원이고 (1000원 + 5000원)으로 UPDATE하게 된다. -> Dirty Read
내 송금작업에 대해 Commit을 진행한다. ( 동언이형 잔고 1000원 + 3000원 )
안채의 송금작업에 대해 Commit을 진행한다. ( 동언이형 잔고 1000원 + 5000원 ) -> 내 송금작업에 대한 갱신손실
여러 트랜잭션이 동시에 접근하는 것을 제어하기 위해 데이터를 고립시킨 뒤 순차적으로 실행하는직렬 스케줄,혹은직렬 가능한 스케줄로 만들어야한다.
직렬 스케줄 : 하나의 트랜잭션이 실행되면, 해당 트랜잭션이 완료되어야만 다른 트랜잭션이 실행될 수 있다. 비직렬 스케줄 : 트랜잭션을 병행수행한다. 기존 트랜잭션이 진행중이더라도 다른 트랜잭션이 실행 될 수 있다. 직렬 가능한 스케줄 : 서로 영향을 주지 않는 직렬 스케줄을 비직렬적으로 수행한다.
즉, 다중사용자환경의 DBMS에서는동시성(병행수행)을 최대한 보장하면서 직렬스케줄과 동일한 결과를 가지는 직렬 가능한 스케줄로 만드는 방법이 가장 좋다.
직렬 스케줄
읽기 연산만 수행한다면, 상호 간섭이 발생하지 않고 연산의 순서도 중요하지 않다.
같은 데이터 항목에 접근하지 않는다면, 상호 간섭이 발생하지 않고 연산의 순서도 중요하지 않다.
트랜잭션1, 트랜잭션2가 같은 데이터 항목에 접근하여 트랜잭션 1은 쓰기 연산을 하고 트랜잭션2가 읽기 또는 쓰기 작업을 수행한다면 상호 간섭이 발생하고 연산의 순서가 중요하다.
LOCK
트랜잭션들이 동일한 데이터 항목에 대해 임의적 병행 접근을 하지 못하도록 제어하는 방법이다.
Lock연산은 아래와 같은 조건들을 지키며 수행된다.
트랜잭션 T가 데이터 항목 D에 대해 READ(D) or WRITE(D)연산을 수행하려면 반드시 LOCK(D) 연산을 수행해야한다.
트랜잭션 T가 실행한 LOCK(D)에 대해서는 트랜잭션 T가 종료되기 전 반드시 UNLOCK(D)연산을 수행해야한다.
트랜잭션 T는 다른 트랜잭션T2에 의해 이미 LOCK(D)이 걸려있는 D에 대해 다시 LOCK(D)를 수행하지 못한다. ->(S-LOCK끼리는 가능)
트랜잭션 T가 데이터항목 D에 대해 S-LOCK(D)을 설정하면 T는 읽기 연산만 가능하다.
하나의 데이터 항목에 대해 여러개의 트랜잭션이 Shared Lock 할 수 있다.
트랜잭션 T1이 데이터항목 D에 대해 S-LOCK(D)을 설정한 경우에, 트랜잭션 T2도 데이터항목 D에대해 S-LOCK(D)를 설정할 수 있다.
S-LOCK(D)가 걸려있다면 S-LOCK을 건 트랜잭션 포함 다른 트랜잭션에서도 읽기연산은 수행가능하다.
트랜잭션 T1이 데이터항목 D에 대해 S-LOCK(D)를 설정한 경우, T1을 포함한 다른 트랜잭션들도 읽기 작업이 수행가능하다. (쓰기작업은 불가능하다.)
배타락 Exclusive-LOCK(X-LOCK)
X-LOCK을 설정한 트랜잭션T1은 데이터항목 D에 대해 읽기와 쓰기 연산 모두 가능하다.
하나의 데이터항목에대해 하나의 트랜잭션만 X-LOCK을 걸 수 있다.
동시에 여러개의 트랜잭션이 X-LOCK을 걸 수 없다.
트랜잭션 T1이 X-LOCK(D)을 걸었다면 T1이 UNLOCK(D)하기 전에 T2에서는 X-LOCK(D)를 수행할 수 없다.
T2는 D에대해 X-LOCK(D)작업이 불가능하다.
잠금규칙
트랜잭션은 READ(D) 연산을 실행할 때 S-LOCK(D)이나 X-LOCK(D) 중 하나를 실행해야한다.
WRITE(D)를 수행하려면 X-LOCK(D)를 실행해야한다.
어떤 LOCK이든, 어떤 연산이든 작업 종료 후에는 UNLOCK(D) 연산을 실행해야 한다.
UNLOCK(D)는 S-LOCK(D) 혹은 X-LOCK(D)가 수행된 후에만 실행될 수 있다.
즉, LOCK 없는 UNLOCK없고 UNLOCK없는 LOCK없다.
잠금단위
잠금단위란 잠금의 대상이 되는 데이터 객체의 크기로 레코드의 필드값, 레코드, 물리적 입출단위인 디스크부터 테이이나 데이터베이스까지 하나의 잠금 단위로 설정할 수 있다.
잠금 단위가 클수록 동시성(병행성)수준은 낮아지고, 동시성 제어 기법이 간단해진다.
잠금 단위가 작을수록 동시성(병행성)수준은 높아지고, 동시성 제어 기법이 복잡해진다.
따라서 여러개의 잠금 단위를 설정하고 필요에따라 사용할 수 있어야한다.
교착상태, 데드락
드디어 교착상태, 데드락이다. 이걸위해 LOCK이고 트랜잭션이고 설명했다.
LOCK은 트랜잭션의 고립성을 만족시키기 위해, 또 동언이형의 3천원을 지키기 위해 꼭 필요하다.
따라서 LOCK은 대부분의 DBMS에서 사용하는 방식이다. 하지만 큰 한계가 존재한다.
트랜잭션 TA가 x에 X-LOCK를 걸고, 해당 LOCK이 끝나기 전에 y에도 X-LOCK(y)를 걸려고 한다.(하나의 트랜잭션이 여러개의 데이터에 접근하는 상황)
하지만 y는 트랜잭션 TB가 S-LOCK을 걸어놓은 상태고 트랜잭션 TB는 y에 대한 S-LOCK(y)를 끝내기 전에 데이터 x에 S-LOCK(x)를 걸려고 한다.
x는 TA가, y는 TB가 LOCK을 걸어놨고, TA는 y에대한 UNLOCK을, TB는 x에대한 UNLOCK을 기다린다. 트랜잭션 둘 중 하나가 양보하지 않으면 한없이 기다리며 이때 교착상태, 데드락이 발생한다.
Isolation을 만족시키지 않으면 동언이형은 3천원을 도둑맞고 Isolation을 만족시키기위해 LOCK을 걸자니 교착상태, 데드락이 발생할 수 있다.
위의 예시 상황은 데드락의 네가지 조건들을 만족한다.
상호배제 : 데이터의 한 항목에대해 함께 작업할 수 없고 독점적으로 사용한다.
점유대기 : 트랜잭션은 데이터에 LOCK을 걸고 다른 데이터의 UNLOCK을 기다린다.
비선점. : 트랜잭션은 다른 트랜잭션이 걸어놓은 LOCK을 UNLOCK할 수 없다.
환형 대기 : 각 트랜잭션이 서로 순환적으로 다른 트랜잭션의 UNLOCK을 기다린다.
네가지 조건 모두 만족해야 데드락이 발생하고 이중 하나라도 만족하지 않으면 데드락이 발생하지 않는다.
데이터베이스에서 데드락 해결하기
DB에서 데드락을 해결하는 방법으로는 4가지 조건 중 하나라도 만족하지 않게 애초에방지하거나, 교착상태가 발생할거 같으면 LOCK하지 않고 회피하거나, 교착상태가 발생하든말든 냅두다가 발생하면탐지하고 회복하는 방법이 있다.
방지기법
각 트랜잭션이 실행되기 전 미리 필요한 모든 데이터를 LOCK한다.
위의 예시와는 다르게 TA는 시작할때 필요한 데이터를 파악하고, x와 y에 모두 LOCK을 실행한다. 그럼 중간에 TB가 y에 LOCK을 시도할때 TA의 UNLOCK을 기다려야하고 TA는 y에대한 데이터를 사용한뒤 UNLOCK하면 TB가 사용할 수 있다. 따라서 데드락을 애초에 방지할 수 있다.
하지만 필요한 모든 데이터를 LOCK해야 하므로 병행성이 떨어진다. 즉, 현재 y데이터는 사용하지 않고 x에 대한 작업만 수행하고 있음에도 y에 접근할 수 없다.
SET LOCK_TIMEOUT문을 통해 일정시간이 지나면 트랜잭션, 쿼리를 취소한다.
데드락인 데이터가 있다면 그 데이터에 대한 작업은 오랜시간 대기하게 될 것이므로, 일정시간이 지나면 취소하는 방법이다.
간단하게 방지할 수 있지만 일정시간 기다려야하므로 근본적인 해결책이 될 수 없다.
회피기법
데이터에 LOCK을 실행할 때 Time Stamp를 활용해서 교착상태가 일어나지 않도록 회피하는 방법이다.
방지기법에서의 단점들때문에 실제로는 회피기법이 주로 사용된다.
Wait-Die방식
트랜잭션 A가 트랜잭션 B에 의해 잠금된 데이터를 요청할 때 트랜잭션 A가 먼저 실행된 트랜잭션이라면 B가 끝날때까지 대기한다.
트랜잭션 A가 트랜잭션 B에 의해 잠금된 데이터를 요청할 때 트랜잭션 A가 늦게 실행된 트랜잭션이라면 포기하고 나중에 다시 요청한다.
Wound-Wait방식
트랜잭션 A가 트랜잭션 B보다 먼저 실행된 트랜잭션이라면, 데이터를 선점(Wound)한다.
트랜잭션 A가 트랜잭션 B보다 늦게 실행된 트랜잭션이라면, B가 끝날때까지 대기한다.
낙관적 병행 제어 기법
낙관적 병행 제어 기법은 트랜잭션이 실행되는 동안 검사를 수행하지 않고 트랜잭션의 계산이 마무리 된 후에 데이터에 문제가 있다면 RollBack, 없다면 commit 하는 방법이다.
낙관적 병행 제어 기법에서는 판독 -> 확인 -> 기록 단계를 따르는데 확인 단계를 거친 트랜잭션만 기록 단계를 수행할 수 있다.
다시 동언이형의 사라진 3천원을 보자.
내 잔고 + 동언이형 잔고 + 안채 잔고의 값은 일정해야한다. 총합에서 3천원이 사라진것은 문제고 이러한 문제는 판독 -> 확인 과정에서 문제가 발생시킨다. 그럼 RollBack되고 내 잔고에 다시 3천원이 돌아올 것이다.
정확히 표현하면 트랜잭션은 다른 사본을 만들어 관리하고, 트랜잭션에서 갱신(UPDATE)은 사본에 대해 실행한다. 그리고 확인단계에서 트랜잭션 실행 결과가 직렬 가능성위반을 체크한다. 예를 들어 동시에 쓰기작업을 진행했다면, 문제가 있는 작업이다. 쓰기작업이 동시에 이뤄지지 않았다면 확인단계를 통과하고 Commit하며, 확인단계를 통과하지 못했다면 RollBack한다.