본문 바로가기

웹 해킹

웹 해킹 공부 일기장 3 - 2 (Blind SQL Injection)

이때까지는 화면에 SQL 질의 결과가 출력될때 사용할 수 있는 SQL Injection 기법을 공부했습니다.

 

하지만, SQL Injection은 가능한데 결과가 화면에 출력이 되지 않는 경우도 있을 것입니다.

예를 들면? 로그인 페이지같은 경우에는 우리가 입력한 값으로 SQL 쿼리문을 만들어 결과를 처리한 후 성공 여부에 따라 다른 페이지를 보여주거나 다시 입력을 받을 것입니다.

 

이런 경우에 어떻게 데이터를 빼낼 수 있을까요??

바로 성공 여부에 따른 결과 차이를 통해 알아낼 수 있습니다.

 

음... 우리는 SQL Injection으로 우리가 원하는 질의를 실행시킬 수 있습니다.

이때! admin의 패스워드는 admin이 맞아? 이렇게 데이터베이스에 질문하여 맞으면 성공 결과 틀리면 틀렸을 때의 결과를 확인하여 admin의 패스워드가 admin이 맞는지 아닌지를 확인하는 것입니다.

즉, 데이터베이스와 스무고개를 한다고 생각하시면 됩니다.

 

하지만, 무작정 한단어를 맞추기는 어렵기 때문에 보통 비밀번호의 첫글자가 a가 맞아? 이렇게 질문하여 한글자씩 알아내어 결과적으로 원하는 전체 데이터를 추출하는 것입니다.

 

이것이 Blind SQL Injection의 원리입니다.

Blind SQL Injection도 다른 SQL Injection 처럼 절차를 따라가며 데이터를 추출하면 됩니다.

 

Blind SQL Injection Process

1. SQL Injection 포인트 찾기

2. SELECT 문 사용가능한지 확인

3. 공격 format 만들기

4. DB 이름 확인

5. Table 이름 확인

6. 컬럼 이름 확인

7. 데이터 추출

 

바로 실습하며 알아보겠습니다.

 

실습

 flag 찾기

 

1. SQL Injection 포인트 찾기

정상 사용

 

아이디를 입력하면 해당 아이디가 있는지 없는지 확인해주는 중복검사 페이지 입니다. (아이디가 없을 경우 존재하지 않는 아이디입니다.)

 

아이디의 존재 여부에 따라 출력되는 값이 다릅니다. 즉, 결과가 다른 것이죠

화면에 데이터베이스 SQL 결과가 출력되는 것이 아니기 때문에 앞서 배운 UNION SQLI 나 Error Based SQLI는 사용할 수 없습니다.

 

이때 사용할 수 있는 것이 Blind SQLI 입니다.

SQL Injection 사용 가능 여부

 

normaltic' and '1'='1을 입력해주니 똑같이 존재하는 아이디 입니다. 출력되는 것을 보니 우리가 입력한 SQL 문법이 정상 작동하는 것 같습니다. 그럼 이 부분은 SQL Injection이 가능한 것입니다.

 

앞부분에 설명을 못 했는데 혹시나 실제로 SQL Injection이 불가능한데 normaltic' and '1'='1 라는 아이디가 있을 경우에도 '존재하는 아이디입니다.' 라고 출력될 것입니다. 하지만 이러한 아이디는 존재하지 않겠죠...?

 

그렇기 때문에 SQL Injection이 가능하다고 판단할 수 있습니다.

 

2. SELECT 문이 사용가능한지 확인

select 가능 여부

 

아이디가 normaltic인 부분은 참이고 (select 'test') 하면 'test'가 되고 'test'='test'를 비교하기 때문에 and 뒷 부분도 참이되어 '존재하는 아이디입니다.' 가 출력됩니다.

거짓으로 만들때

 

뒷 부분을 거짓으로 만들면 '존재하지 않는 아이디입니다.'가 출력됩니다.

select가 정상적으로 작동되며 참과 거짓으로 결과가 다르다는 것 또한 확인이 가능합니다.

참과 거짓에 따른 결과가 다르기 때문에 Blind SQLI 를 사용하면 될 것 같습니다.

 

3. 공격 format 만들기

이제 우리가 데이터베이스와 스무고개를 하기위해 알아야할 것이 좀 있습니다.

우리는 데이터베이스에서 원하는 데이터의 첫글자가 a가 맞아? 이렇게 질문을 할 것입니다.

첫 글자를 가져오기 위한 함수가 있습니다. substr()함수입니다.

 

substr(문자열, index, length) 이렇게 사용하며 3개의 인자를 받아 첫번째로 받은 문자열에서 index위치(1부터 시작)부터 시작하여 length길이만큼 선택해줍니다.

- substr('test', 1, 1) => 't', substr('test', 2, 2) => 'es'

 

이 함수를 사용하여 우리는 원하는 데이터의 첫글자를 가져와 질문을 할 것입니다.

어떻게 하면 될까요?

substr((select 'test'), 1, 1) = 'a' 이렇게 하면 test의 첫 글자가 a 이냐고 물어보게 됩니다.

 

이렇게 한글자씩 뒤에 a를 바꿔가며 물어보면 되지만!! 문자가 많습니다. 이렇게 하나씩 물어보면 답도 없겠죠...? 

혹시 업 앤 다운 게임 아시나요?

한 사람이 1부터 100까지 숫자를 생각하면 다른 사람이 생각한 숫자를 맞추는 게임입니다.

이때 생각한 사람은 생각한 숫자가 말한 정답보다 업인지 다운인지 말해주면서 쉽게 생각한 숫자를 맞출 수 있게 해줍니다.

그럼 질문하는 사람은 아마? 중앙값인 50부터 질문하고 만약 업이면 50~100의 중앙값 75를 말할 것입니다.

 

이렇게 질문하면 최소한의 질문으로 정답에 가까워질 수 있습니다.

이것을 Binary Search(이진탐색)이라고 합니다. 우리는 이것을 사용하여 데이터베이스에 질문을 하며 최소한의 질문으로 데이터를 추출할 것입니다.

 

이진 탐색을 사용하려면 숫자로 하면 편하겠죠? 이때 사용할 수 있는 함수가 ord(), ascii()함수입니다.

이 함수들은 인자로 받은 문자를 아스키코드를 통해 숫자로 표현해줍니다.

아스키 코드표

 

아스키 코드표를 보면 사용할 수 있는 문자는 33~126 까지 입니다. 그렇기 때문에 이 값들의 중앙값 79보다 크냐 질문하면 됩니다.

그럼 조금더 빨리 데이터를 찾을 수 있습니다.

 

ord(substr((select 'test'), 1, 1)) > 79 이렇게 하면 되겠죠 그럼 test의 첫글자가 79(O) 보다 크냐고 물어보며

만약 맞으면 참이되니 '존재하는 아이디입니다.'가 출력되고 틀리면 '존재하지 않는 아이디입니다.' 가 출력될 것입니다.

 

 

t의 아스키코드는 116이니 참이 되어 존재하는 아이디라고 뜹니다.

 

이제 우리는 공격 format을 만들었습니다.

normaltic' and ord(substr((__SQL__), 1, 1)) > 79 #

 

위의 공격 포맷에서 SQL 부분에 원하는 데이터를 SELECT하는 구문만 넣어주면 됩니다.

 

4. DB 이름 확인

select database() 

 

데이터베이스의 한글자씩 알아내기전에 데이터베이스의 글자수가 몇글자인지 먼저 알아내면 좋겠죠?

length를 사용하면 글자수를 알아낼 수 있습니다.

select length(database())를 하면 데이터베이스의 글자수를 select하여 알 수 있습니다.

normaltic' and (select length(database())) > 10 #

 

데이터베이스의 글자수가 10글자 이상인지 확인합니다. 맞는지 여러번 확인하여 글자수를 알아낼 것이기 때문에 burp suite의 repeater 기능을 사용하면 조금 더 편할겁니다.

repeater 기능 사용

 

응답의 auto-scroll기능을 사용하여 '존재하는' 이라는 단어가 있으면 해당 문자가 있는 위치로 이동시켜줍니다. 

(auto-scroll기능은 응답부분의 아래쪽에 설정표시에 들어가면 있습니다.)

 

'존재하는' 이라는 단어가 응답에 없기 때문에 해당위치로 이동을 안합니다. 그럼 10보다 작다는 뜻이 되겠죠?

이렇게 그럼 5보다는 크니? 질문하고 ... 이렇게 계속 데이터베이스와 스무고개를 하면 됩니다.

제가 해보니 결과가 9가 나왔습니다.

 

데이터베이스의 길이는 9입니다. 그럼 이제 데이터베이스의 이름을 확인해보겠습니다.

우리가 만든 공격 format의 SQL 부분에 select database()를 넣어주겠습니다.

normaltic' and ord(substr((select database()), 1, 1)) > 79 #

데이터베이스 이름 확인

 

첫 글자가  79(O)보다 크니? 질문하니 '존재하는 아이디입니다.' 즉, 참이라는 소리입니다.

이렇게 계속 하다보니 첫 글자가 98(b)이라고 합니다. 

 

두번째 글자는 어떻게 알아낼까요?

normaltic' and ord(substr((select database()), 2, 1)) > 79 #

 

넵!! substr의 index부분을 2로 바꿔주어 두번째 글자를 선택해주고 똑같이 진행하면 됩니다.

데이터베이스의 글자수가 9이니 9까지 진행해주면 됩니다.

이렇게 전체 데이터 베이스를 확인해보니 'blindSqli' 라는 데이터베이스라고 합니다.

 

5. Table 이름 확인

이제부터는 데이터베이스 이름 구한 것과 똑같이 진행하면 됩니다.

먼저 테이블 이름의 글자수를 확인해보겠습니다.

normaltic' and (select length(table_name) from information_schema.tables 
where table_schema='blindSqli' limit 0,1) > 10 #

 

length(table_name)으로 테이블 이름의 글자 수를 select하겠습니다. 그리고 limit를 넣어주어 첫번째 테이블이름에 대하여 진행합니다.

 

첫번째 테이블 글자 수

 

5부터 계속 반복해보니 글자 수가 9인 것을 알아냈습니다.

 

이제 데이터베이스 이름 알아낸 것과 똑같이 첫번째 테이블 이름도 알아보겠습니다.

normaltic' and ord(substr((select table_name from information_schema.tables 
where table_schema='blindSqli' limit 0,1 ), 1, 1)) > 79 #

테이블 첫글자 구하기

 

첫 글자는 102(f)일 때 존재하는 아이디입니다. 즉, 참이니까 첫 글자는 f입니다.

 

두번째 글자를 알아내는 방법은 substr()의 인덱스 부분을 2로 바꿔주면 되겠죠?

normaltic' and ord(substr((select table_name from information_schema.tables 
where table_schema='blindSqli' limit 0,1 ), 2, 1)) > 79 #

 

이렇게 인덱스 부분을 글자수가 9이니 9까지 계속 반복하면 됩니다.

 

해보니 테이블 이름은 'flagTable' 이 나옵니다.

아마 flag는 이 테이블에 있지 않을까요? 만약 이상한 다른 테이블이 나왔다면 limit 1,1로 바꾸어 두번째 테이블에 대하여 똑같이 글자수 알아내고 한 글자씩 알아내어 테이블이름을 또 구해야 했을 것입니다.

 

6. 컬럼 이름 확인

이제 컬럼 이름 확인도 어떻게 하는지 바로 아실겁니다.

먼저 글자수부터 구해볼까요?

normaltic' and (select length(column_name) from information_schema.columns 
where table_name='flagTable' limit 0,1) > 5 #

첫번째 컬럼 글자 수 확인

 

첫번째 컬럼의 글자수는 3입니다. 제 생각엔 flag 라는 컬럼에 있을 것 같긴한데 일단 첫번째 컬럼 이름 확인해보겠습니다.

normaltic' and ord(substr((select column_name from information_schema.columns 
where table_name='flagTable' limit 0,1 ), 1, 1)) > 79 #

 

역시 첫번째 컬럼의 이름은 idx 였습니다. 여기는 flag가 없을 것 같으니 두번째 컬럼 확인해보겠습니다.

normaltic' and ord(substr((select column_name from information_schema.columns 
where table_name='flagTable' limit 1,1 ), 1, 1)) > 79 #

 

두번째 컬럼 이름은 flag입니다.

여기에 아마 flag가 있을 것 같습니다.

이제 마지막!! 원하는 데이터를 추출하러 갑시다!!

 

7. 데이터 추출

 

일단 flag의 글자 수부터 알아봅시다.

normaltic' and (select length(flag) from flagTable limit 0,1) > 5 #

flag 글자 수 확인

음... 일단 글자수가 33글자네요 언제하지....

flag의 시작은 segfault{ 로 시작할 것이니 9글자까지 확인 저 문자가 맞는지 확인하겠습니다.

normaltic' and substr((select flag from flagTable limit 0,1 ), 1, 9) = 'segfault{' #

 

이렇게 작성하면 flag의 처음부터 9번째 글자까지 해당 문자열이 맞는지 비교해줄 것입니다.

시작부분 확인

 

일단 참을 반환하는 것 보니 segfault{ 로 시작하는 것은 맞습니다.

우리는 이제 substr로 10번째 글자부터 확인하면 됩니다.

normaltic' and ord(substr((select flag from flagTable limit 0,1 ), 10, 1)) > 79 #

 

진짜 33글자 다 일일이 확인했습니다...ㅠㅠ 

 

결국 flag 찾기 성공!!!

 

우리는 진짜 수작업으로 데이터 추출을 처음부터 끝까지 했습니다.

사실 보시다시피 한글자씩 알아내는 Blind SQLI 를 직접 수작업으로 하긴 힘듭니다. 시간도 많이걸리고...

하지만 이것을 파이썬으로 자동화 코드를 짜서 실행할 수 있습니다.

자동화 코드 짜서 하는 것은 과제하면서 해보겠습니다. 그리고 마지막엔 이번 실습 문제도 같이 자동화하여 해보겠습니다.

 

이렇게 수작업으로 해보면서 Blind SQLI이 어떤 원리로 실행되는 지 정확히 이해할 수 있을  것 같습니다.

 

사실은 한 번은 꼭 이렇게 직접 해보라고 해서 했습니다....

 

또한, Blind SQLI 는 화면에 출력되지 않을 경우 뿐만 아니라 화면에 데이터를 출력하는 경우에도 충분히 사용할 수 있습니다.

즉, 어떠한 페이지든 SQL Injection만 가능하다면 사용할 수 있습니다.

하지만, 자동화를 하더라도 다른 SQL Injection 기법에 비해 시간이 오래 걸립니다. 

 

그렇기 때문에 모든 상황에 Blind SQL Injection을 사용하는 것이 아닌 다른 기법을 사용할 수 있으면 해당 기법을 사용하고 다른 기법들을 사용할 수 없을 때, 최후의 수단으로 사용하는 것이 좋습니다.

 

이렇게 이번 주차에는

데이터베이스 쿼리 실행하면서 생기는 에러 메시지를 출력할 경우 사용하는 Error Based SQL Injection

데이터베이스 쿼리 실행 결과가 화면에 출력되지 않을 경우 사용하는 Blind SQL Injection

을 알아보았습니다.

 

과제를 진행하며 이번 주차를 마무리 하겠습니다.

 

과제 

1. CTF 문제 풀기

+ 웹 개발

 

웹 개발 이어서 게시판을 만들긴했는데 솔직히 너무 별로인 것 같아 다시 만들고 좀 꾸며서 올리겠씁니다.