什么 env x='() { :;}; 命令' bash 做,为什么不安全?

jip*_*pie 246 bash vulnerability shellshock

bash 中明显存在漏洞(CVE-2014-6271):Bash 特制环境变量代码注入攻击

我试图弄清楚发生了什么,但我不完全确定我理解它。如何echo在单引号中执行?

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
vulnerable
this is a test
Run Code Online (Sandbox Code Playgroud)

编辑 1:打补丁的系统如下所示:

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `x'
this is a test
Run Code Online (Sandbox Code Playgroud)

编辑 2:有一个相关的漏洞/补丁:CVE-2014-7169,它使用了一个稍微不同的测试:

$ env 'x=() { :;}; echo vulnerable' 'BASH_FUNC_x()=() { :;}; echo vulnerable' bash -c "echo test"
Run Code Online (Sandbox Code Playgroud)

未打补丁的输出

vulnerable
bash: BASH_FUNC_x(): line 0: syntax error near unexpected token `)'
bash: BASH_FUNC_x(): line 0: `BASH_FUNC_x() () { :;}; echo vulnerable'
bash: error importing function definition for `BASH_FUNC_x'
test
Run Code Online (Sandbox Code Playgroud)

部分(早期版本)打补丁的输出

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `x'
bash: error importing function definition for `BASH_FUNC_x()'
test
Run Code Online (Sandbox Code Playgroud)

修补输出高达并包括 CVE-2014-7169:

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `BASH_FUNC_x'
test
Run Code Online (Sandbox Code Playgroud)

编辑 3:故事继续:

Chr*_*own 213

bash 将导出的函数定义存储为环境变量。导出的函数如下所示:

$ foo() { bar; }
$ export -f foo
$ env | grep -A1 foo
foo=() {  bar
}
Run Code Online (Sandbox Code Playgroud)

也就是说,环境变量foo具有文字内容:

() {  bar
}
Run Code Online (Sandbox Code Playgroud)

当 bash 的新实例启动时,它会查找这些特制的环境变量,并将它们解释为函数定义。你甚至可以自己写一个,看看它仍然有效:

$ export foo='() { echo "Inside function"; }'
$ bash -c 'foo'
Inside function
Run Code Online (Sandbox Code Playgroud)

不幸的是,从字符串(环境变量)解析函数定义可能会产生比预期更广泛的影响。在未打补丁的版本中,它还解释在函数定义终止后发生的任意命令。这是由于在确定环境中可接受的类似函数的字符串方面的约束不足。例如:

$ export foo='() { echo "Inside function" ; }; echo "Executed echo"'
$ bash -c 'foo'
Executed echo
Inside function
Run Code Online (Sandbox Code Playgroud)

请注意,在 bash 启动期间意外执行了函数定义之外的 echo。函数定义只是让评估和利用发生的一个步骤,函数定义本身和使用的环境变量是任意的。shell 查看环境变量, sees foo,它看起来符合它知道的关于函数定义是什么样子的约束,并且它评估该行,无意中还执行了 echo(可能是任何命令,无论是否恶意)。

这被认为是不安全的,因为通常不允许或不期望变量本身直接导致调用其中包含的任意代码。也许您的程序根据不受信任的用户输入设置环境变量。非常出乎意料的是,由于代码中声明的原因,这些环境变量可能会以这样一种方式被操纵:用户可以运行任意命令,而无需您明确意图使用该环境变量来执行此操作。

这是一个可行的攻击示例。作为其生命周期的一部分,您在某处运行一个运行易受攻击的 shell 的 Web 服务器。此 Web 服务器将环境变量传递给 bash 脚本,例如,如果您使用的是 CGI,则有关 HTTP 请求的信息通常作为来自 Web 服务器的环境变量包含在内。例如,HTTP_USER_AGENT可能设置为您的用户代理的内容。这意味着,如果您将用户代理伪装成类似 '() { :; }; echo foo',当该 shell 脚本运行时,echo foo将被执行。同样,echo foo可能是任何东西,恶意与否。

  • @user815423426 rc 是在环境中传递函数的另一个 shell,但它的变量名称以“fn_”为前缀,并且它们仅在调用时被解释。 (20认同)
  • @StéphaneChazelas - 感谢您报告错误。 (19认同)
  • @gnclmorais 你的意思是你运行 `export bar='() { echo "bar" ; }'; zsh -c bar` 并显示 `bar` 而不是 `zsh:1: command not found: bar`?您确定没有将正在调用的 shell 与用于设置测试的 shell 混淆吗? (14认同)
  • 这会影响任何其他类似 Bash 的 shell,比如 Zsh? (3认同)
  • @user815423426 不,zsh 没有此功能。ksh有,但实现方式不同,我认为函数只能在非常狭窄的情况下传递,只有在shell fork的情况下,而不是通过环境。 (3认同)
  • 最近(第二次!)bash 更新将导出的函数显示为环境变量`BASH_FUNC_foo()=...`,而不是`foo=() ...`,这是一个非常重要的区别,因为它*也* 使它不可能通过使用这些名称制作环境变量并将函数定义放入其中来覆盖诸如 `grep` 和 `cat` 之类的常用工具,因为手动设置 `var()="value"` 不是有效的语法,并且会产生错误。 (3认同)

sde*_*ham 85

这可能有助于进一步证明正在发生的事情:

$ export dummy='() { echo "hi"; }; echo "pwned"'
$ bash
pwned
$
Run Code Online (Sandbox Code Playgroud)

如果您正在运行一个易受攻击的 shell,那么当您启动一个新的子 shell(这里,只需使用 bash 语句)时,您将看到任意代码 ( echo "pwned") 作为其启动的一部分立即执行。显然,shell 看到环境变量 (dummy) 包含一个函数定义,并评估该定义以在其环境中定义该函数(请注意,它没有执行该函数:这将打印 'hi'。)

不幸的是,它不仅评估函数定义,还评估环境变量值的整个文本,包括函数定义之后可能存在的恶意语句。请注意,如果没有初始函数定义,则不会评估环境变量,它只会作为文本字符串添加到环境中。正如 Chris Down 所指出的,这是一种实现导入导出的 shell 函数的特定机制。

我们可以看到在新 shell 中定义的函数(并且它在那里被标记为导出),我们可以执行它。此外, dummy 尚未作为文本变量导入:

$ declare -f
dummy ()
{
    echo "hi"
}
declare -fx dummy
$ dummy
hi
$echo $dummy
$
Run Code Online (Sandbox Code Playgroud)

这个函数的创建,或者它运行时会做的任何事情,都不是漏洞利用的一部分——它只是执行漏洞利用的工具。关键是,如果攻击者可以在放入导出的环境变量的文本字符串中提供恶意代码,前面是最​​小且不重要的函数定义,那么它将在子shell启动时执行,这是一个常见事件在许多脚本中。此外,它将以脚本的权限执行。

  • 虽然如果您仔细阅读,接受的答案实际上确实说明了这一点,但我发现这个答案更清晰,更有助于理解问题是*定义*(而不是执行函数本身)的评估。 (18认同)

Fix*_*xee 72

我写这篇文章是对上面 Chris Down 的优秀答案的教程式重铸。


在 bash 中,您可以拥有这样的 shell 变量

$ t="hi there"
$ echo $t
hi there
$
Run Code Online (Sandbox Code Playgroud)

默认情况下,这些变量不会被子进程继承。

$ bash
$ echo $t

$ exit
Run Code Online (Sandbox Code Playgroud)

但是如果你将它们标记为导出,bash 会设置一个标志,这意味着它们将进入子进程的环境(虽然envp参数并不多见,但main在你的 C 程序中有三个参数:main(int argc, char *argv[], char *envp[])最后一个指针数组是一个数组shell 变量及其定义)。

所以让我们导出t如下:

$ echo $t
hi there
$ export t
$ bash
$ echo $t
hi there
$ exit
Run Code Online (Sandbox Code Playgroud)

而上面t在子shell中未定义,它现在在我们导出后出现(export -n t如果您想停止导出它,请使用)。

但是 bash 中的函数是另一种动物。你像这样声明它们:

$ fn() { echo "test"; }
Run Code Online (Sandbox Code Playgroud)

现在您可以像调用另一个 shell 命令一样调用该函数:

$ fn
test
$
Run Code Online (Sandbox Code Playgroud)

再一次,如果你生成一个子shell,我们的函数不会被导出:

$ bash
$ fn
fn: command not found
$ exit
Run Code Online (Sandbox Code Playgroud)

我们可以导出一个函数export -f

$ export -f fn
$ bash
$ fn
test
$ exit
Run Code Online (Sandbox Code Playgroud)

这是棘手的部分:fn就像我们上面导出的 shell 变量一样,将类似的导出函数转换为环境变量t。当fn是局部变量时不会发生这种情况,但是在导出后我们可以将其视为外壳变量。但是,您可以有定期的(即非功能)外壳具有相同名称的变量。bash 根据变量的内容进行区分:

$ echo $fn

$ # See, nothing was there
$ export fn=regular
$ echo $fn
regular
$ 
Run Code Online (Sandbox Code Playgroud)

现在我们可以使用env来显示所有标记为导出的 shell 变量,并且常规fn和函数都fn显示出来:

$ env
.
.
.
fn=regular
fn=() {  echo "test"
}
$
Run Code Online (Sandbox Code Playgroud)

一个子 shell 将摄取两个定义:一个作为常规变量,一个作为函数:

$ bash
$ echo $fn
regular
$ fn
test
$ exit
Run Code Online (Sandbox Code Playgroud)

您可以fn像上面那样定义,也可以直接定义为常规变量赋值:

$ fn='() { echo "direct" ; }'
Run Code Online (Sandbox Code Playgroud)

请注意,这是非常不寻常的事情!通常我们会fn像上面那样用fn() {...}语法定义函数。但是由于 bash 通过环境导出它,我们可以“捷径”直接到上面的常规定义。需要注意的是(反你的直觉,也许)这并不会导致新的功能fn在当前shell中可用。但是如果你生成一个 **sub**shell,那么它就会。

让我们取消函数的导出fn并保留新的常规fn(如上所示)不变。

$ export -nf fn
Run Code Online (Sandbox Code Playgroud)

现在函数fn不再导出,但常规变量fn是,它包含() { echo "direct" ; }在其中。

现在,当子 shell 看到以()它开头的常规变量时,会将其余部分解释为函数定义。但这只是在新的外壳开始时。正如我们在上面看到的,仅仅定义一个以 开头的常规 shell 变量()不会使其表现得像一个函数。你必须启动一个子shell。

现在是“shellshock”错误:

正如我们刚刚看到的,当一个新的 shell 接收到一个以()它开头的常规变量的定义时,它会将它解释为一个函数。但是,如果在定义函数的右大括号之后有更多的给定,它也会执行那里的任何内容。

这些是要求,再次:

  1. 产生了新的 bash
  2. 环境变量被摄取
  3. 这个环境变量以“()”开头,然后在大括号内包含一个函数体,然后是命令

在这种情况下,易受攻击的 bash 将执行后面的命令。

例子:

$ export ex='() { echo "function ex" ; }; echo "this is bad"; '
$ bash
this is bad
$ ex
function ex
$
Run Code Online (Sandbox Code Playgroud)

常规导出的变量ex被传递到被解释为函数的子外壳,exthis is bad随着子外壳的产生,尾随命令被执行 ( )。


解释光滑的单行测试

@jippie 的问题中引用了一种用于测试 Shellshock 漏洞的流行单行代码:

env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
Run Code Online (Sandbox Code Playgroud)

这是一个细分:首先:in bash 只是true. true并且:在 bash 中都评估为(你猜对了)真:

$ if true; then echo yes; fi
yes
$ if :; then echo yes; fi
yes
$
Run Code Online (Sandbox Code Playgroud)

其次,该env命令(也内置在 bash 中)打印环境变量(如我们上面所见),但也可用于运行单个命令,并为该命令提供一个导出变量(或多个变量),并bash -c从其运行单个命令命令行:

$ bash -c 'echo hi'
hi
$ bash -c 'echo $t'

$ env t=exported bash -c 'echo $t'
exported
$
Run Code Online (Sandbox Code Playgroud)

因此,将所有这些东西缝合在一起,我们可以将 bash 作为命令运行,给它一些虚拟的事情(例如bash -c echo this is a test)并导出一个以 开头的变量,()以便子shell 将其解释为一个函数。如果存在 shellshock,它还会立即执行子 shell 中的任何尾随命令。由于我们传递的函数与我们无关(但必须解析!)我们使用可以想象的最短的有效函数:

$ f() { :;}
$ f
$ 
Run Code Online (Sandbox Code Playgroud)

f这里的函数只是执行:命令,返回true并退出。现在附加一些“邪恶”命令并将常规变量导出到子shell,你就赢了。这里又是单线:

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
Run Code Online (Sandbox Code Playgroud)

Sox导出为带有简单有效函数的常规变量,并附echo vulnerable加到末尾。这被传递给 bash,并且 bash 解释x为一个函数(我们不关心)然后可能会执行echo vulnerableif shellshock 存在。

我们可以通过删除this is a test消息来稍微缩短一行代码:

$ env x='() { :;}; echo vulnerable' bash -c :
Run Code Online (Sandbox Code Playgroud)

这不会打扰this is a test:再次运行静默命令。(如果你离开,-c :那么你就坐在子shell中并且必须手动退出。)也许对用户最友好的版本是这个:

$ env x='() { :;}; echo vulnerable' bash -c "echo If you see the word vulnerable above, you are vulnerable to shellshock"
Run Code Online (Sandbox Code Playgroud)

  • 很好的解释。这个问题收到了很多观点(可能不是每个人都像其他人一样精通 bash),而且我相信没有人对 `{:;};` 实际所说的内容多说几句。在我看来,这将是对您的回答的一个很好的补充。可以解释一下你是如何从你的例子中得到问题中的原始命令的吗? (12认同)

kas*_*erd 20

如果您可以为程序提供任意环境变量,则可以通过加载您选择的库来使其执行任何操作。在大多数情况下,这不被视为接收这些环境变量的程序中的漏洞,而是外部人员可以输入任意环境变量的机制中的漏洞。

但是 CVE-2014-6271 是不同的。

在环境变量中包含不受信任的数据并没有错。只需要确保它不会被放入任何可以修改程序行为的环境变量中。说得更抽象一点,对于特定的调用,您可以创建环境变量名称的白名单,允许外部人员直接指定。

在 CVE-2014-6271 上下文中提出的一个示例是用于解析日志文件的脚本。那些可能非常需要在环境变量中传递不受信任的数据。当然,选择这样一个环境变量的名称是为了它不会产生任何不利影响。

但这里是这个特定 bash 漏洞的坏处。它可以通过任何变量名来利用。如果您创建一个名为 的环境变量GET_REQUEST_TO_BE_PROCESSED_BY_MY_SCRIPT,除了您自己的脚本之外,您不会期望任何其他程序来解释该环境变量的内容。但是通过利用这个 bash 漏洞,每一个环境变量都变成了一个攻击向量。

请注意,这并不意味着环境变量的名称应该是秘密的。知道所涉及的环境变量的名称不会使攻击变得更容易。

如果program1调用program2又调用program3,则program1可以program3通过环境变量将数据传递给。每个程序都有一个它设置的特定环境变量列表和一个它所作用的特定列表。如果您选择了不被 识别的名称program2,则可以将数据从 传递program1到 ,program3而不必担心这会对 产生任何不利影响program2

如果名称集之间没有重叠,则知道导出的变量的确切名称program1和解释的变量的名称的攻击者program2无法利用此知识来修改“program2”的行为。

但是如果program2bash脚本,这就会崩溃,因为由于这个错误,bash会将每个环境变量解释为代码。


Ban*_*uin 9

您链接的文章中对此进行了解释...

您可以在调用 bash shell 之前使用特制的值创建环境变量。这些变量可以包含代码,一旦调用 shell,就会立即执行这些代码。

这意味着被调用的 bash 在调用时-c "echo this is a test"执行单引号中的代码。

Bash 有一些函数,虽然在一个有限的实现中,并且可以将这些 bash 函数放入环境变量中。当将额外代码添加到这些函数定义的末尾(在环境变量内)时,会触发此缺陷。

意味着您发布的代码示例利用了以下事实,即调用的 bash 在执行分配后不会停止评估此字符串。在这种情况下的功能分配。

据我了解,您发布的代码片段的真正特殊之处在于,通过在我们要执行的代码之前放置一个函数定义,可以绕过一些安全机制。