为什么我们不能在没有 sudo 的情况下以不同的用户身份执行命令列表?

rag*_*rag 8 bash su sudo

在其他论坛中以不同的方式提出了这个问题。但是没有一个像样的解释为什么你不能在 bash 中执行以下操作。

#!/bin/bash
command1
SWITCH_USER_TO rag
command2
command3
Run Code Online (Sandbox Code Playgroud)

通常,建议的方法是

#!/bin/bash
command1
sudo -u rag command2
sudo -u rag command3
Run Code Online (Sandbox Code Playgroud)

但是为什么不能在bashbash 脚本执行期间的某个时刻更改为不同的用户并以不同的用户身份执行其余命令?

Mir*_*kár 8

内核提供man 2 setuid和朋友。

现在,它适用于调用过程。更重要的是你不能提升你的特权。这就是为什么su并且sudo设置了 SETUID 位,以便它们始终以最高权限 ( root) 运行并相应地降到所需的用户。

这种组合意味着您不能通过运行其他程序来更改外壳 UID(这就是为什么您不能期望sudo或任何其他程序这样做)并且您不能(作为外壳)自己做,除非您愿意以 root 或您想要切换到的同一用户身份运行,这是没有意义的。此外,一旦你放弃特权,就没有办法恢复了。


Sté*_*las 7

该信息已经在我的另一个答案中,但它有点埋在那里。所以我想,我应该在这里添加它。

bash没有更改用户的规定,但zsh有。

在 中zsh,您可以通过为这些变量赋值来更改用户:

  • $EUID:更改有效用户 ID(仅此而已)。通常,您可以在真实用户 ID 和保存的设置用户 ID 之间更改您的 euid(如果从 setuid 可执行文件调用),或者如果您的 euid 为 0,则更改为任何内容。
  • $UID:将有效用户id、真实用户id和保存设置用户id更改为新值。除非新值是 0,否则就不会再回来,因为一旦所有 3 个值都设置为相同的值,就无法将其更改为其他值。
  • $EGID$GID:同样的事情,但对于组 ID。
  • $USERNAME。这就像使用sudoor su。它将您的 euid、ruid、ssuid 设置为该用户的 uid。它还根据用户数据库中定义的组成员身份设置egid、rgid 和ssgid 以及补充组。与 for 一样$UID,除非您设置$USERNAMEroot,否则不会返回,但与 for 一样$UID,您只能更改子 shell 的用户。

如果您以“root”身份运行这些脚本:

#! /bin/zsh -
UID=0 # make sure all our uids are 0

id -u # run a command as root

EUID=1000

id -u # run a command as uid 1000 (but the real user id is still 0
      # so that command would be able to change its euid to that.
      # As for the gids, we only have those we had initially, so 
      # if started as "sudo the-script", only the groups root is a
      # member of.

EUID=0 # we're allowed to do that because our ruid is 0. We need to do
       # that because as a non-priviledged user, we can't set our euid
       # to anything else.

EUID=1001 # now we can change our euid since we're superuser again.

id -u # same as above
Run Code Online (Sandbox Code Playgroud)

现在,要像sudo或 那样更改用户su,我们只能通过使用子 shell 来完成,否则我们只能执行一次:

#! /bin/zsh -

id -u # run as root

(
  USERNAME=rag
  # that's a subshell running as "rag"

  id # see all relevant group memberships are applied
)
# now back to the parent shell process running as root

(
  USERNAME=stephane
  # another subshell this time running as "stephane"

  id
)
Run Code Online (Sandbox Code Playgroud)


Sté*_*las 6

好吧,你总是可以这样做:

#! /bin/bash -
{ shopt -s expand_aliases;SWITCH_TO_USER(){ { _u=$*;_x="$(declare;alias
shopt -p;set +o);"'set -- "${_a[@]}";unset _x _a';set +x;} 2>/dev/null
exec sudo -u "$1" env "_x=$_x" bash -c 'eval "$_x" 2> /dev/null;. "$0"
' "$0";};alias skip=":||:<<'SWITCH_TO_USER $_u'"
alias SWITCH_TO_USER="{ eval '"'_a=("$@")'"';} 2>/dev/null;SWITCH_TO_USER"
${_u+:} alias skip=:;} 2>/dev/null
skip

echo test
a=foo
set a b

SWITCH_TO_USER root

echo "$a and $1 as $(id -un)"
set -x
foo() { echo "bar as $(id -un)"; }

SWITCH_TO_USER rag

foo
set +x

SWITCH_TO_USER root again

echo "hi again from $(id -un)"
Run Code Online (Sandbox Code Playgroud)

(???)

这首先是一个笑话,因为它实现了所要求的内容,尽管可能不完全符合预期,而且实际上并没有用。但是随着它发展到在某种程度上有效并且涉及一些不错的技巧,这里有一些解释:

正如Miroslav 所说,如果我们抛开 Linux 风格的功能(无论如何这也无济于事),非特权进程更改 uid 的唯一方法是执行 setuid 可执行文件。

但是,一旦您获得超级用户权限(例如,通过执行所有者为 root 的 setuid 可执行文件),您就可以在原始用户 ID、0 和任何其他 ID 之间来回切换有效用户 ID,除非您放弃已保存的用户 ID(喜欢sudosu通常做的事情)。

例如:

$ sudo cp /usr/bin/env .
$ sudo chmod 4755 ./env
Run Code Online (Sandbox Code Playgroud)

现在我有了一个env命令,它允许我运行任何具有有效用户 ID 和保存的用户 ID 为 0 的命令(我的真实用户 ID 仍然是 1000):

$ ./env id -u
0
$ ./env id -ru
1000
$ ./env -u PATH =perl -e '$>=1; system("id -u"); $>=0;$>=2; system("id -u");
   $>=0; $>=$<=3; system("id -ru; id -u"); $>=0;$<=$>=4; system("id -ru; id -u")'
1
2
3
3
4
4
Run Code Online (Sandbox Code Playgroud)

perl有包装到setuid/ seteuid(那些$>$<变量)。

zsh 也是如此:

$ sudo zsh -c 'EUID=1; id -u; EUID=0; EUID=2; id -u'
1
2
Run Code Online (Sandbox Code Playgroud)

尽管上面的这些id命令是使用真实用户 ID 调用的,并且保存的用户 ID 为 0(尽管如果我使用 my./env而不是sudo那将只是保存的用户 ID,而真实用户 ID 将保持为 1000),这意味着如果它们是不受信任的命令,它们仍然会造成一些损害,因此您需要将其编写为:

$ sudo zsh -c 'UID=1 id -u; UID=2 id -u'
Run Code Online (Sandbox Code Playgroud)

(即设置所有 uid(有效的、真实的和保存的集合)只是为了执行这些命令。

bash没有任何这样的方式来更改用户 ID。因此,即使您有一个 setuid 可执行文件来调用您的bash脚本,那也无济于事。

使用bash,每次要更改 uid 时,您都需要执行 setuid 可执行文件。

上面脚本中的想法是调用 SWITCH_TO_USER 来执行一个新的 bash 实例来执行脚本的其余部分。

SWITCH_TO_USER someuser或多或少是一个函数,它以不同的用户(使用sudo)再次执行脚本,但跳过脚本的开始直到SWITCH_TO_USER someuser.

棘手的地方在于,我们希望在以不同用户身份启动新 bash 后保持当前 bash 的状态。

让我们分解一下:

{ shopt -s expand_aliases;
Run Code Online (Sandbox Code Playgroud)

我们需要别名。此脚本中的技巧之一是跳过脚本的一部分,直到SWITCH_TO_USER someuser,类似于:

:||: << 'SWITCH_TO_USER someuser'
part to skip
SWITCH_TO_USER
Run Code Online (Sandbox Code Playgroud)

这种形式类似于#if 0C中使用的形式,是一种完全注释掉一些代码的方式。

:是一个返回 true 的空操作。所以在: || :,第二个:永远不会被执行。但是,它被解析。并且<< 'xxx'是 here-document 的一种形式,其中(因为xxx被引用),没有进行扩展或解释。

我们本可以这样做:

: << 'SWITCH_TO_USER someuser'
part to skip
SWITCH_TO_USER
Run Code Online (Sandbox Code Playgroud)

但这意味着必须编写 here-document 并将其作为 stdin 传递给:. :||:避免这种情况。

现在,问题在于我们bash在解析过程中很早就使用了扩展别名的事实。有skip作为的别名:||: << 'SWITCH_TO_USER someuther'的一部分被注释掉结构。

让我们继续:

SWITCH_TO_USER(){ { _u=$*;_x="$(declare;alias
shopt -p;set +o);"'set -- "${_a[@]}";unset _x _a';set +x;} 2>/dev/null
exec sudo -u "$1" env "_x=$_x" bash -c 'eval "$_x" 2> /dev/null;. "$0"
' "$0";}
Run Code Online (Sandbox Code Playgroud)

这是 SWITCH_TO_USER函数的定义。我们将在下面看到 SWITCH_TO_USER 最终将成为围绕该函数的别名。

该函数执行了大量的重新执行脚本。最后,我们看到它使用环境中的变量重新执行(在同一进程中,因为exec)(我们在这里使用是因为通常清理其环境并且不允许传递任意的环境变量)。它将该变量的内容评估为 bash 代码并获取脚本本身。bash_xenvsudobash$_x

_x 之前定义为:

_x="$(declare;alias;shopt -p;set +o);"'set -- "${_a[@]}";unset _x _a'
Run Code Online (Sandbox Code Playgroud)

所有declare, alias,shopt -p set +o输出构成了外壳内部状态的转储。也就是说,它们将所有变量、函数、别名和选项的定义转储为准备评估的 shell 代码。最重要的是,我们根据数组的值添加了位置参数 ( $1, $2...)的设置$_a(见下文),并进行了一些清理,使巨大的$_x变量不会停留在剩余的环境中的脚本。

您会注意到第一部分set +x被包装在一个命令组中,该命令组的 stderr 重定向到/dev/null( {...} 2> /dev/null)。这是因为,如果在脚本set -x(或set -o xtrace)中的某个时刻运行,我们不希望该前导码生成跟踪,因为我们希望使其尽可能少侵入。所以我们运行一个set +x(在确保xtrace预先转储选项(包括)设置之后)将跟踪发送到 /dev/null。

eval "$_X"出于类似的原因,stderr 也被重定向到 /dev/null,但也是为了避免尝试写入特殊只读变量时出现错误。

让我们继续编写脚本:

alias skip=":||:<<'SWITCH_TO_USER $_u'"
Run Code Online (Sandbox Code Playgroud)

这就是我们上面描述的技巧。在初始脚本调用时,它将被取消(见下文)。

alias SWITCH_TO_USER="{ eval '"'_a=("$@")'"';} 2>/dev/null;SWITCH_TO_USER"
Run Code Online (Sandbox Code Playgroud)

现在是 SWITCH_TO_USER 周围的别名包装器。主要原因是能够将位置参数 ( $1, $2...) 传递给bash将解释脚本其余部分的 new 。我们不能在SWITCH_TO_USER 函数中这样做,因为在函数内部,"$@"是函数的参数,而不是脚本的参数。stderr 重定向到 /dev/null 再次是为了隐藏 xtraces,并且eval是为了解决bash. 然后我们调用SWITCH_TO_USER 函数

${_u+:} alias skip=:
Run Code Online (Sandbox Code Playgroud)

除非设置了变量,否则该部分会取消skip别名(用:no-op 命令替换它)$_u

skip
Run Code Online (Sandbox Code Playgroud)

那是我们的skip别名。在第一次调用时,它只是:(无操作)。在序列重新调用,它会是这样的::||: << 'SWITCH_TO_USER root'

echo test
a=foo
set a b

SWITCH_TO_USER root
Run Code Online (Sandbox Code Playgroud)

所以在这里,作为一个例子,在这一点上,我们以root用户身份重新调用脚本,脚本将恢复保存的状态,并跳到该SWITCH_TO_USER root行并继续。

这意味着它必须完全像 stat 那样写,SWITCH_TO_USER在行首,参数之间正好有一个空格。

大多数状态、stdin、stdout 和 stderr 将被保留,但不会保留其他文件描述符,因为sudo通常会关闭它们,除非明确配置为不这样做。所以例如:

exec 3> some-file
SWITCH_TO_USER bob
echo test >&3
Run Code Online (Sandbox Code Playgroud)

通常不会工作。

另请注意,如果您这样做:

SWITCH_TO_USER alice
SWITCH_TO_USER bob
SWITCH_TO_USER root
Run Code Online (Sandbox Code Playgroud)

这仅在您有权sudoasalice并且alice有权sudoasbobbobas时才有效root

所以,在实践中,这并不是很有用。使用su替代sudo(或sudo其中的配置sudo,而不是验证主叫方的目标用户)可能使一些更有意义,但是这仍然意味着你需要知道的所有那些家伙的密码。