正确解析脚本中的参数,其行为类似于通过 SSH 调用的 shell

正确解析脚本中的参数,其行为类似于通过 SSH 调用的 shell

我有一台使用非常有限的服务器,我希望能够通过 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读取。因此,您可能想要避免(或至少将其称为)或确保不是由用户写入或使用该选项。~/.bashrcsshbashsh~/.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

相关内容