서론
PHP 의 동적 타입 특성은 개발 편의성을 제공하지만, 특정 상황에서는 예상치 못한 보안 취약점을 야기할 수 있습니다. 특히 값의 비교 연산 과정에서 일어나는 암묵적인 타입 캐스팅 현상은 웹 애플리케이션에서 인증 우회나 입력값 검증 무효화 등의 문제로 활용되곤 합니다. 본 글에서는 이러한 느슨한 비교(Loose Comparison) 로직이 어떻게 작동하는지 기술적으로 분석하고, 실제 코드로 확인해봅니다.
1. 비교 연산자와 타입 조옮김 (Type Juggling)
PHP 에서 등호 연산자 (==) 는 좌우 피연산자의 타입이 상이할 경우, 비교 전 하나의 타입으로 자동 변환된 후 대등 여부를 판단합니다. 이를 느슨한 비교라고 부르며, 주로 숫자와 문자열 간의 비교에서 주의가 필요합니다.
공식 규격에 따르면, 양쪽이 수치형 문자열이거나 한쪽이 숫자라면 수치 기반으로 계산됩니다. 이때 문자열 내 포함된 숫자 패턴이 어떻게 파싱되는지 확인해야 합니다.
<?php
// 문자열 내 숫자 식별 로직 테스트
$testStr = "1024abc";
$valTarget = 1024;
if ($testStr == $valTarget) {
echo "일치\n"; // 문자열 앞부분 숫자로만 인식되어 참이 됨
} else {
echo "불일치\n";
}
// 비수치성 접두어가 있는 경우
$badStr = "hello123";
if ($badStr == 0) {
echo "0 으로 간주됨\n"; // php 버전差异에 따라 결과가 달라질 수 있음
}
위 예시처럼 문자열이 숫자로 해석될 때, 첫 번째 문자까지 읽다가 알파벳이나 특수문자가 발견되면 그 전까지의 숫자를 사용합니다. 또한 1e5 형식은 과학기수법으로 인식되어 실제 큰 정수로 변환됩니다.
2. 제어구조에서의 적용
switch 문도 내부에서 동일한 느슨한 비교 규칙을 따릅니다. 이 특징을 이용하면 변수의 타입을 다르게 전달하여 분기를 제어할 수 있습니다.
<?php
$input = $_REQUEST['code'];
switch ($input) {
case 0:
print 'A';
break;
case 1:
print 'B';
break;
default:
print 'Unknown';
}
// 사용자에게 "0a"와 같은 값을 입력받아도 숫자 0 으로 처리可能被함
3. 암호화 함수의 취약점: MD5 충돌
MD5 와 같은 해시 함수는 고정 길이 문자열을 반환합니다. 그런데 해시 결과물이 0e 로 시작하고 이후가 숫자만인 경우, PHP 는 이를 지수형태의 0 을 의미하는 것으로 잘못 해석할 수 있습니다.
예를 들어, 정상 비밀번호와 공격자 입력값의 MD5 가 서로 다르더라도, 둘 다 0e... 형식이면 == 연산 시 모두 0 으로 취급되어 일치 판정이 떨어질 수 있습니다.
이를 찾기 위한 검색 스크립트는 다음과 같이 구현할 수 있습니다.
import hashlib
import itertools
import random
def find_magic_hash(target_len=8):
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
for attempt in range(10000):
candidate = ''.join(random.choices(chars, k=target_len))
h = hashlib.md5(candidate.encode()).hexdigest()
# 0e 로 시작하며 뒤에 숫자가 붙는 구조인지 확인
if h.startswith('0e') and h[2:].isdigit():
return candidate, h
return None, None
result_str, result_hash = find_magic_hash()
print(f"Found: {result_str} -> {result_hash}")
이러한 특수한 해시값 조합을 사전에 확보하거나 실시간으로 생성하여 인증 절차를 통과시킬 수 있습니다.
4. 함수 호출 시 타입 강제 변환 이슈
strcmp() 는 원래 두 문자열을 비교하는 함수이지만, PHP 8 이전 버전에서는 배열 등이 전달되었더라도 경고를 출력하면서 0 을 반환하는 일이 있었습니다.
<?php
$a = "secret";
$b = $_GET['check'];
// b 가 배열([]) 이더라도 경고가 나오지만 return 0 일 수 있어 우회가 가능
if (strcmp($a, $b) === 0) {
// 엄격한 비교를 해야 안전하나 == 사용 시 위험
echo "Access Granted";
}
또한 배열 탐색 함수인 in_array() 와 array_search() 는 기본 옵션상 느슨한 비교를 사용합니다. 세 번째 인자로 true 를 주입하지 않으면 타입 구분을 하지 않고 값만 비교하게 됩니다.
5. 부울 값과 문자열의 관계
부울 타입인 true 를 문자열과 비교할 때는 주의가 필요합니다. PHP 에서는 true 와 빈 문자열이 아닌 대부분의 문자열 값을 비교했을 때 true 로 간주합니다.
<?php
$config = json_decode('{"is_admin": true}');
// true 와 "admin" 이 일치하는지 확인하는 로직
if ($config->is_admin == "admin") {
// true == "admin" 은 참으로 평가됨
echo "Admin Role";
}
이는 JSON 또는 직렬화된 데이터를 탈출하여 권한을 상승시키는 데 악용될 소지가 있습니다.
6. 안전한 비교를 위한 방안
위와 같은 문제를 근본적으로 해결하려면 타입과 값 모두를 일치시켜야 하는 삼중 등호 (===) 를 사용하는 것이 원칙입니다. 이 연산자는 타입 불일치가 발생하면 바로 false 를 반환하므로 타입 변형을 통한 우회를 차단합니다.
<?php
$value1 = "123";
$value2 = 123;
if ($value1 === $value2) {
echo "Match";
} else {
echo "No Match (String vs Integer)"; // 실행됨
}
7. 실습 문제 분석
다음과 같은 단순한 인증 코드가 있다고 가정해봅시다.
<?php
$id = $_GET['id'];
// id 가 숫자여야 하지만 1 과 비교 시
if (!is_numeric($id)) {
echo "Not Number";
} elseif ($id == 1) {
echo "Success";
}
is_numeric() 를 통과하지 않는 문자열임에도 불구하고 == 연산 시 숫자로 캐스팅되어 참이 되는 케이스를 만들어야 합니다. 예를 들어 1a 와 같은 값을 전송하면 숫자로 인식되지 않을 수 있으나, 맥락에 따라 캐스팅 논리가 다를 수 있으므로 정확히 테스트해야 합니다. 일반적인 CTF 환경에서는 1sj 처럼 앞뒤에 텍스트를 섞어 숫자 변환 시 앞부분만 취하도록 유도하기도 합니다.
다른 예시로 MD5 인증 우회를 요구하는 경우가 있습니다.
<?php
$target = md5('QNKCDZO');
$user_input = $_GET['inp'];
$h2 = md5($user_input);
if ($user_input !== 'QNKCDZO' && $target == $h2) {
echo "Flag";
}
여기서는 입력값이 원본과 같지 않아야 하며, MD5 값의 비교 시 0e 계열의 해시 충돌을 일으키는 문자열 (예: 240610708 등) 을 찾으면 조건을 만족할 수 있습니다.