5.2. 环境变量

默认情况下,环境变量从进程的父进程继承。但是,当一个程序执行另一个程序时,调用程序可以将环境变量设置为任意值。 这对 setuid/setgid 程序来说是危险的,因为它们的调用者可以完全控制它们获得的环境变量。由于它们通常被继承,这也适用于传递;一个安全程序可能会调用另一个程序,并且在没有特殊措施的情况下,会将潜在的危险环境变量值传递给它调用的程序。 以下小节讨论环境变量以及如何处理它们。

5.2.1. 某些环境变量是危险的

某些环境变量是危险的,因为许多库和程序以晦涩、微妙或未记录的方式受环境变量控制。 例如,IFS 变量被 shbash shell 使用,以确定哪些字符分隔命令行参数。 由于 shell 被几个底层调用调用(例如 C 中的 system(3) 和 popen(3) 或 Perl 中的反引号运算符),将 IFS 设置为不寻常的值可能会破坏表面上安全的调用。 此行为已记录在 bash 和 sh 中,但很晦涩; 许多长期用户只知道 IFS 是因为它在破坏安全性方面的用途,而不是因为它实际上经常用于其预期目的。 更糟糕的是,并非所有环境变量都已记录在案,即使记录在案,其他程序也可能会更改并添加危险的环境变量。 因此,唯一的真正解决方案(如下所述)是选择您需要的变量并丢弃其余变量。

5.2.2. 环境变量存储格式是危险的

通常,程序应使用标准访问例程来访问环境变量。 例如,在 C 中,您应该使用 getenv(3) 获取值,使用 POSIX 标准例程 putenv(3) 或 BSD 扩展 setenv(3) 设置它们,并使用 unsetenv(3) 删除环境变量。 我应该在这里指出,setenv(3) 也在 Linux 中实现。

但是,攻击者不必那么好; 攻击者可以使用 execve(2) 直接控制传递给程序的环境变量数据区域。 这允许一些令人讨厌的攻击,只有通过了解环境变量的真正工作方式才能理解。 在 Linux 中,您可以查看 environ(5) 了解有关环境变量的真正工作方式的摘要。 简而言之,环境变量在内部存储为指向字符指针数组的指针; 此数组按顺序存储并以 NULL 指针终止(因此您会知道数组何时结束)。 反过来,字符指针中的每个指针都指向一个 NIL 终止的字符串值,其形式为“NAME=value”。 这有几个含义,例如,环境变量名称不能包含等号,名称和值都不能包含嵌入的 NIL 字符。 但是,这种格式更危险的含义是它允许具有相同变量名称但具有不同值的多个条目(例如,SHELL 的多个值)。 虽然典型的命令 shell 禁止这样做,但本地执行的攻击者可以使用 execve(2) 创建这种情况。

这种存储格式(以及它的设置方式)的问题在于,程序可能会检查这些值中的一个(以查看它是否有效),但实际上使用不同的值。 在 Linux 中,GNU glibc 库试图保护程序免受此影响; glibc 2.1 的 getenv 实现将始终获取第一个匹配的条目,setenv 和 putenv 将始终设置第一个匹配的条目,而 unsetenv 实际上会取消设置所有匹配的条目(祝贺 GNU glibc 的实现者以这种方式实现 unsetenv!)。 但是,某些程序直接访问 environ 变量并迭代所有环境变量; 在这种情况下,他们可能会使用最后一个匹配的条目而不是第一个条目。 因此,如果针对第一个匹配的条目进行了检查,但实际使用的值是最后一个匹配的条目,则攻击者可以利用这一事实来规避保护例程。

5.2.3. 解决方案 - 提取和清除

对于安全的 setuid/setgid 程序,应仔细提取作为输入所需的环境变量的简短列表(如果有)。 然后应清除整个环境,然后将一小部分必需的环境变量重置为安全值。 如果您对下级程序进行任何调用,则确实没有更好的方法; 没有列出“所有危险值”的实用方法。 即使您审查了您直接或间接调用的每个程序的源代码,在您编写代码后,有人可能会添加新的未记录的环境变量,并且其中一个可能会被利用。

在 C/C++ 中清除环境的简单方法是将全局变量 environ 设置为 NULL。 全局变量 environ 在 <unistd.h> 中定义; C/C++ 用户将需要 #include 此头文件。 您需要在生成线程之前操作此值,但这很少是一个问题,因为您希望在程序执行的早期(通常在生成线程之前)执行这些操作。

全局变量 environ 的定义在各种标准中定义; 目前尚不清楚官方标准是否认可直接更改其值,但我不知道任何 Unix 类系统在执行此操作时遇到问题。 我通常只是直接修改“environ”; 操作此类低级组件可能不可移植,但它可以确保您获得干净(且安全)的环境。 在极少数情况下,您需要稍后访问整个变量集,您可以将“environ”变量的值保存在某个地方,但这很少是必需的; 几乎所有程序只需要几个值,其余的都可以删除。

清除环境的另一种方法是使用未记录的 clearenv() 函数。 函数 clearenv() 有一个奇怪的历史; 它应该在 POSIX.1 中定义,但不知何故从未进入该标准。 但是,clearenv() 在 POSIX.9(Fortran 77 与 POSIX 的绑定)中定义,因此它具有准官方状态。 在 Linux 中,clearenv() 在 <stdlib.h> 中定义,但在使用 #include 包含它之前,您必须确保 #define 了 __USE_MISC。 一种稍微“官方”的方法是使 __USE_MISC 被定义,方法是首先 #define _SVID_SOURCE 或 _BSD_SOURCE,然后 #include <features.h> - 这些是官方功能测试宏。

您几乎肯定会重新添加的一个环境值是 PATH,它是搜索程序的目录列表; PATH 应包含当前目录,通常应类似于“/bin:/usr/bin”。 通常,您还会设置 IFS(为其默认值“ \t\n”,其中空格是第一个字符)和 TZ(时区)。 如果您不提供 IFS 或 TZ,Linux 不会死机,但如果您的不提供 TZ 值,某些基于 System V 的系统会出现问题,并且据传某些 shell 需要设置 IFS 值。 在 Linux 中,请参阅 environ(5) 获取您可能想要设置的常见环境变量列表。

如果您确实需要用户提供的值,请首先检查这些值(以确保这些值与合法值的模式匹配,并且它们在某个合理的长度限制内)。 理想情况下,/etc 中应该有一个标准的受信任文件,其中包含“标准安全环境变量值”的信息,但此时尚未为此目的定义标准文件。 对于类似的内容,您可能需要检查那些具有该模块的系统上的 PAM 模块 pam_env。 如果您允许用户设置任意环境变量,那么您将让他们破坏受限 shell(下面将详细介绍)。

如果您使用 shell 作为编程语言,则可以使用带有“-”选项的“/usr/bin/env”程序(它会清除正在运行的程序的所有环境变量)。 基本上,您调用 /usr/bin/env,给它“-”选项,然后在后面跟上您希望设置的变量及其值(如 name=value),然后跟上要运行的程序的名称及其参数。 您通常希望使用完整路径名 (/usr/bin/env) 调用该程序,而不仅仅是“env”,以防用户创建了危险的 PATH 值。 请注意,GNU 的 env 也接受选项“-i”和“--ignore-environment”作为同义词(它们也会清除正在启动的程序的环境),但这些选项不可移植到其他版本的 env。

如果您正在用一种不允许您直接重置环境的语言编程 setuid/setgid 程序,一种方法是创建一个“包装器”程序。 包装器将环境变量程序设置为安全值,然后调用另一个程序。 注意:确保包装器实际上会调用预期的程序; 如果它是一个解释程序,请确保不存在允许解释器加载与被授予特殊 setuid/setgid 权限的程序不同的程序的竞争条件。

5.2.4. 不要让用户设置自己的环境变量

如果您允许用户设置自己的环境变量,那么用户将能够逃脱受限帐户(这些帐户应该只允许用户运行某些程序,而不能作为通用计算机工作)。 这包括允许用户写入或修改其主目录中的某些文件(例如 .login),支持从用户控制下的文件中加载环境变量的约定(例如 openssh 的 .ssh/environment 文件),或支持传输环境变量的协议(例如 Telnet Environment Option;有关详细信息,请参阅 CERT Advisory CA-1995-14)。 永远不应允许受限帐户修改或添加直接包含在其主目录中的任何文件,而应仅被赋予一个特定的子目录,他们可以修改该子目录(如果他们可以修改任何内容)。

ari 在 2002 年 6 月 24 日在 Bugtraq 上发布了对这个问题的详细讨论:

鉴于与其他某些安全问题的相似性,我很惊讶之前没有讨论过这个问题。 如果已经讨论过,人们只是没有给予足够的重视。

这个问题不一定是 ssh 特有的,尽管大多数支持环境传递的 telnet 守护进程应该已经配置为删除危险变量,因为 95 年出现了一个类似(且更严重)的问题(参考:[1])。 我将在这里提供基于 ssh 的示例。

场景一:假设管理员 bob 有一台主机,他想给人们 ftp 访问权限。 Bob 不希望任何人能够实际 _登录_ 他的系统,所以 bob 没有给用户正常的 shell,甚至没有 shell,而是给他们所有人(例如)/usr/sbin/nologin,这是他自己用 C 编写的程序,本质上是将尝试记录到 syslog 并退出,从而有效地结束用户的会话。 就大多数人而言,用户除了设置加密隧道之外,不能用它做太多事情。

问题是,bob 的系统使用动态库(就像大多数系统一样),并且 /usr/sbin/nologin 是动态链接的(就像大多数此类程序一样)。 如果用户可以设置他的环境变量(例如,通过上传 '.ssh/environment' 文件)并在系统上放置一些任意文件(例如 'doevilstuff.so'),他可以通过 LD_PRELOAD(或 LD_* 环境系列中的另一个成员)完全绕过 /usr/sbin/nologin 的任何功能。

用户现在可以在系统上获得一个 shell(当然,使用他自己的权限,除非有任何 'UseLogin' 问题(参考:[2])),并且管理员 bob,如果他知道刚刚发生了什么,将会非常不高兴。

诚然,有很多有趣的方法可以(或多或少地)解决这个问题。Bob可以咬紧牙关,给ftp用户一个不存在的shell,或者他可以静态编译nologin,假设他的操作系统带有静态库。Bob也可以幽默地将他的nologin程序设置为setuid,让标准的C库来处理这种情况。当然,还有一些针对ssh的访问控制,例如AllowGroup和AllowUsers。这些可能在这种情况下有所缓解,但并不能从根本上解决问题。

...现在,如果Bob不想使用/usr/sbin/nologin,而是想使用(例如)他自己编写或下载的某种BBS类型的界面,会发生什么?它可以是用Perl、Tcl或Python编写的脚本,也可以是一个编译后的程序;这都没关系。此外,Bob不一定非要在该主机上运行ftp服务器;相反,也许Bob使用NFS或Veritas从网络上的文件服务器挂载用户主目录;这种完全一样的设置(不幸地)被许多堡垒机、密码管理主机和邮件服务器所采用——仅举几例。也许Bob运营着一家ISP,并在用户不付费时替换用户的shell。有了所有这些可能的(和常见的)情况,Bob要绕过这个问题就会更加困难。

...利用这个问题很简单。规避代码将被编译成一个动态库,并且LD_PRELOAD=/path/to/evil.so应该被放置到~user/.ssh/environment中(一个类似的环境选项可以附加到authohrized_keys文件中的公钥中)。如果没有执行任何动态加载的程序,这将不会产生任何影响。

ISP和大学(以及受类似影响的组织)应该静态编译他们的拒绝(或以其他方式限制的)二进制文件(假设您的操作系统带有静态库)...

理想情况下,sshd(以及所有允许用户自定义环境的远程访问程序)应该剥离libc为setuid程序忽略的任何环境设置。