SSH-使用 ProxyCommand(bash)编写命令行时出现引用错误

SSH-使用 ProxyCommand(bash)编写命令行时出现引用错误

我想使用ssh在 上运行的 bash 脚本中的命令srcHost来远程执行一系列主机(targetHostX)上的命令。其中一些远程主机只能通过跳转主机(jumpHost)访问,而其他远程主机可以直接从 访问srcHost。SSH 密钥身份验证已到位,密钥srcHost:~/id_rsa被 和 接受usr@jumpHostusr@targetHostX并且整个过程必须以非交互方式进行,即没有密码提示。这意味着,我无法使用-J(ProxyJump) 选项,因为它不允许为跳转主机指定 SSH 密钥。

确认可以正常工作的基本命令行如下(例如远程命令:主机名):

usr@srcHost:~$ ssh -i ~/id_rsa -o ProxyCommand="ssh -i ~/id_rsa -W %h:%p usr@jumpHost" usr@targetHostX hostname

在我的 bash 脚本中,我有一个关联数组来定义某些 targetHosts 的跳转主机,我想根据是否定义了跳转主机来编写 SSH 命令行。稍微简化一下,它看起来像这样:

user="usr"
targethost="targetHostX"
remotecommand="hostname"
jumphost="${jumphosts[$targethost]}"
param=""

[ -n "$jumphost" ] && param="-o ProxyCommand=\"ssh -W $targethost:22 $jumphost\""

ssh $param -l $user $targethost "$remotecommand"

当我执行这个(看看set -x发生了什么)时,我得到:

+ '[' -n jumpHost ']'
+ param='-o ProxyCommand="ssh -W targetHostX:22 jumpHost"'
+ ssh -o 'ProxyCommand="ssh' -W targetHostX:22 'jumpHost"' -l usr targetHostX hostname
/bin/bash: -c: line 0: unexpected EOF while looking for matching `"'
/bin/bash: -c: line 1: syntax error: unexpected end of file
write: Broken pipe

我认为这肯定是一些模糊的引用问题,就像 bash 经常出现的情况一样,我尝试了各种不同的引用样式,包括将命令组合成数组 - 但都无济于事。作为一种解决方法,我现在已经在两个 if 分支中输入了两个完整的 ssh 命令行(整个命令行比这里作为最小示例显示的长度要长),虽然这有效,但既不漂亮也不易于维护。另外,我想了解发生了什么。

然后我注意到,错误只发生在命令上ssh!如果我用一个只回显其命令行的小脚本替换 ssh,我会得到:

+ '[' -n jumpHost ']'
+ param='-o ProxyCommand="ssh -W targetHostX:22 jumpHost"'
+ ./command.sh -o 'ProxyCommand="ssh' -W targetHostX:22 'jumpHost"' -l usr targetHostX ''
Commandline: ./command.sh -o ProxyCommand="ssh -W targetHostX:22 jumpHost" -l usr targetHostX

没有错误,并且./command.sh按预期执行。

我对此有点困惑,这里有人能解释一下这个问题吗?我猜我是从 那里得到 bash 错误的jumpHost,所以也许我必须更改里面的引用ProxyCommand- 但该怎么做呢?

谢谢!

答案1

分析

该问题有点是由于错误的引用造成的,但你不能通过更好的引用来真正解决这个问题。

为了了解发生了什么,让我们首先分析一下起作用的命令:

ssh -i ~/id_rsa -o ProxyCommand="ssh -i ~/id_rsa -W %h:%p usr@jumpHost" usr@targetHostX hostname

shell 将其拆分为单词,扩展未加引号的部分~并删除引号。最后ssh得到以下参数:

  • -i
  • /home/…/id_rsa
  • -o
  • ProxyCommand=ssh -i ~/id_rsa -W %h:%p usr@jumpHost
  • usr@targetHostX
  • hostname

ProxyCommand=您在消失后使用的双引号。ssh看不到引号。它们的作用是告诉 shell 不要将您想要的代理命令拆分为单独的单词(它们还阻止了第二个波浪号)。这是完全正确的。看到 后-ossh期望,在下一个参数的开头Option=value检测并使用该参数的其余部分作为值,即代理命令。ProxyCommand=

但是,在脚本中存在这样的情况:

ssh $param …

(其中表示与问题无关的论点)。一般来说你应该用双引号引起变量。我知道你为什么不使用双引号$param。你想$param扩展为多个单词。完成此任务后:

param="-o ProxyCommand=\"ssh -W $targethost:22 $jumphost\""

“正确”引用$param(即"$param")将扩展为一个单词:

-o ProxyCommand="ssh -W …:22 …"

(其中表示$targethost$jumphost扩展为什么;注意,它们是在分配给 期间扩展的param,而不是在 的扩展期间"$param")。ssh不会以单一参数的形式理解它。

您未引用的$param确实扩展为多个参数;第一个是-o(到目前为止一切都很好),但是......这些是参数:

  • -o
  • ProxyCommand="ssh
  • -W
  • …:22从先前的扩展$targethost:22
  • …"从先前的扩展$jumphost\"
  • 与问题无关的后续论点

事实上set -x已经给了你同样的列表:

+ ssh -o 'ProxyCommand="ssh' -W targetHostX:22 'jumpHost"' -l usr targetHostX hostname

它在必要时使用单引号来使其输出无歧义,但您需要注意并知道如何在输出中解释这些单引号。这里的-o意思是sshgot -o,但'ProxyCommand="ssh'意思是sshgot ProxyCommand="ssh

如果先前的扩展$targethost$jumphost将空格,制表符或换行符带入param,则我使用的上述列表中的一个或两个参数实际上将是多个参数。

我猜你在 的值中嵌入了双引号,$param以保护其中的某些部分不被未加引号的 分割$param。这不起作用。变量内的引号对 shell 来说并不特殊。你嵌入的引号不仅无法防止分割;它们也没有消失。ssh看到了-o,预期了Option=value,得到了ProxyCommand="ssh。它得到的结果看起来像Option=value,所以ssh它本身没有抱怨。不过,当它试图value通过将 as shell 代码传递给 来运行它时/bin/bash -c,实际值实际上是:

"ssh

这不是有效的 shell 代码。因此出现错误:

/bin/bash: -c: line 0: unexpected EOF while looking for matching `"'

解决方案

您可以使用 egIFS=#使 shell$param在您想要的位置准确拆分引号;加上set -o noglob以防万一。或者您可以使用eval "ssh $param …"并重新评估 的扩展ssh $param …,因此嵌入其中的引号$param在某些时候会变得特殊。每种解决方法都要求您仔细设计 的值$param以及随附的 shell 代码和变量;如果代码和值是静态的并且您知道自己在做什么,则每种方法都可能有效。另一方面,如果一个人足够熟悉 Bash 以知道他或她将在这里使用IFS或做什么eval,那么他可能知道正确的方法。

正确的方法是使用数组。即使在sh那里,"$@"它的行为也像数组一样。你的问题被标记为并且您已经在代码中使用了一个数组("${jumphosts[$targethost]}",关联数组),因此您当然可以使用另一个数组。名称可能是param

(更多内容请点击这里:我们如何运行存储在变量中的命令?

您的代码已修复:

user="usr"
targethost="targetHostX"
remotecommand="hostname"
jumphost="${jumphosts[$targethost]}"
param=()

[ -n "$jumphost" ] && param=(-o "ProxyCommand=ssh -W $targethost:22 $jumphost")

ssh "${param[@]}" -l "$user" "$targethost" "$remotecommand"

param=()定义一个空数组。如果数组保持为空,"${param[@]}"则将扩展为零个单词。整个过程将按预期进行……

…除非$targethost$jumphost不幸或不完全受您控制。当ssh将值传递给ProxyCommand=/bin/bash -c,已经扩展的$targethost$jumphost将只是解释为 shell 代码的字符串的子字符串。例如,如果攻击者设法扩展"${jumphosts[$targethost]}"为,; rm -f /important/file;那么您的代理命令将执行rm -f /important/file。如果这是一个问题,请考虑:

[ -n "$jumphost" ] && param=(-o "ProxyCommand=ssh -W ${targethost@Q}:22 ${jumphost@Q}")

Bash 解释脚本时会将${targethost@Q}和替换${jumphost@Q}为相应的值(如果需要,会加引号或转义)。实际上,当整个字符串被解释为 shell 代码(通过/bin/bash -c调用ssh)时,来自变量的任何内容都无法注入代码。


关于什么echo

然后我注意到,错误只发生在ssh命令上!如果我用一个只回显其命令行的小脚本替换 ssh,我就会得到 [无错误]。"

echo并不是一个适合进行此类调试的好工具,因为它可以接受任何内容,但实际上并不会告诉您它获得了哪些确切的参数。它会连接其参数,并在其间插入空格。从输出中您无法分辨哪些空格来自某个参数,哪些来自echo。例如,所有这些命令都会打印相同的输出:

echo -o ProxyCommand=ssh … 
echo -o "ProxyCommand=ssh …"
echo "-o ProxyCommand=ssh …"

但是如果你替换echossh那么显然生成的命令将不等效。

我个人使用printf '<%s>\n'。逐一尝试这些:

printf '<%s>\n' -o ProxyCommand=ssh … 
printf '<%s>\n' -o "ProxyCommand=ssh …"
printf '<%s>\n' "-o ProxyCommand=ssh …"

>(zero or more spaces or tabs)(newline)<包含或回车符(或我的终端将解释的转义序列,或...)的参数仍然可以欺骗我,尽管如此,这种方法比...好得多echo

相关内容