setuid许可权位告诉Linux使用所有者的有效用户ID而不是执行程序来运行程序:

> cat setuid-test.c

#include <stdio.h>
#include <unistd.h>

int main(int argc, char** argv) {
    printf("%d", geteuid());
    return 0;
}

> gcc -o setuid-test setuid-test.c
> ./setuid-test

1000

> sudo chown nobody ./setuid-test; sudo chmod +s ./setuid-test
> ./setuid-test

65534


然而,这仅适用于可执行文件; shell脚本忽略setuid位:

> cat setuid-test2

#!/bin/bash
id -u

> ./setuid-test2

1000

> sudo chown nobody ./setuid-test2; sudo chmod +s ./setuid-test2
> ./setuid-test2

1000



维基百科说:


由于增加了安全可能性缺陷,许多操作系统在应用于可执行的Shell脚本时都会忽略setuid属性。


我愿意接受那些风险,有什么办法告诉Linux处理setuid位吗?

如果没有,是否存在解决此问题的常用解决方法?我当前的解决方案是添加一个sudoers条目,以允许ALL以我希望其运行的用户身份运行给定脚本,并使用NOPASSWD避免出现密码提示。这样做的主要缺点是每次我想执行此操作时都需要sudoers条目,并且调用方需要sudo some-script而不是some-script

#1 楼

Linux会忽略所有已解释的可执行文件(即以#!行开头的可执行文件)中的setuid¹位。 comp.unix.questions常见问题解答解释了setuid shell脚本的安全性问题。这些问题有两种:与shebang有关的和与shell有关的。我将在下面详细介绍。

如果您不关心安全性并希望允许setuid脚本,那么在Linux下,您需要修补内核。从3.x内核开始,我认为您需要在调用install_exec_creds之前在load_script函数中添加对open_exec的调用,但是我尚未测试。


Setuid shebang

shebang(#!)的典型实现方式存在一个竞争条件:


内核打开可执行文件,并发现它以#!开头。
内核关闭可执行文件并改为打开解释器。
内核将脚本的路径插入参数列表(如argv[1]),并执行解释器。

如果此实现允许使用setuid脚本,攻击者可以通过创建到现有setuid脚本的符号链接来执行任意脚本,然后执行它,并在内核执行了步骤1之后且解释器开始打开之前安排更改链接。它的第一个论点。因此,大多数unices在检测到shebang时都会忽略setuid位。

保护此实现的一种方法是内核锁定脚本文件,直到解释器打开它为止(请注意,必须不仅防止取消链接或覆盖文件,而且还防止重命名路径中的任何目录)。但是Unix系统倾向于回避强制性锁定,而符号链接会使正确的锁定功能特别困难且具有侵入性。我认为没有人会这样。

一些Unix系统(主要是OpenBSD,NetBSD和Mac OS X,所有这些都需要启用内核设置)使用附加功能来实现安全的setuid shebang:路径/dev/fd/N引用已在文件描述符N上打开的文件(因此打开/dev/fd/N大致等于dup(N))。许多unix系统(包括Linux)都具有/dev/fd,但没有setuid脚本。


内核会打开可执行文件,并发现它以#!开头。假设可执行文件的文件描述符为3。
内核打开解释器。
内核插入/dev/fd/3参数列表(作为argv[1])并执行解释器。

Sven Mascheck的shebang页面上有关于unices的shebang的大量信息,包括setuid支持。


Setuid解释器

假设您已经成功编写了程序由于您的操作系统支持setuid shebang或因为您使用了本机二进制包装器(例如sudo),所以以root身份运行。你打开安全孔了吗?也许。这里的问题不是关于解释程序还是编译程序。问题是您的运行时系统是否具有特权执行才能安全地运行。


任何动态链接的本机二进制可执行文件都是由动态加载程序(例如/lib/ld.so)解释的方式,动态加载程序会加载动态该程序所需的库。在许多操作系统上,您可以配置环境中动态库的搜索路径(LD_LIBRARY_PATH是环境变量的通用名称),甚至可以将其他库加载到所有已执行的二进制文件中(LD_PRELOAD)。通过在libc.so中放置特制的$LD_LIBRARY_PATH(以及其他策略),程序的调用者可以在该程序的上下文中执行任意代码。所有理智的系统都会忽略setuid可执行文件中的LD_*变量。
在sh,csh和派生类之类的shell中,环境变量自动成为shell参数。通过诸如PATHIFS之类的参数以及更多其他参数,脚本的调用者有很多机会在shell脚本的上下文中执行任意代码。如果某些shell检测到已使用特权调用了脚本,则将这些变量设置为默认的默认设置,但我不知道我会信任任何特定的实现。
大多数运行时环境(本机,字节码或解释)具有相似的功能。在setuid可执行文件中,很少有人采取特殊的预防措施,尽管运行本机代码的可执行文件通常不比动态链接做得更好(这样做确实采取了预防措施)。
Perl是一个明显的例外。它以安全的方式显式支持setuid脚本。实际上,即使您的操作系统忽略了脚本中的setuid位,您的脚本也可以运行setuid。这是因为perl附带了setuid根辅助程序,该辅助程序执行必要的检查并以所需的特权在所需的脚本上重新调用解释器。在perlsec手册中对此进行了说明。过去,setuid perl脚本需要#!/usr/bin/suidperl -wT而不是#!/usr/bin/perl -wT,但是在大多数现代系统上,#!/usr/bin/perl -wT足够。

请注意,使用本机二进制包装程序本身并不能防止这些问题。实际上,这会使情况变得更糟,因为它可能阻止您的运行时环境检测到它是使用特权调用的,并且绕过了其运行时可配置性。

如果包装程序清理了环境,则本机二进制包装程序可以使Shell脚本安全。脚本必须注意不要做出太多假设(例如,关于当前目录),但这是可以做到的。您可以为此使用sudo,前提是它已设置为清理环境。将变量列入黑名单容易出错,因此请始终将其列入白名单。使用sudo时,请确保env_reset选项已打开,setenv已关闭,并且env_fileenv_keep仅包含无害变量。


TL,DR:


Setuid shebang是不安全的,但通常会被忽略。
如果您以特权方式运行程序(通过sudo或setuid),请编写本机代码或perl,或使用包装程序启动该程序以清理环境(例如带有env_reset选项的sudo)。

¹如果将“ setgid”替换为“ setuid”,则本讨论同样适用。它们都被Linux内核在脚本上忽略

评论


@Josh:可以使用安全的setuid shell脚本,但前提是shell实施者和脚本编写者都非常小心。我建议使用Perl,而不要使用本机代码,在这里,实现者应注意setuid脚本应该是安全的,而脚本编写者则应毫不费力。

–吉尔斯'所以-不再是邪恶的'
2010-12-11 13:52

显然suidperl的东西已被弃用并标记为可移除多年(但仍然存在)

– jmtd
2011年5月12日14:32

实际上,从perl 5.11(稳定于5.12)起已删除suidperl:perl5110delta:>已删除“ suidperl”。它过去提供了一种机制,可以在不正确支持setuid权限的系统上模拟setuid权限。 perl5120delta:>“ suidperl”不再是Perl的一部分。它过去提供了一种机制,可以在不正确支持setuid权限的系统上模拟setuid权限。

–兰迪·斯汤纳(Randy Stauner)
2011年7月14日17:40



还要注意来自perl 5.6.1 docs(大约十年前)的这一行... perl561delta:>请注意,在任何最新版本的perl中,suidperl都不是默认生成或安装的。强烈建议不要使用suidperl。如果您认为需要,请先尝试使用sudo之类的方法。参见courtesan.com/sudo。

–兰迪·斯汤纳(Randy Stauner)
2011年7月14日在17:45

我不明白:这似乎是造成问题的原因的解释,但是这里对OP的问题有实际答案吗?有没有办法告诉我的操作系统运行setuid shell脚本?

–汤姆
15年3月31日在13:36

#2 楼

解决此问题的一种方法是从可以使用setuid位的程序中调用shell脚本。
类似于sudo。
例如,这是在C程序中如何实现此目的:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    setuid( 0 );   // you can set it at run time also
    system( "/home/pubuntu/setuid-test2.sh" );
    return 0;
 }


将其另存为setuid-test2.c。
编译
现在在此程序二进制文件上执行setuid:

su - nobody   
[enter password]  
chown nobody:nobody a.out  
chmod 4755 a.out  


现在,您应该能够运行它,并且会看到您的脚本在没有任何权限的情况下执行。
但是在这里,您还需要对脚本路径进行硬编码或将其作为命令行arg传递给以上exe。

评论


我建议不要允许将脚本作为命令行参数进行传递,因为从本质上讲,任何可以执行该程序的人都可以以该定义的用户身份运行任何脚本。

–dsp
2010年8月12日在8:05

请注意,即使脚本的完整路径是硬编码的,这也不安全。 Shell将从环境中继承变量,并且其中许多变量允许调用者注入任意代码。 PATH和LD_LIBRARY_PATH是显而易见的向量。有些shell甚至在开始正确执行脚本之前就执行$ ENV或$ BASHENV或〜/ .zshenv,因此您无法在脚本中完全保护它们。调用具有特权的shell脚本的唯一安全方法是清理环境。 Sudo知道如何安全地进行操作。因此,请勿编写您自己的包装器,而应使用sudo。

–吉尔斯'所以-不再是邪恶的'
10-10-8在20:26

我感到很难过,因为他突然为此感到不满意-我确实明确地说过我也想听到不安全的版本,并且我在想象一个可执行程序,当我说这个可执行程序时会使用shell脚本作为参数。显然这是完全不安全的,但是我想知道存在什么可能性

– Michael Mrozek
2010-10-8 22:58

@Gilles:仅供参考,Linux在遇到setuid位时会取消LD_LIBRARY_PATH的设置。

–user1686
2012年2月15日15:31

而不是使用系统,您可能会发现使用exec系列之一-最可能是execve更加简单(高效)。这样,您无需创建新进程或启动Shell,就可以转发参数(假设特权脚本可以安全地处理参数)。

– Toby Speight
15年7月23日在18:52

#3 楼

我为这条船中的一些脚本添加了前缀:

#!/bin/sh
[ "root" != "$USER" ] && exec sudo q4312078q "$@"


请注意,这不使用setuid,而只是使用sudo执行当前文件。

评论


这不使用setuid,而只是给您一个sudo提示。 (对我而言,setuid的全部要点是允许事物以root身份运行而无需sudo。)

–吕克
18/12/12在21:13

@Luc如果您在sudoers文件中使用NOPASSWD标记,那么这不会给您sudo提示,因为OP表示确实如此。我也使用这种方法,它还具有sudoers提供的额外的环境卫生。

– dannyw
20年7月26日在22:59

@dannyw是的,很公平。我猜想我对标题的期望(以及我正在寻找的答案,来自搜索引擎)与OP在他们的问题中实际写的内容之间不匹配。但是,我无法删除该否决票……“您最后一次对此答案进行了投票,是在18年12月13日晚上8:26。除非对此答案进行了编辑,否则您的投票现已锁定。” wtf stackexchange

–吕克
20 Jul 26'23:28



#4 楼

如果要避免调用sudo some_script,则可以执行以下操作:
  #!/usr/bin/env sh
  
  sudo /usr/local/scripts/your_script

SETUID程序在设计时必须格外小心,因为它们具有root特权运行并且用户对其具有很大的控制权。他们需要检查一切。您不能使用脚本来执行此操作,因为:

Shell是与用户进行大量交互的大型软件。几乎不可能对所有内容进行检查,尤其是因为大多数代码都不打算在这种模式下运行。
脚本是一种几乎是快速的“肮脏”解决方案,并且通常在编写时不小心setuid。它们具有许多潜在的危险功能。
它们严重依赖于其他程序。仅检查外壳是不够的。请同时检查sedawk等。

请注意,sudo提供了一些健全性检查,但还不够,请检查您自己代码中的每一行。
最后一点:考虑使用功能。它们使您可以授予以用户身份运行的进程特殊特权,这些特权通常需要root特权。但是,例如,尽管ping需要操纵网络,但它不需要访问文件。功能可以有选择地继承到子流程。

评论


我假设/ ust / bin / env应该是/ usr / bin / env。

–鲍勃
17年4月18日在16:07

#5 楼

super [-r reqpath]命令[args]

Super允许指定用户像执行root一样执行脚本(或其他命令);或者它可以在执行命令之前根据每个命令设置uid,gid和/或补充组。它旨在作为使脚本setuid为root的安全替代方法。 Super还允许普通用户提供其他人执行的命令;它们使用提供命令的用户的uid,gid和组执行。

Super查阅``super.tab''文件以查看是否允许用户执行所请求的命令。如果授予权限,super将执行pgm [args],其中pgm是与此命令相关联的程序。 (默认情况下允许执行根,但是如果规则不包括根,则仍然可以拒绝根。默认情况下,不允许普通用户执行。)

如果命令是符号链接(或硬链接)
,然后键入%command args等同于键入%super command args(该命令不能为super,否则super将无法识别它是通过链接调用的。)

http://www.ucolick.org/~will/RUE/super/README

http://manpages.ubuntu.com/manpages/utopic/en/man1/super.1.html

评论


嗨,欢迎来到该网站!我们希望答案在这里更加详细。您能否编辑答案并解释该程序是什么,以及它如何帮助解决OP的问题?

– terdon♦
15年1月27日在13:09

谢谢Nizam,我们花了数小时试图寻找类似于super之类的东西,以便其他用户可以作为文件用户执行帐户。

–乍得
16/09/14在15:39

#6 楼

您可以为sudo +脚本名称创建别名。当然,这还需要做更多的工作,因为您还必须设置一个别名,但这使您不必键入sudo。

但是如果您不介意可怕的安全性,风险,请使用setuid shell作为shell脚本的解释器。不知道这是否对您有用,但我想可能会。

让我声明,我建议不要这样做。我只是出于教育目的提及它;-)

评论


会的。正如您所说的那样可怕。 SETUID位允许使用所有者权限执行。 Setuid外壳程序(除非它旨在与setuid一起使用)将以root用户身份运行给任何用户。即任何人都可以运行rm -rf /(以及“请勿在家中”系列中的其他命令)。

– Maciej Piechotka
2010年8月17日在10:52

@MaciejPiechotka by不要在家里做你的意思是在工作中随意做吗? :)

–彼得
2014-2-27在21:22

#7 楼

如果由于某种原因sudo不可用,则可以在C中编写一个精简包装脚本:

#include <unistd.h>
int main() {
    setuid(0);
    execle("/bin/bash","bash","/full/path/to/script",(char*) NULL,(char*) NULL);
}


编译后,将其设置为setuidchmod 4511 wrapper_script。 />
这类似于另一个已发布的答案,但是在干净的环境中运行脚本,并显式使用/bin/bash而不是system()调用的shell,因此关闭了一些潜在的安全漏洞。

请注意,这会完全丢弃环境。如果要使用某些环境变量而不打开漏洞,则实际上只需要使用sudo即可。

显然,您想要确保脚本本身只能由root写入。

#8 楼

我首先发现了这个问题,没有所有答案都令人信服,这是一个更好的选择,如果您愿意,还可以完全混淆bash脚本!

它应该是不言自明的。

#include <string>
#include <unistd.h>

template <typename T, typename U>
T &replace (
          T &str, 
    const U &from, 
    const U &to)
{
    size_t pos;
    size_t offset = 0;
    const size_t increment = to.size();

    while ((pos = str.find(from, offset)) != T::npos)
    {
        str.replace(pos, from.size(), to);
        offset = pos + increment;
    }

    return str;
}

int main(int argc, char* argv[])
{
    // Set UUID to root
    setuid(0);

    std::string script = 
R"(#!/bin/bash
whoami
echo 
)";

    // Escape single quotes.
    replace(script, std::string("'"), std::string("'\"'\"'"));

    std::string command;
    command = command + "bash -c '" + script + "'"; 

    // Append the command line arguments.
    for (int a = 0; a < argc; ++a)
    {
        command = command + " " + argv[a];
    }

    return system(command.c_str());
}


然后运行

g++ embedded.cpp -o embedded
sudo chown root embedded
sudo chmod u+s embedded


评论


为什么要下票???

–西奥多·R·史密斯
17年7月27日在8:05

可能是因为它是一个过于复杂的解决方案,可以进行字符串操作并嵌入了外壳代码。您可以制作一个简单的启动器,也可以使用sudo。这是所有选择中最糟糕的。

–西里德
17年9月27日在22:01