[🥉 Bronze 2] 비밀번호 대신 배열 하나 — DreamHack [wargame.kr] strcmp 풀이 — ZINO
2026-07-07·1분 읽기·
[🥉 Bronze 2] 비밀번호 대신 배열 하나 — DreamHack [wargame.kr] strcmp 풀이
매 요청마다 rand()로 다시 뽑는 비밀번호를 strcmp()로 비교한다. sleep(1)까지 붙여 브루트포스를 차단했지만, 애초에 값을 맞힐 필요가 없다. PHP의 strcmp()에 배열을 넘기면 TypeError 대신 NULL을 반환하고, NULL == 0은 PHP 느슨한 비교에서 참이 된다. password 대신 password[]를 보내는 것만으로 인증을 통째로 우회한다.
echo "Congratulations! Flag is <b>" . $FLAG . "</b>";
exit();
} else {
echo "Wrong password..";
}
}
$password는 요청이 올 때마다 rand()를 네 번 굴려 새로 만든 값을 md5 → sha1로 감싼다. 저장돼 있지도, 재사용되지도 않는다. 게다가 sleep(1)까지 박아 초 단위 이상 걸리게 만들었다 — 브루트포스를 정면으로 차단하려는 의도가 주석에도 그대로 적혀 있다("do not brute force!"). 실제로 아무 값이나 넣으면 1초 뒤 "Wrong password.."가 뜬다.
임의의 비밀번호 — 1초 뒤 Wrong password..
이 설계 자체는 훌륭하다 — 매번 바뀌고, 저장도 안 되는 값을 브루트포스로 맞히는 건 불가능하다. 문제는 그 값을 비교하는 함수 쪽에 있다.
💣 핵심 — strcmp()에 문자열이 아닌 걸 주면
PHP의 strcmp(a, b)는 두 문자열을 비교하는 함수다. 그런데 인자로 문자열이 아니라 배열을 주면 어떻게 될까. 오래된 PHP(8.0 이전) 버전은 이런 타입 오류에 관대해서, strcmp()가 TypeError를 던지는 대신 경고(warning)만 내고 NULL을 반환한다.
여기서 두 번째 함정이 이어진다. PHP의 ==는 느슨한(loose) 비교라, NULL == 0은 참이다. 정리하면:
즉 $_POST['password']를 배열로 만들기만 하면, 실제 문자열 비교는 아예 일어나지도 않고 if 조건이 그냥 참이 돼 버린다. 매번 바뀌는 비밀번호를 맞힐 필요 자체가 없다. HTML 폼은 password를 평범한 텍스트 입력으로만 받게 생겼지만, PHP는 name="password[]" 형태의 필드나 POST 바디를 그대로 배열로 파싱한다는 걸 이용하면 된다.
password[]=1 로 요청 — 배열이라 strcmp가 NULL을 반환, NULL==0 이라 그대로 통과해 flag가 출력된다
비밀번호가 요청마다 다시 뽑힌다는 걸 감안해, 페이지를 새로 불러온 뒤 같은 방식으로 한 번 더 시도해도 동일하게 통과한다 — 애초에 값을 비교하지 않으니 당연하다.
새로고침 후 다시 같은 방식으로 시도 — 비밀번호가 매번 바뀌어도 상관없이 통과한다
🚀 Full Exploit
import sysimport requestsBASE = sys.argv[1] if len(sys.argv) > 1 else "http://host3.dreamhack.games:20300"# strcmp($_POST['password'], $password) == 0 검사.# PHP(<8)에서 strcmp()에 배열을 넘기면 TypeError 대신 NULL을 반환하고,# NULL == 0 은 PHP의 느슨한(loose) 비교에서 참이 된다.# password=<값> 대신 password[]=1 로 보내 실제 랜덤 비밀번호 없이 우회한다.r = requests.post(BASE + "/", data={"password[]": "1"})print(r.text.strip())
solve.py 소스 + 실행 결과 — flag가 그대로 출력된다
Congratulations! Flag is <b>DH{aede9e7fa4ccb8225f12040a16bdfd37c0c5d2f0}</b>
📝 결론
이 문제는 "완벽해 보이는 방어"도 그 아래 언어의 타입 규칙을 놓치면 무너진다는 걸 보여준다. 비밀번호를 매번 재발급하고 브루트포스 딜레이까지 넣은 설계는 **"공격자가 값을 알아내려 시도할 것"**이라는 가정 위에서만 완벽하다. 하지만 strcmp()의 실제 계약은 "문자열 두 개를 받는다"이지 "아무 값이나 받아 안전하게 처리한다"가 아니다. 배열을 넘겼을 때의 동작(NULL 반환)과 PHP의 느슨한 비교 규칙(NULL == 0은 참)이 우연히 맞물리면서, 비교 자체를 건너뛸 길이 생긴다.
방어는 두 겹으로 해야 한다. 비교 연산은 항상 엄격한(===) 연산자를 쓰고, 사용자 입력은 기대하는 타입(문자열)인지 명시적으로 검증(is_string())한 다음에 비교 함수에 넘겨야 한다. "브루트포스를 막았으니 안전하다"와 "값을 비교하는 함수가 안전하다"는 서로 다른 문제라는 걸, 이 작은 문제가 정확히 짚어준다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…