su another user through ssh with a local script

Nick Bull

I have the following snippet:

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

which is inspired by the following (working) snippet:

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

where both files are local files.

However, I can't find a way to quote or escape the top command so that it successfully works. The above throws ${value of BB_USER}": ./server-user-setup.sh: Permission denied. I have tried many variations:


Can anybody explain to me how to quote/escape this command properly?


Update: getting promisingly close - this fires the script, but doesn't pass the arguments!

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

returns

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}    
Kamil Maciorowski

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"

Code injection possible via BB_USER and APP_USER variables.


Long answer

Let's analyze what happens. It's quite complex. For clarity let's assume:

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

Original (working) snippet

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

All variables are expanded locally. The redirection is local and it doesn't matter it's in the middle. With our assumed variable values this is the equivalent command:

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

ssh gets these arguments: root@ip, -i, key, sh, -s, --, b-user, a-user. Since root@ip looks like user@hostname, the first following argument not recognized as option (like -i) or option-argument (like key being an option-argument to -i) is considered to start an array of operands to build the remote command from. The said argument is sh and this is the command ssh tries to run on the remote side:

sh -s -- b-user a-user

This starts sh which expects commands from its stdin (due to -s). -- is a POSIX convention that tells the tool anything that follows is not an option (see this, guideline 10). This way even if our ${BB_USER} expanded to -f or so, sh wouldn't consider it an option. The following operands are set as the positional parameters of the shell ($1, $2). Stdin is provided by ssh, the local file ./server-root-setup.sh is streamed through.

If the file and the relevant two variables were available on the remote side, you could get (almost) the same result with the following command there:

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

Or if there's a proper shebang in the script:

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

So the snippet is indeed a way to run a local script on the remote side. But note I deliberately didn't quote ${BB_USER} or ${APP_USER}. In the original snipped they are quoted on the local side, but it doesn't matter because ssh builds and passes the command as a string to be parsed on the remote side. If any of the two variables contained inner spaces, the remote sh would get more arguments than you expected; leading or trailing spaces would be lost. Characters like $, ", ' or \ would cause trouble, unless the variable values were deliberately designed to be parsed (but then they couldn't be used locally to produce an equivalent result, that's why I wrote "almost").

Worse things will happen if e.g. ${APP_USER} expands to a-user& rogue_command. On the remote side you get

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

Note such code injection cannot happen when you run things locally (with or without quotes)

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

because separators like & and ; are recognized before variables are expanded, not after. But if you expand variables locally and then the resulting string is parsed again on the remote side, this is a whole different story. It's like remote eval.

I guess ${BB_USER} and ${APP_USER} are usernames, quite safe strings, so the whole thing works despite possible general issues. But if these variables are not fully controlled by you, the approach is not safe.


Your (failed) attempt

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

No redirection this time. Again the variables are expanded locally and the equivalent command looks like this:

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

ssh tries to run this on the remote side:

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

Note all arguments are now arguments to su. At this moment sh is just an option-argument to -c option, similarly -- is an option-argument to another -c option (so it doesn't indicate end of options here). My tests show that from multiple -c options to su only the last one takes effect. This means -c sh is discarded, su uses whatever shell is specified in the (remote) /etc/passwd for the www user. This may or may not be sh. Let's say it's some-shell. This is what's run next (as www user):

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

with remaining operands of su, so

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

If some-shell is a common shell like sh or bash, it behaves alike. Here -c doesn't take an option-argument (see the specification), so -- does indicate end of options. In this case no following argument can be taken as an option, so we can omit --:

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

This is like

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

or (discarding everything we don't use)

some-shell -c command_string command_name argument

So ./server-user-setup.sh is command_string, b-user is command_name and a-user is argument.

-c
Read commands from the command_string operand. Set the value of special parameter 0 […] from the value of the command_name operand and the positional parameters ($1, $2, and so on) in sequence from the remaining argument operands. […]

In effect the remote some-shell tries to read (source) ./server-user-setup.sh, with the string a-user available as $1. Special parameter 0 (now with the value b-user) is what the shell considers to be its own name. The name is used in error messages like this one:

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

Apparently there is ./server-user-setup.sh on the remote side but www user cannot read it.


My approach (still unsafe):

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

The last line (a comment) is just to enumerate quotes in the previous line. To understand how the command works we need to "decrypt" this quoting frenzy. The important things are:

  • Everything inside 1 and 3 single quotes should be taken literally.
  • ${BB_USER} and ${APP_USER} are in double quotes 2; "inner" single quotes belong to the quoted string, they don't prevent the variables from expanding.
  • Wherever a closing quote immediately precedes an opening quote (e.g. the quotes above 12), the quoted strings will be concatenated.

Knowing this we can tell the command with expanded variables looks somewhat like this:

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

where X denotes (only for us, here and now) a single quote character that belongs to the string that starts with su and ends with the last ". Sorry, I couldn't express this in a clearer way. Hopefully it will be less cryptic when we see what ssh gets:

root@ip, -i, key, su www -c "exec sh -s -- 'b-user' 'a-user'"

The last argument contains double quotes and single quotes. This is exactly the string ssh will run on the remote side. Note I took care to pass the whole string as one argument to ssh, so the tool doesn't build a string from multiple arguments and spaces; this way I'm in full control over the string. In general this would be important e.g. if ${BB_USER} expanded to something with a trailing space (the original snippet would fail in this case).

The command that runs on the remote side:

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

The behavior of su has already been discussed. Note the whole quoted string is an option-argument to -c. If not the quote, -s would be an option to su. This is what runs as www user:

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

Note the quotes in su invocation also cause there is no command_name and no argument(s) (compare some-shell -c command_string command_name argument somewhere above). Then some-shell just runs:

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

This would run without exec, but since we don't need some-shell anymore, exec may be a good idea. It makes sh replace some-shell, so instead of some-shell and sh as its child, only sh remains, as if when we run some-shell we had

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

Now this is very similar to what the original snippet runs (sh -s -- b-user a-user). Thanks to additional quotes, possible spaces or few other troublesome characters from ${BB_USER} or ${APP_USER} may not break anything. However ' or " will. Code injection is still possible. Consider ${APP_USER} expanding to "& rogue_command;". The first thing to run on the remote side will be:

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

It doesn't matter some-shell will throw an error, rogue_command will run anyway. Make sure your variables expand to safe strings.

이 기사는 인터넷에서 수집됩니다. 재 인쇄 할 때 출처를 알려주십시오.

침해가 발생한 경우 연락 주시기 바랍니다[email protected] 삭제

에서 수정
0

몇 마디 만하겠습니다

0리뷰
로그인참여 후 검토

관련 기사

분류에서Dev

Can't connect to another user than root through SSH

분류에서Dev

SSH key authentication with another user

분류에서Dev

SSH key authentication with another user

분류에서Dev

Connect to ssh from another ssh via script

분류에서Dev

Run the same script on several servers through SSH

분류에서Dev

Running a script when users connect through ssh

분류에서Dev

Tunnel all remote ports through SSH to local hostname

분류에서Dev

What's the difference between login as user and changing users using su through root?

분류에서Dev

ssh $ host $ FOO 및 ssh $ host "sudo su user -c $ FOO"유형 구성에서 인용

분류에서Dev

How do I set up a local SOCKS proxy that tunnels traffic through SSH?

분류에서Dev

su-vs "ssh user @ machine"을 lucid의 사용자로 사용하는 환경

분류에서Dev

sudo su-SSH 직후

분류에서Dev

SSH into another VM via Perl / Sh script failing. Works manually

분류에서Dev

'su user'동작은 로컬로 로그인 할 때와 ssh를 통해 로그인 할 때 다릅니다.

분류에서Dev

limit user to only a single command: "su -"

분류에서Dev

Creating a cronjob through script

분류에서Dev

View Script Over SSH?

분류에서Dev

Force login through ssh only

분류에서Dev

su-through 루프에 암호 입력

분류에서Dev

로그인 쉘에 ssh + sudo + su

분류에서Dev

Permissions depending on how you login: ssh/su/sudo

분류에서Dev

su-ssh 키 자동 잠금 해제

분류에서Dev

ssh master-원격으로 su 실행

분류에서Dev

ssh command from another machine

분류에서Dev

Redirect user through multiple domains

분류에서Dev

SSH remote user need to be logged in?

분류에서Dev

How to login with ssh as a specific user?

분류에서Dev

Shell Script ssh $SERVER >> EOF

분류에서Dev

SSH password script using root

Related 관련 기사

뜨겁다태그

보관