非root用户的setuid等效方法

6

Linux是否有类似于setuid的C接口,允许程序使用用户名/密码切换到不同的用户?setuid的问题在于只能由超级用户使用。

我正在运行一个简单的Web服务,需要作业以已登录的用户身份执行。因此,主进程以root身份运行,在用户登录后fork并调用setuid以切换到适当的UID。然而,我不太舒服主进程以root身份运行。我宁愿让它以另一个用户身份运行,并拥有类似于su(但无需启动新进程)的切换到其他用户的机制。


当您需要切换到不同的用户时,您可以从root切换到非特权用户,然后通过[保存的用户ID机制](http://en.wikipedia.org/wiki/User_identifier#Saved_user_ID)切换回来。 - David Schwartz
嗯,我认为在我的情况下这不会增加任何安全性,假设切换回来的机制没有受到保护。因此,如果主要服务被攻击,那么这只是一个微不足道的额外步骤,直到获得 root 权限。 - Jeroen Ooms
这取决于你所说的“被利用”。如果你的意思是他们可以让它访问任何他们想要的代码,那么我认为这是根本性的。他们可以添加代码来记录它使用的任何凭据以切换用户,然后使用这些凭据来利用系统。除非你的意思是特别指root会受到损害。 - David Schwartz
2个回答

10
首先,非超级用户绝对可以使用setuid()。在Linux中,你只需要CAP_SETUID(和/或CAP_SETGID)capability即可切换到任何用户。其次,setuid()setgid()可以在真实(执行进程的用户)、有效(setuid/setgid二进制文件的所有者)和保存的标识之间更改进程身份。
然而,这些都与你的情况无关。
存在一个相对简单但极其强大的解决方案:在服务守护程序创建任何线程之前,使用一个设置为setuid root的帮助程序进行分叉和执行,并在该服务和帮助程序之间使用Unix域套接字对进行通信,当用户二进制文件要执行时,服务将其凭据和管道端点文件描述符传递给帮助程序。帮助程序将安全地检查所有内容,如果一切正常,它将分叉并执行所需的用户帮助程序,并将指定的管道端点连接到标准输入、标准输出和标准错误。
开始帮助程序的服务流程尽早执行,步骤如下:
  1. 创建一个Unix域套接字对,用于服务和辅助程序之间的特权通信。

  2. 分叉。

  3. 在子进程中关闭所有多余的文件描述符,仅保留套接字对的一端。将标准输入、输出和错误重定向到/dev/null

  4. 在父进程中关闭子进程的套接字对端。

  5. 在子进程中执行特权辅助二进制文件。

  6. 父进程发送一个简单消息,可能没有任何数据,但包含其凭据的辅助信息

  7. 辅助程序等待来自服务的初始消息。当它收到后,检查凭据。如果凭据不符合要求,立即退出。

辅助消息中的凭据定义了起始进程的UID、GID和PID。虽然进程需要填写这些信息,但内核会验证它们是否正确。 当然,帮助程序会验证UID和GID是否符合预期(对应于服务应该运行的帐户),但诀窍在于获取指向/proc/PID/exe符号链接指向的文件的统计信息。那是发送凭据的进程的真正可执行文件。您应该验证它与已安装的系统服务守护程序相同(由root:root拥有,在系统二进制目录中)。
有一个非常简单的攻击可能会击败到此为止的安全性。恶意用户可以创建自己的程序,分叉并以正确的方式执行帮助程序二进制文件,发送具有其真实凭据的初始消息,但在帮助程序检查凭据实际上指的是什么之前,将其替换为正确的系统二进制文件!
通过进一步采取三个步骤,可以轻松地击败该攻击:
  1. 辅助程序生成一个(加密安全的)伪随机数,比如1024位,并将其发送回父进程。

  2. 父进程将该数字发送回去,但再次添加了其凭据的附属消息。

  3. 辅助程序验证UIDGIDPID未更改,并且/proc/PID/exe仍然指向正确的服务守护程序二进制文件。(我会仅重复完整的检查。)

在第8步中,辅助程序已经确认套接字的另一端正在执行应该执行的二进制文件。发送一个要求对方返回的随机cookie,意味着对方不能事先通过消息“填充”套接字。当然,这假设攻击者无法提前猜测伪随机数。如果您想要谨慎,可以从/dev/random读取一个合适的cookie,但请记住这是有限资源(如果内核中没有足够的随机性可用,则可能会阻塞)。 我个人会从/dev/urandom中读取比如1024位(128字节)并使用它。

在这一点上,助手已经确定了套接字对的另一端是您的服务守护程序,并且助手可以信任控制消息,就像它信任服务守护程序一样。(我假设这是服务守护程序将生成用户进程的唯一机制; 否则,您需要在每个进一步的消息中重新传递凭据,并在助手中每次重新检查它们。)
每当服务守护程序希望执行用户二进制文件时,它会执行以下操作:
1. 创建必要的管道(一个用于向用户二进制文件提供标准输入,一个用于获取来自用户二进制文件的标准输出)。 2. 发送消息给助手,其中包含: - 要运行二进制文件的身份; 用户(和组)名称或UID和GID(s) - 二进制文件的路径 - 给定给二进制文件的命令行参数 - 包含数据管道的用户二进制文件端点的文件描述符的辅助消息。
每当助手收到这样的消息时,它会进行分叉。在子进程中,它将标准输入和输出替换为辅助消息中的文件描述符,使用setresgid()setresuid()和/或initgroups()更改身份,将工作目录更改为适当的位置,并执行用户二进制文件。父助手进程关闭辅助消息中的文件描述符,并等待下一条消息。

如果助手在套接字不再有输入时退出,则当服务退出时,它将自动退出。

如果有足够的兴趣,我可以提供一些示例代码。有很多细节需要注意,所以编写代码有点繁琐。但是,正确编写的话,它比例如Apache SuEXEC更安全。


1
好的答案。这应该被选择为正确答案。 - Tagar
1
亲爱的@Nominal Animal,非常有趣的帖子,您知道一些开源代码已经实现了这种技术吗?我想研究一下这个代码。 - Imylor

4
不,仅通过用户名和密码无法更改UID。(内核不承认“密码”的概念,它只存在于用户空间。)要从一个非root UID切换到另一个,必须作为中间步骤变成root,通常是通过执行setuid二进制文件来实现。exec()
在您的情况下,另一个选项可能是将主服务器作为未特权用户运行,并与以root身份运行的后端进程通信。

是的,正如你在回答中所说的那样,后端进程必须以root身份运行。 - caf
这个答案不正确。对于非root用户,肯定有一种方法可以设置setuid()。请查看Linux功能 http://man7.org/linux/man-pages/man7/capabilities.7.html - Tagar
@Tagar 有CAP_SETUID/GID,但该功能等同于root -- 它允许进程切换到任何UID,包括UID 0。因此,它几乎从不使用。而且它不使用密码,这正是OP所要求的。 - user149341
@duskwuff,通过命名空间可以限制setuid()可以使用的UID http://man7.org/linux/man-pages/man7/user_namespaces.7.html - Tagar
1
我认为duskwuff在这里基本上是正确的,@Tagar。我唯一不同意的是一个小问题:你可以通过使用setuid(和setgid)二进制文件从一个用户切换到另一个用户,而无需成为中间步骤的root。这是一个小问题,因为每个setuid/setgid二进制文件只能允许切换到一个固定的UID和/或GID(二进制文件的所有者和组)。无论如何,我似乎记得在LKML上有关于基于令牌的非root身份验证/凭据更改的讨论,以解决这里的根本问题,而不需要暂时提升权限。 - Nominal Animal
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接