我有一台使用非常有限的服务器,我希望能够通过 SSH 运行两个非常具体(和自定义)的指令。为了做到这一点,我已将该受限用户的 shell 设置为自定义 BASH 脚本,该脚本仅接受这两个“命令”。
这是/etc/passwd
该用户的:
limited:x:1000:1000:,,,:/home/limited:/usr/sbin/limited_shell.bash
这是limited_shell.bash
脚本(嗯,到目前为止我所拥有的脚本无法正常工作):
#!/bin/bash
readonly RECORDER_SCRIPT="/usr/sbin/record.py"
echo "Verifying params: \$1: $1, \$2: $2"
shift
case $1 in
[0-9]*)
nc -z localhost $1 < /dev/null >/dev/null 2>&1
result=$?
if [[ $result -eq 0 ]]
then
echo "O"
else
echo "C"
fi
exit $result
;;
record)
$RECORDER_SCRIPT ${*:3}
exit $?
;;
*)
exit 0
;;
esac
exit 0
正如您可能从脚本中推断出的那样,我想要limited_shell.bash
接受的两个命令是:一个数字和一个“记录”字符串。当limited_shell.bash
使用数字调用时,将返回它是否对应于打开或关闭的本地端口。另一个允许的命令触发对 python 脚本 ( /usr/sbin/record.py
) 的调用,然后该脚本从输入记录一些视频。第二个命令需要使用额外的参数来调用,这就是问题开始的地方。
当我在另一台机器上尝试远程执行record
命令时......
ssh [email protected] record -option1 foo -option2 bar
...实际到达的limited_shell.bash
是:(-c 'record -option1 foo -option2 bar'
从技术上讲有两个参数,其中之一是-c
,第二个是整个字符串命令+参数我要执行的)
我考虑过移动第一个参数(-c
),并按空格分割第二个参数(带参数的实际命令),但这很脏,如果我想将其中一个参数传递给脚本record
,会给我带来很多麻烦record.py
是一个包含空格的字符串。
解析命令的正确方法是什么?我确信一定有比转移和分裂更好的方法。
答案1
你的脚本旨在实施壳。即命令行解释器。
当你跑步时:
ssh host echo '$foo;' rm -rf '/*'
ssh
(客户端),连接参数(使用 ASCII SPC 字符),并将其发送到sshd
.sshd
将用户的登录 shell 调用为:
exec("the-shell", "-c", "the command-line")
那是在这里:
exec("the-shell", "-c", "echo $foo; rm -rf /*")
如果你运行的话,结果是完全一样的:
ssh host echo '$foo;' 'rm -rf' '/*'
ssh host 'echo $foo;' "rm -rf /*"
ssh host 'echo $foo; rm -rf /*'
(后一种是更好的选择,因为它可以更清楚地表明正在做什么)。
由the-shell
决定如何使用该命令行来决定。例如,类似 Bourne 的 shell 将扩展$foo
为变量的内容foo
,它将被视为命令分隔符;
,并将扩展为./*
/
现在,就您而言,您可以做任何您想做的事情。但既然这意味着受限制的用户,您可能想做的尽可能少,例如,不扩展变量、命令替换、全局变量、不允许多个命令、不进行重定向...
另一件要记住的事情是,即使在非交互式情况下(如解释脚本时),调用时也会bash
读取。因此,您可能想要避免(或至少将其称为)或确保不是由用户写入或使用该选项。~/.bashrc
ssh
bash
sh
~/.bashrc
--norc
现在,由于由您决定如何解释命令行,因此您可以简单地分割一个空格、换行符或制表符:
#!/bin/sh -
[ "$1" = "-c" ] || exit
set -f
set -- $2
case $1 in
(record)
shift
exec /usr/sbin/record.py "$@"
;;
("" | *[!0-9]*)
echo >&2 "command not supported"
exit 1
;;
(*)
nc ...
;;
esac
但这意味着record
将无法接受包含空格、制表符或换行符或空的参数。
如果您希望他们能够做到这一点,您需要提供某种引用语法。
zsh
有一个报价解析工具可以帮助您:
#! /bin/zsh -f
[ "$1" = "-c" ] || exit
set -- "${(Q@)${(Z:cn:)2}}"
case $1 in
(record)
shift
exec /usr/sbin/record.py "$@"
;;
("" | *[!0-9]*)
echo >&2 "command not supported"
exit 1
;;
(*)
nc ...
;;
esac
支持单引号、双引号和反斜杠。但它也会将诸如此类的事情$(foo bar)
视为单个参数(即使不扩展它)。
答案2
#!/bin/bash
shopt -s extglob
# domain-specific language: provide a "record" command
record () {
/usr/sbin/record.py "$@"
}
command=$2
set -- $command # re-use the positional parameters
case $1 in
record)
# let the shell parse the given command
eval "$command"
;;
+([0-9]))
nc ... # as above
;;
*) exit 0 ;;
esac
注意我使用扩展的 glob 模式+([0-9])
来匹配一个或多个数字的单词。该模式[0-9]*
匹配一个单词开始与数字,所以1foo
会匹配 - 我认为这不是你想要的。
警告使用eval
是非常危险的。考虑向用户向您提供的命令添加额外的命令验证。当出现以下情况时会发生什么:
ssh [email protected] record -option1 foo -option2 \`rm -rf /\`
答案3
我建议采用一种更简单的方法,该方法需要更多的设置,但可以节省编写复杂且容易出错的脚本的时间。
您可以声明 SSH 公钥是只允许运行特定命令。生成密钥对,将公钥复制到服务器,并向command=…
其添加指令。对您想要允许的少量命令中的每一个执行此操作。这意味着您可以在~/.ssh/authorized_keys
服务器上的文件中添加如下行:
command="/path/to/command1" ssh-rsa AAAA…== key1
command="/path/to/command2" no-pty ssh-rsa AAAA…== key2
当您执行远程命令时,选择适当的私钥:
ssh -i ~/.ssh/key1.id_rsa server.example.com 'this is ignored'
您可以在command
指令中编写任何 shell 代码。客户端提供的命令在变量中可用SSH_ORIGINAL_COMMAND
。