如何判断二进制文件是否可以安全地将 sudo 权限授予不受信任的用户?

信息安全 linux 特权升级 须藤
2021-09-02 17:13:16

sudo有时用于授予不受信任或“半信任”用户以 root 身份执行某些任务的能力,而不是授予他们无限的 root 访问权限。这通常通过进入 来完成/etc/sudoers,指定可以执行哪些程序。

但是,某些程序可能会提供比预期更多的(不是双关语)功能,例如moreless它们提供执行其他程序 - 最值得注意的是 shell。manfind


通常,哪些程序可以安全执行取决于系统管理员的知识。某些二进制文件喜欢echocat可能是安全的(即不允许用户生成 shell),而其他类似上述示例的文件已知是可利用的。

sudo有没有办法以合理的信心评估可执行文件在授予权限时是否“安全” ?或者是全面的源代码审计的唯一方法?


cat不安全的回应:是的,它可以用来以root身份读取敏感文件。在某些设置中,这可能是预期的用例(例如,受限用户能够以 root 身份读取,但不能写入)。

sudo此外,向我解释这不是授予读取权限的正确方法的评论或答案如下:我知道。我绝对知道文件系统应该如何构建,但由于我的工作性质,我无法影响文件系统在这些服务器上的结构。能做的就是看看哪个建议可以解决眼前的问题。所以请不要挑战问题的框架。我没有XY问题。

4个回答

一般来说,不可能确定——即使是看似完美安全的程序也可能存在漏洞,这意味着它可以用于任意操作——但这​​里有一些事情需要检查:

该程序是否执行以下任何操作?

  • 显示任意文件或设备的内容。
  • 复制、移动、写入或删除任意文件。
  • 设置/修改任意环境变量(将被其他特权进程获取),或对特定变量进行任意更改。
  • 调用 IOCTL 或以其他方式与任意设备交互。
  • 更改任意文件的所有权或权限。
  • 挂载任意文件系统,或更改现有文件系统的挂载选项。
  • 允许直接访问系统或任意进程(例如调试器)的内存。
  • 允许启动任意程序。

任何执行这些操作的程序都不能安全地授予低权限用户sudo访问权限。例如,这排除了任何能够指定输出文件(通常通过 a-o-f参数)的程序,以任何显示其内容的方式处理输入文件(即使只是通过有关输入格式的信息足够丰富的错误消息)是错误的),以及绝大多数脚本运行时(包括 shell)。

如果您将这些检查中的任意替换为limitedspecific,那么您已经将问题降低了一步(或多个):程序可以做的任何事情是否会导致此类任意事件,可能是通过多个间接级别?例如,如果您的程序允许用户设置一个唯一的环境变量,这将导致特权程序读取与预期不同的文件,并且该不同文件将导致系统允许用户安装他们选择的图像文件作为一个受尊重的文件系统setuid,那么您不能允许不受信任的用户运行该程序。

然而,仅仅因为一个程序通过了所有这些检查并不意味着它实际上是安全的。例如,如果它执行某些网络操作(例如侦听受限端口或发送原始数据包),它可能是不安全的,因为网络上(或同一台机器上)可能有另一个程序假设每个进程都能够这样做things 由受信任的用户拥有——毕竟,这些操作需要 root 用户——而你已经打破了这个假设。此外,上面的要点列表只是我在几分钟内想到的东西;几乎可以肯定有一些我没有包括的特权升级途径。

最后,与所有安全问题一样,这取决于您的威胁模型。

  • 攻击者(不受信任的用户)是具有物理访问权限的机器本地还是远程?阻止具有物理访问权限的坚定攻击者获得 root 权限是极其困难的,特别是如果他们需要能够在没有帮助的情况下(重新)启动机器,因此请考虑您愿意接受的风险。
  • 机器是否在用户之间共享?然后您需要考虑额外的跨用户攻击,例如拒绝服务(通过消耗过多资源或使机器无法使用)。
  • 您是否需要不可否认性(证明谁做了某事的能力)?然后,您需要确保您可以将完成的任何操作与执行这些操作sudo的用户联系起来。
  • 你是否需要阻止用户做一些即使是非 root 用户通常也能做的事情(例如在他们自己的用户上下文中运行任意程序,即使这些程序是游戏或加密矿工之类的东西,或者打开 TCP 客户端连接到任意主机和端口)?然后,您需要另外考虑执行此限制的方式,并防止以 sudo 身份运行任何可能导致用户绕过限制的程序。

等等。这里不可能有一个真正全面的答案。这取决于太多的事情。但是,我要说的是:很难确保不受信任的用户能够以 root 身份运行任何重要的程序(未明确设计为以这种方式安全运行),不会做一些意想不到的事情。即使任何一个这样的程序都不允许任何你认为需要阻止的事情,也有可能将多个这样的程序链接在一起以实现攻击者的目标。

它本质上归结为停止问题,您可以审核代码或对二进制文件进行逆向工程,但是即使没有让您执行任意命令的“功能”,二进制文件或 sudo 本身仍可能存在漏洞,可能导致为启用的用户以 root 身份执行任意命令。

一般来说,你能想到的不是二进制文件,而是系统调用。例如 cat,它们执行系统调用、打开、读取、写入、关闭等的可能性越来越小。另一方面,如果二进制文件(例如 find)可以使用 -exec 参数执行其他二进制文件,则此进程中隐含的系统调用是 fork、exec、mmap 等。这可能是您想要的系统调用保护。在 SELinux 甚至 ( secompfilter ) 上,您可以拥有特定系统调用的策略,并与 sudo 的权限相结合,或者直接在 SELinux 上拥有策略。一个很好的例子是 ptrace 二进制文件并查看隐含的系统调用,然后根据您的要求开始添加或删除这些系统调用的权限

基本上,您将依赖于评估程序安全性的专家,他们对允许提升权限的程序可以做什么有深入的了解。

上面这句话包括程序使用的库和您正在使用的系统(Linux?*BSD?)的知识。

可能不需要进行完整的源代码审核。对于文档齐全的程序,完整阅读文档可能足以找到不安全的选项。它还取决于您可用的时间和专业知识。

通常情况下,证明程序不安全通常很容易显示(一旦发现),但证明它是安全的将非常困难。

主要的问题点是:

  • 不适合您的用例的功能。从在 shell 中查看的选项到现在允许嵌入式 LUA 的配置文件。
  • 程序中的漏洞。交互式程序中的缓冲区溢出通常并不重要,但当以 root 身份运行时,它会成为特权提升的途径。
  • 使用许多或过于复杂的库。例如图形程序。你会想马上拒绝这些。

您将希望允许这些用户使用尽可能简单和“封闭”的 sudo 程序运行。如果程序允许多个选项,则提供它们以使其固定,如果有配置文件,则使其使用用户不可配置的文件,等等。

我建议允许使用有限的 shell 脚本或短程序,而不是成熟的程序。

例如,对于“不受信任的用户需要绝对读取任何文件”(或 emacs、vi……)的问题,我可能会使用:

/**
 * This program when run by sudo allows reading as root an arbitrary file via less(1)
 */
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

int sudo_var(const char* varname) {
    char *value, *endptr;
    int number;

    value = getenv(varname);
    if (!value) {
        fprintf(stderr, "Environment doesn't contain %s!\n", varname);
        exit(5);
    }

    errno = 0;
    number = strtol(value, &endptr, 10);
    if (*endptr != '\0') {
        fprintf(stderr, "Bad environment variable %s!\n", varname);
        exit(5);
    }
    if (errno || number <= 0) {
        fprintf(stderr, "Bad value for environment variable %s!\n", varname);
        exit(5);
    }
    return number;
}

int main(int argc, char **argv, char **envp) {
    gid_t gid;
    uid_t uid;
    int fd, err;
    struct stat stat_variable;
    char *less_argv[] = {"/usr/bin/less", NULL};

    if (argc < 2) {
        fprintf(stderr, "Provide the file to read\n");
        return 1;
    }
    fd = open(argv[1], O_RDONLY | O_NOCTTY);
    if (fd == -1) {
        perror("Error opening file");
        return 2;
    }

    err = fstat(fd, &stat_variable);
    if (err) {
        perror("stat error");
        return 3;
    }

    if (!S_ISREG(stat_variable.st_mode)) {
        fprintf(stderr, "Not a regular file\n");
        return 3;
    }

    err = dup2(fd, STDIN_FILENO);
    if (err) {
        perror("dup2 failed");
        return 4;
    }

    gid = sudo_var("SUDO_GID");
    uid = sudo_var("SUDO_UID");

    err = setgid(gid);
    if (err) {
        perror("Error setting group id");
        return 6;
    }

    err = setuid(uid);
    if (err) {
        perror("Error setting user id");
        return 7;
    }

    execve(less_argv[0], less_argv, envp);
    perror("exec failed");
    return 8;
}