Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I have a server with a very limited used whom I want to be able to run two very specific (and custom) instructions through SSH. In order to do that, I have set the shell for that limited user to be a custom BASH script that will only accept those two "commands".

This is the /etc/passwd for that user:

limited:x:1000:1000:,,,:/home/limited:/usr/sbin/limited_shell.bash

This is the limited_shell.bash script (well, what I have so far, which is not working properly):

#!/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

As you might deduce from the script, the two commands I want limited_shell.bash to accept are: a number and a "record" string. When limited_shell.bash is called with a number, it will return whether it corresponds with an opened or closed local port. The other allowed command triggers a call to a python script (/usr/sbin/record.py) which then records some video from an input. This second command needs to be called with extra arguments, and that's where the problems start.

When, in another machine I try to remotely execute the record command...

ssh limited@192.168.1.5 record -option1 foo -option2 bar

... what actually arrives to limited_shell.bash is: -c 'record -option1 foo -option2 bar' (technically two arguments, one of them being -c and the second one being the whole string command+args that I want to execute)

I thought about shifting the first argument (the -c), and split the second argument (the actual record command with arguments) by space, but that is dirty and will give me a lot of troubles if one of the parameters I want to pass to the record.py script is an string containing an space.

What is the right way of parsing the commands? I'm sure there has to be a better way than shifting and splitting.

share|improve this question

3 Answers 3

up vote 2 down vote accepted

Your script is meant to implement a shell. That is, a command line interpreter.

When you run:

ssh host echo '$foo;' rm -rf '/*'

ssh (the client), concatenates the arguments (with ASCII SPC characters), and sends that to sshd. sshd calls the user's login shell as:

exec("the-shell", "-c", "the command-line")

That is here:

exec("the-shell", "-c", "echo $foo; rm -rf /*")

And that would have been exactly the same had you run:

ssh host echo '$foo;' 'rm -rf' '/*'
ssh host 'echo $foo;' "rm -rf /*"
ssh host 'echo $foo; rm -rf /*'

(the later one being the preferable one as it makes it clearer what is being done).

It's up to the-shell to decide what to do with that command line. For instance, a Bourne-like shell would expand $foo to the content of the foo variable, it would consider ; as a command separator, and would expand /* into a sorted list of non-hidden files in /.

Now, in your case since, you can do whatever you want. But since that's meant to be a restricted user, you may want to do as little as possible, for instance, not expand variable, command substitution, globs, not allow several commands, not do redirections...

Another thing to bear in mind in that bash reads ~/.bashrc when called over ssh even when non-interactive (as in when interpreting scripts). So you probably want to avoid bash (or at least call it as sh) or make sure ~/.bashrc is not writabe by the user or use the --norc option.

Now, since it's up to you do define how the command line is interpreted, you can either simply split one space, newline or tab:

#!/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

But that means record won't be able to take arguments that contain spaces, tabs or newlines or that are empty.

If you want them to be able to do that, you need to provide with some sort of quoting syntax.

zsh has a quote parsing tool that can help you there:

#! /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

That supports single, double quotes and backslashes. But it would also consider things like $(foo bar) as a single argument (even if not expanding it).

share|improve this answer

I'll suggest a simpler approach that requires a little more setup but saves writing a complex, error-prone script.

You can declare that an SSH public key is only allowed to run a specific command. Generate the key pair, copy the public key to the server, and add a command=… directive to it. Do this for each of the small number of commands you want to allow. This means you lines like this in the ~/.ssh/authorized_keys file on the server:

command="/path/to/command1" ssh-rsa AAAA…== key1
command="/path/to/command2" no-pty ssh-rsa AAAA…== key2

When you execute the remote command, choose the appropriate private key:

ssh -i ~/.ssh/key1.id_rsa server.example.com 'this is ignored'

You can write any shell code in the command directive. The command supplied by the client is available in the variable SSH_ORIGINAL_COMMAND.

share|improve this answer
#!/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

Note I use the extended glob pattern +([0-9]) to match a word that is one or more digits. The pattern [0-9]* matches a word beginning with a digit, so 1foo would match -- I assume that's not what you want.

Caution the use of eval is highly dangerous. Consider adding additional command verification to the command the user is giving you. What happens when:

ssh limited@192.168.1.5 record -option1 foo -option2 \`rm -rf /\`
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.