使用本地脚本通过 ssh 执行 su 操作

使用本地脚本通过 ssh 执行 su 操作

我有以下片段:

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" su www -c sh -c -- \
  "./server-user-setup.sh" "${BB_USER}" "${APP_USER}"

其灵感来自以下(有效)片段:

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" sh -s -- < \
  "./server-root-setup.sh" "${BB_USER}" "${APP_USER}"

两个文件本地文件

但是,我找不到引用或转义 top 命令的方法,以使其成功运行。上面的命令抛出了${value of BB_USER}": ./server-user-setup.sh: Permission denied。我尝试了许多变体:


有人能向我解释如何正确地引用/转义这个命令吗?


更新:非常接近了——这会触发脚本,但不会传递参数!

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" sh -s -- su www -c < \
  "./server-user-setup.sh" "${BB_USER}" "${APP_USER}"

返回

server-user-setup.sh: Wrong number of arguments. # Part of the script - no args passed
su www -c ${value of BB_USER} ${value of APP_USER}    

答案1

总结

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" \
  'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"

可以通过BB_USERAPP_USER变量注入代码。


长答案

让我们分析一下发生了什么。这很复杂。为了清楚起见,我们假设:

DO_DROPLET_IP=ip
SSH_PRIVKEY_PATH=key
BB_USER=b-user
APP_USER=a-user

原始(有效)片段

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" sh -s -- < \
    "./server-root-setup.sh" "${BB_USER}" "${APP_USER}"

所有变量都在本地扩展。重定向是本地的,没关系它在中间. 使用我们假设的变量值,这是等效命令:

<"./server-root-setup.sh" ssh root@"ip" -i "key" sh -s -- "b-user" "a-user"

ssh获取以下参数:root@ip-ikeysh-s--b-usera-user由于root@ip看起来像user@hostname,第一个以下参数未被识别为选项(如-i)或选项参数(如key的选项参数-i)被视为启动构建远程命令的操作数数组。所述参数是,这是尝试在远程端运行的sh命令:ssh

sh -s -- b-user a-user

这将启动它sh,它需要从其 stdin 中输入命令(由于-s)。--这是一个 POSIX 约定,它告诉工具任何后续操作都不是选项(请参阅,准则 10)。这样,即使我们的${BB_USER}扩展到-f左右,sh也不会考虑它是一种选择。以下操作数设置为 shell 的位置参数($1$2)。stdin 由 提供ssh,本地文件./server-root-setup.sh通过流式传输。

如果该文件和相关的两个变量在远程端可用,则可以使用以下命令获得(几乎)相同的结果:

sh "./server-root-setup.sh" ${BB_USER} ${APP_USER}

或者如果脚本中有一个合适的内容:

./server-root-setup.sh ${BB_USER} ${APP_USER}

因此,该代码片段确实是一种在远程端运行本地脚本的方法。但请注意,我故意没有引用${BB_USER}${APP_USER}。在原始代码片段中,它们在本地端被引用,但这并不重要,因为ssh构建并传递命令作为细绳在远程端进行解析。如果两个变量中的任何一个包含内部空格,远程端sh将获得比您预期更多的参数;前导或尾随空格将丢失。除非变量值是故意设计为可解析的(但这样它们就不能在本地使用以产生等效结果,这就是我写“几乎”的原因),否则像$"'这样\的字符会引起麻烦。

${APP_USER}如果 eg扩展到 ,情况会更糟a-user& rogue_command。在远端,你会得到

sh -s -- b-user a-user& rogue_command

请注意,当您在本地运行时(带或不带引号)不会发生此类代码注入

./server-root-setup.sh ${BB_USER} ${APP_USER}

因为分隔符(如&和 );是在变量扩展之前识别的,而不是之后。但如果您在本地扩展变量,然后在远程端再次解析生成的字符串,情况就完全不同了。就像远程eval

我猜${BB_USER}${APP_USER}是用户名,非常安全的字符串,所以尽管可能存在一般问题,但整个过程仍然有效。但如果这些变量不完全由您控制,则该方法并不安全。


你的(失败的)尝试

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" su www -c sh -c -- \
  "./server-user-setup.sh" "${BB_USER}" "${APP_USER}"

这次没有重定向。变量再次在本地扩展,等效命令如下所示:

ssh root@"ip" -i "key" su www -c sh -c -- "./server-user-setup.sh" "b-user" "a-user"

ssh尝试在远程端运行此操作:

su www -c sh -c -- ./server-user-setup.sh b-user a-user

注意,所有参数现在都是 的参数su。此时,sh只是 option 的选项参数-c,同样, 也是--option 的选项参数其他 -c选项(因此这里并不表示选项结束)。我的测试表明,从多个-c选项到su只有最后一个选项生效。这意味着-c sh被丢弃,使用用户su在(远程)中指定的任何 shell 。这可能是也可能不是。假设它是。这是接下来运行的内容(作为用户):/etc/passwdwwwshsome-shellwww

some-shell -c --   # but wait, it's not all

其余操作数为su,所以

some-shell -c -- ./server-user-setup.sh b-user a-user

如果是类似或 的some-shell通用 shell ,则其行为类似。这里不接受选项参数(请参阅shbash-c规范),因此--表示选项结束。在这种情况下,不能将以下参数作为选项,因此我们可以省略--

some-shell -c ./server-user-setup.sh b-user a-user

这就像

sh -c [-abCefhimnuvx] [-o option]... [+abCefhimnuvx] [+o option]... command_string [command_name [argument...]]

或者(丢弃所有我们不使用的东西)

some-shell -c command_string command_name argument

所以./server-user-setup.sh命令字符串b-user命令名称并且a-user争论

-c
从操作数读取命令。根据操作数的值和位置参数(、等等)按顺序从剩余的参数操作数中command_string设置特殊参数 […] 的值。[…]0command_name$1$2

实际上,远程some-shell尝试读取 (source) ./server-user-setup.sh,字符串a-user可用为$1。特殊参数0(现在值为b-user)是 shell 认为是其自身名称的名称。该名称用于错误消息中,如下所示:

b-user: ./server-user-setup.sh: Permission denied

./server-user-setup.sh显然远程端有此信息,但www用户无法读取。


我的方法(仍然不安全):

ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" \
  'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
# 1                         12                          23 3

最后一行(注释)只是列举了上一行中的引号。要了解该命令的工作原理,我们需要“解密”这种引用狂潮。重要的是:

  • 里面的所有内容1以及3单引号都应按字面意思理解。
  • ${BB_USER}并且${APP_USER}用双引号引起来2;“内部”单引号属于引用的字符串,它们不会阻止变量扩展。
  • 无论何时,结束引号紧接着开始引号(例如上面的引号12),引用的字符串都会被连接起来。

了解了这一点,我们可以知道带有扩展变量的命令看起来有点像这样:

<"./server-user-setup.sh" ssh root@"ip" -i "key" 'su www -c "exec sh -s -- Xb-userX Xa-userX"'

在哪里X 表示(仅对我们而言,此时此地)一个单引号字符,它属于以su最后一个 开头和结尾的字符串"。抱歉,我无法更清楚地表达这一点。希望当我们看到结果时,它会变得不那么神秘ssh

root@ip,,,,-ikeysu www -c "exec sh -s -- 'b-user' 'a-user'"

最后一个参数包含双引号和单引号。这正是ssh将在远程端运行的字符串。注意,我小心地将整个字符串作为一个参数传递给ssh,因此该工具不会从多个参数和空格构建字符串;这样我就可以完全控制字符串。一般来说,这很重要,例如如果${BB_USER}扩展为带有尾随空格的内容(在这种情况下,原始代码片段将失败)。

远端运行的命令:

su www -c "exec sh -s -- 'b-user' 'a-user'"

的行为su已经讨论过了。请注意,整个带引号的字符串是 的选项参数-c。如果不是引号,-s则将是 的选项su。以下是以www用户身份运行的:

some-shell -c "exec sh -s -- 'b-user' 'a-user'"

注意,调用中的引号su也会导致没有command_name和没有argument(s)(比较some-shell -c command_string command_name argument上面的某处)。然后some-shell运行:

exec sh -s -- 'b-user' 'a-user'

这可以在没有 的情况下运行exec,但由于我们不再需要some-shellexec这可能是一个好主意。它替换了shsome-shell因此代替some-shellsh作为其子项,只剩sh下,就好像我们运行时some-shell我们有

sh -s -- 'b-user' 'a-user'

现在,这与原始代码片段运行的内容非常相似(sh -s -- b-user a-user)。由于额外的引号、可能的空格或来自${BB_USER}或的其他一些麻烦字符,${APP_USER}可能不会破坏任何内容。但是'"会。代码注入仍然是可能的。考虑${APP_USER}扩展到"& rogue_command;"。在远程端运行的第一件事将是:

su www -c "exec sh -s -- 'b-user' '"& rogue_command;"'"

不管会不会some-shell出错,rogue_command都会运行。确保你的变量扩展为安全字符串。

相关内容