mir.pe (일반/어두운 화면)
최근 수정 시각 : 2024-12-03 16:54:17

SQL injection

1. 개요2. 공격 방법
2.1. 방법 12.2. 방법 22.3. 블라인드 SQL 인젝션
3. 방어 방법4. 실제 사례


SQL injection, (공격자의) SQL 삽입

파일:RmfbEsZ.jpg
과속방지 카메라에 SQL injection 하기[1][2]


파일:xkcd sql ko.png
xkcd 327화 'Exploits of a Mom'
위의 만화에 대해 설명하자면, 저 학교에서 입력한 명령은 다음과 같을 것이다.
INSERT INTO Students (이름) VALUES (\'학생 이름');
여기서 "Robert'); DROP TABLE Students;--"학생을 "학생 이름" 자리에 넣을 경우 다음과 같은 명령문이 된다.
INSERT INTO Students (이름) VALUES (\'Robert');
DROP TABLE Students;
--');
첫 번째 줄에서는 Robert라는 학생이 입력되었지만, 두 번째 줄에서 학생들의 데이터가 있는 테이블을 제거한다. 그리고 세 번째에서는 뒤에 오는 내용을 모두 주석 처리한다. 따라서 이 명령문이 실행되면 최종적으로 모든 학생 기록이 삭제된다.

1. 개요

SQL 인젝션(SQL 삽입, SQL 주입으로도 불린다)은 코드 인젝션의 한 기법으로 클라이언트의 입력값을 조작하여 서버의 데이터베이스를 공격할 수 있는 공격방식을 말한다. 주로 사용자가 입력한 데이터를 제대로 필터링, 이스케이핑하지 못했을 경우에 발생한다. 공격의 쉬운 난이도에 비해 파괴력이 어마어마하기 때문에 시큐어 코딩을 하는 개발자라면 가장 먼저 배우게 되는 내용이다. 이러한 injection 계열의 취약점들은 테스트를 통해 발견하기는 힘들지만 스캐닝툴이나 코드 검증절차를 거치면 보통 쉽게 발견되기 때문에 탐지하기는 쉬운 편이다. 보안회사 Imperva가 2012년에 발표한 보고서에 따르면 월평균 4회가량의 SQL 인젝션 공격이 일어난다고 한다. OWASP에서도 수년 동안 인젝션 기법이 보안 위협 1순위로 분류되었던 만큼 보안에 각별한 주의가 필요하다.

너무나도 잘 알려진 취약점 공격이기 때문에 대부분의 서버는 SQL 인젝션에 대한 방어가 되어있다. 괜히 애먼 곳에서 이 공격을 시도하지 말자. 공격이 먹히지도 않고 본인의 IP만 차단당할 것이며, 설령 먹힌다 해도 법률에 따라 처벌받는다.

2. 공격 방법

2.1. 방법 1

로그인 폼에 아이디와 비밀번호를 입력하면 입력한 값이 서버로 넘어가고, 데이터베이스를 조회하여 정보가 있다면 로그인에 성공하게 된다. 이때 데이터베이스에 값을 조회하기 위해 사용되는 언어를 SQL이라고 하며, SQL문이 다음과 같이 작성되었다고 가정하자.
SELECT user FROM user_table WHERE id='입력한 아이디' AND password='입력한 비밀번호';

일반적인 유저라면 이렇게 로그인할 것이다.
아이디: 세피로트
비밀번호: 나무
SELECT user FROM user_table WHERE id='세피로트' AND password='나무';

그리고 SQL injection을 시도하려는 공격자가 다음과 같이 입력했다.
아이디: admin
비밀번호: ' OR '1' = '1
SELECT user FROM user_table WHERE id='admin' AND password=' ' OR '1' = '1';[5]
비밀번호 입력값과 마지막 구문을 자세히 살펴보자. 따옴표를 올바르게 닫으며 password=' '를 만듦과 동시에 SQL 구문 뒤에 ' OR '1' = '1'을 붙였다. WHERE 뒤에 있는 구문을 간단히 축약하면 false AND false OR true로 정리할 수 있는데, SQL 규정 상 AND 연산은 OR보다 연산 우선순위가 높기 때문에 해당 구문의 연산 값은 true가 되어 올바른 값으로 판단하고 실행하게 된다. 즉, 아이디와 비밀번호를 몰라도 ' OR '1' = '1 구문 때문에 로그인에 성공하게 되는 것이다.

이해가 어렵다면? 다소 풀어서 생각해보자.

먼저 사용자의 데이터를 저장하는 DB상에 기록된 데이터가 아이디: [세피로트, admin], 비밀번호: 나무만 있다고 생각하자.

여기서 아이디는 'admin'으로 쓰였으며 비밀번호는 ' OR '1' = '1로 쓰였다. 비밀번호를 분해해보면 실제로 입력된 비밀번호는 ' '가 입력됐으며 그 뒤의 OR '1' = '1은 연산자로 쓰이게 된다.

첫 번째 비교인 아이디와 비밀번호를 확인해보자.

아이디 admin이 DB에 있으니 True값이 출력되고 비밀번호는 ' '로 DB에 없어 False가 출력되어 결과적으로 A와 B값 모두가 참이어야 True가 나와야 하는 AND 논리연산자에 의해 결과값이 False가 나오게 된다.

이 결과값을 A라고 가정하자.

두 번째 비교인 OR '1' = '1을 확인해보자.

OR 연산자는 위에서 말했던 것처럼 A또는 B값 중 하나라도 True값이면 True값이 나오게 된다. '1'과 '1'은 서로 같기 때문에 값은 True가 나오게 된다. 이 결과값을 B라고 가정하자.

이후 AND연산을 먼저 하고 나온 A값(False)과 B(True)값을 OR연산으로 진행하게 되면 A값(False)과 B값(True)중 하나라도 참(True)이면 DB에서 정보를 가져온다. 라는 결과가 도출되게 된다.

login admin.asp라고 덕덕고에 쳐서 나오는 웹사이트 아무데나 1'or'1'='1을 넣으면 작동이 되는 사이트인 경우 작동이 된다!

로그인뿐만 아니라 JOIN이나 UNION 같은 구문을 통해 공격자가 원하는 코드를 실행할 수도 있다.

MySQL 기준으로 SQL injection이 되는지 확인하려면 ' 나 "를 로그인 창에 넣어서 SQL 에러를 뿜어내는지 확인하면 된다.
Warning: mysql_fetch_array():
Warning: mysql_fetch_assoc():
Warning: mysql_numrows():
Warning: mysql_num_rows():
Warning: mysql_result():
Warning: mysql_preg_match():

보통은 이렇게 나올 것이다.

2.2. 방법 2

로그인 폼도 결국엔 서버에 요청을 해서 받는 것이다. HTTP 헤더를 보면 응답 헤더에 서버의 종류와 버전이 나온다. Apache 서버 MySQL 서버, IIS는 MSSQL 같은 방식으로 데이터베이스의 종류를 추측할 수 있다. DB엔진을 알아내서 해당 시스템에 맞는 명령어를 이용해 데이터를 뽑아내거나 할 수 있다.

2.3. 블라인드 SQL 인젝션

에러 메시지가 정보가 아무런 도움이 되지 않거나 아예 에러 페이지를 보여주지 않을 때 사용한다. 대표적인 기술로는 시간 지연 공격이 있다. 간단하게 몇 초 정도의 time for delay를 이용해 원하는 시간만큼 데이터베이스가 움직여 준다면 취약하다고 볼 수 있다.

3. 방어 방법

아마도 XSS와 상당 부분 겹치겠지만 기본적으로 유저에게 받은 값을 직접 SQL로 넘기면 안 된다. 요즘에 쓰이는 거의 모든 데이터베이스 엔진은 유저 입력이 의도치 않은 동작을 하는 걸 방지하는 escape 함수와 Prepared Statement[6]를 제공한다. prepared statement 는 변수를 문자열로 바꾸는 것이라 안전하다.

또한 DB에 유저별로 접근 권한과 사용 가능한 명령어를 설정하면 최악의 경우에 SQL injection에 성공하였다고 하더라도 그나마 피해를 최소화할 수 있다. 혹자는 SQL injection은 데이터베이스 스키마를 알아야 가능한 공격기법이라고 하지만 스키마 구조를 몰라도 SQL injection을 사용하면 스키마 구조를 알아낼 수가 있다.[7] 그리고 데이터베이스를 변조하려는 게 아니라 파괴하려는 거라면 와일드카드 문자( * )를 사용해서 그냥 싹 다 지워버리는 공격이 가능.

DBMS마다 문법이 다르기 때문에 개발자가 그걸 다 고려해서 코딩하는 방법은 매우 비추천한다. 해당 언어나 프레임워크에서 제공하는 prepared statement라는 방법을 사용하는 게 최선. escape_string 같은 함수를 사용하면 몇몇 군데에서 빼먹거나 하는 실수로 보안 구멍이 생길 수 있다. 그리고 prepared statement는 사용 전에 일부 컴파일돼서 DB 쿼리를 가속시켜주므로 적극적으로 사용하자. 다만 컴파일하는 시간이 있다 보니 변수만 다른 같은 쿼리를 반복적으로 하는 작업에서야 유의미한 속도 향상이 있다. 프로시저(Procedure)라는 쿼리 캡슐화 기능을 쓰거나 최신 프레임워크에서 지원하는 ORM(Object Relational Mapping)을 사용해서 SQL에 직접 접근하지 않는 것도 방법이다(더불어 ORM은 DBMS를 타지 않으므로 DB를 변경해도 그에 따른 공수를 최소화 할 수 있다는 장점도 있다.).

추천되는 방어법은 클라이언트 측의 입력을 받을 웹 사이트에서 자바스크립트로 폼 입력값을 한 번 검증하고[8], 서버 측은 클라이언트 측의 자바스크립트 필터가 없다고 가정하고[9]한 번 더 입력값을 필터한다. 이때 정규표현식등으로 한번 걸러내고, SQL 쿼리로 넘길 때 해당 파라미터를 prepared statement로 입력받고 이를 프로시저[10]로 처리하는 게 좋다. 다음, 쿼리의 출력값을 한 번 더 필터하고( XSS 공격 방어의 목적이 강하다) 유저에게 전송한다. 이렇게 하면 해당 폼에 대해서는 SQL injection 공격이 완전히 차단된다. 물론 이것이 공격 기법의 전부가 아니므로 정보 유출에 민감한 사이트를 운영할 생각이라면 보안 회사의 컨설팅을 꼭 받아야 한다.

4. 실제 사례



[1] DROP DATABASE 구문은 데이터베이스를 삭제하는 명령어다. 물론 과속방지 카메라는 입력값으로 숫자와 로마자만 받을 테니 이게 먹힐 가능성은 거의 없다. 번호판 인식 프로그램을 짜봤다면 알겠지만 가장 먼저 확인하는 게 일반적인 번호판이 갖는 특정 가로세로 비율을 갖는 직사각형인데 이 비율도 한참 벗어났다. 차량은 르노 메간. [2] 다만 미국에서 이를 실제로 실행에 옮긴이가 있었는데, Joseph Tartaro라는 사람이 미국에서 자동차 번호판에 들어가는 번호를 지정할 수 있다는 점을 이용해서 자신의 차량 번호판을 ' NULL'로 지정 했었다. 실제로 이게 시스템에 문제를 일으키긴 했는데, 문제는 오히려 안 좋은 쪽으로 영향을 끼친 것. 경찰이 범칙금(티켓)을 발부 할 때 차량 번호를 기록하는 것을 까먹거나 모종의 사유로 기록할 수 없으면 그냥 빈칸으로 비워뒀는데, 이게 데이터 베이스에서 NULL로 처리되어 Joseph Tartaro에게 부과가 된 것이다. 그래서 주 전역에서 아무도 모르는 이의 교통 법규 위반 티켓을 받게 되었는데, 물론 DMV에 이를 알려 면제 받긴 했지만, 시스템 자체를 고치진 않아서 여전히 들어온다고 한다. # [해석] 입력한 아이디와 비밀번호가 데이터베이스 상에 저장된 아이디와 비밀번호와 같을 경우에만 user_table에서 user 데이터를 가져온다. [참고] 여기서는 패스워드를 평문으로 비교하는데 사실 이는 예제를 간단히 하기 위한 거고, 테스트가 아닌 일반 사이트에서 이렇게 운영하면 개인정보보호법 29조 위반(개인정보처리자는 개인정보가 분실·도난·유출·위조·변조 또는 훼손되지 아니하도록 내부 관리계획 수립, 접속기록 보관 등 대통령령으로 정하는 바에 따라 안전성 확보에 필요한 기술적·관리적 및 물리적 조치를 하여야 한다. (출처: 법제처 국가법령정보센터))으로 과태료 폭탄을 맞을 수 있다. 패스워드는 반드시 PBKDF2 이상의 보안성을 가지는 해시 함수로 해싱해야 한다. 대부분 SHA를 쓴다. [5] 여기서 and와 or은 '그리고, 또는'이 아닌 A와 B값이 모두 참이면 True(참 값), A와 B값 중 하나라도 특정 값이 참이면 True를 출력하는 연산자다. [6] 직역하면 '선처리 방식'으로, SQL 쿼리를 사전에 컴파일한 뒤 변수만 따로 집어넣어 실행하는 것이다. 해당 공격 방어기법에서 가장 중요한 기법으로 과거 정보보안기사 실기시험에 출제된 적이 있다. [7] 애초에 앞서 설명했듯 JOIN, UNION 구문을 통한 임의 코드 실행이 가능한 공격 방법이다. [8] 이때의 폼 검증은 사용자에게 인젝션에 필요한 특수문자의 사용이 불가능하다고 알리는 용도로, 방어방법과는 관련이 없다는 걸 유의하자. [9] 공격자가 브라우저에서 자바스크립트 사용을 끄거나 폼 페이지를 변조하면 그만이기 때문. [10] SQL 구문을 '함수' 꼴로 묶어 놓은 것이다.