tl; dr
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" \
'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
可以通过BB_USER
和APP_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
,-i
,key
,sh
,-s
,--
,b-user
,a-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
也不会认为它是一种选择。下列操作数被设置为外壳(的位置参数$1
,$2
)。Stdin由提供ssh
,本地文件./server-root-setup.sh
通过其中流送。
如果文件和相关的两个变量在远程端都可用,则可以在此处使用以下命令获得(几乎)相同的结果:
sh "./server-root-setup.sh" ${BB_USER} ${APP_USER}
或者,如果脚本中有适当的shebang:
./server-root-setup.sh ${BB_USER} ${APP_USER}
因此,该代码段确实是在远程端运行本地脚本的一种方式。但请注意,我故意未引用${BB_USER}
或${APP_USER}
。在原始片段中,它们在本地引用,但这没关系,因为ssh
构建并通过将命令作为字符串进行解析以在远程解析。如果两个变量中的任何一个包含内部空间,则遥控器sh
将获得比您期望的更多的参数。前导或尾随空格将丢失。字符,如$
,"
,'
或\
会引起麻烦,除非该变量的值被故意设计成解析(但当时他们无法在当地使用,以产生同样的结果,这就是为什么我写了“几乎”)。
如果例如${APP_USER}
扩展到会发生更糟的事情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
只是选项的一个选项参数-c
,类似地--
,另一个 -c
选项的一个选项参数(因此这里并不表示选项的结束)。我的测试表明,从多个-c
选项到su
仅最后一个选项生效。这意味着-c sh
被丢弃,su
使用任何壳中(远程)被指定/etc/passwd
为www
用户。这可能是,也可能不是sh
。假设是some-shell
。接下来是(作为www
用户)运行的内容:
some-shell -c -- # but wait, it's not all
剩余的操作数为su
,因此
some-shell -c -- ./server-user-setup.sh b-user a-user
如果some-shell
是像sh
或的通用外壳bash
,则其行为类似。这里-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
在command_string,b-user
是COMMAND_NAME并a-user
为论证。
-c
从command_string
操作数读取命令。0
从command_name
操作数的值中设置特殊参数[…]的值,并从其余参数操作数中依次设置位置参数($1
,$2
等等)。[…]
实际上,远程some-shell
尝试读取(源)./server-user-setup.sh
,字符串a-user
可用$1
。外壳认为自己的名称是特殊参数0
(现在带有value b-user
)。该名称用于此类错误消息中:
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
last 开头和结尾的字符串"
。抱歉,我无法以更清晰的方式表达这一点。希望当我们看到结果时,它的神秘性会降低ssh
:
root@ip
,-i
,key
,su 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
(比较some-shell -c command_string command_name argument
上面的某处)。然后some-shell
运行:
exec sh -s -- 'b-user' 'a-user'
这可以在没有的情况下运行exec
,但是由于我们不再需要some-shell
了,exec
可能是一个好主意。它使sh
replace 成为替换子some-shell
,而不是some-shell
和sh
作为其子代,仅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
无论如何都会运行。确保您的变量扩展为安全字符串。