su another user through ssh with a local script
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}
linux ssh shell su
add a comment |
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}
linux ssh shell su
Apologies! Local
– Nick Bull
Jan 31 at 17:08
Lol yes sorry. End of the day tiredness making me type things incorrectly. I'm sshing as root so no sudo
– Nick Bull
Jan 31 at 17:42
1
I'm not sure if this is what you want (so only a comment); and I'm not sure it will work; and keep in mind that running random code from the internet is dangerous (especially as root). This madness:ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" 'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
– Kamil Maciorowski
Jan 31 at 17:58
@KamilMaciorowski That's... wow. I feel a lot better about not getting it. Post it as an answer for others! I'll accept as soon as you do :)
– Nick Bull
Jan 31 at 18:52
@KamilMaciorowski It works correctly! I'll leave it as an edit in the question for somebody, with the answer being rewarded for the proper explanation :)
– Nick Bull
Jan 31 at 20:54
add a comment |
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}
linux ssh shell su
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}
linux ssh shell su
linux ssh shell su
edited Jan 31 at 20:57
Kamil Maciorowski
28.7k156187
28.7k156187
asked Jan 31 at 16:46
Nick BullNick Bull
1084
1084
Apologies! Local
– Nick Bull
Jan 31 at 17:08
Lol yes sorry. End of the day tiredness making me type things incorrectly. I'm sshing as root so no sudo
– Nick Bull
Jan 31 at 17:42
1
I'm not sure if this is what you want (so only a comment); and I'm not sure it will work; and keep in mind that running random code from the internet is dangerous (especially as root). This madness:ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" 'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
– Kamil Maciorowski
Jan 31 at 17:58
@KamilMaciorowski That's... wow. I feel a lot better about not getting it. Post it as an answer for others! I'll accept as soon as you do :)
– Nick Bull
Jan 31 at 18:52
@KamilMaciorowski It works correctly! I'll leave it as an edit in the question for somebody, with the answer being rewarded for the proper explanation :)
– Nick Bull
Jan 31 at 20:54
add a comment |
Apologies! Local
– Nick Bull
Jan 31 at 17:08
Lol yes sorry. End of the day tiredness making me type things incorrectly. I'm sshing as root so no sudo
– Nick Bull
Jan 31 at 17:42
1
I'm not sure if this is what you want (so only a comment); and I'm not sure it will work; and keep in mind that running random code from the internet is dangerous (especially as root). This madness:ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" 'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
– Kamil Maciorowski
Jan 31 at 17:58
@KamilMaciorowski That's... wow. I feel a lot better about not getting it. Post it as an answer for others! I'll accept as soon as you do :)
– Nick Bull
Jan 31 at 18:52
@KamilMaciorowski It works correctly! I'll leave it as an edit in the question for somebody, with the answer being rewarded for the proper explanation :)
– Nick Bull
Jan 31 at 20:54
Apologies! Local
– Nick Bull
Jan 31 at 17:08
Apologies! Local
– Nick Bull
Jan 31 at 17:08
Lol yes sorry. End of the day tiredness making me type things incorrectly. I'm sshing as root so no sudo
– Nick Bull
Jan 31 at 17:42
Lol yes sorry. End of the day tiredness making me type things incorrectly. I'm sshing as root so no sudo
– Nick Bull
Jan 31 at 17:42
1
1
I'm not sure if this is what you want (so only a comment); and I'm not sure it will work; and keep in mind that running random code from the internet is dangerous (especially as root). This madness:
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" 'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
– Kamil Maciorowski
Jan 31 at 17:58
I'm not sure if this is what you want (so only a comment); and I'm not sure it will work; and keep in mind that running random code from the internet is dangerous (especially as root). This madness:
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" 'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
– Kamil Maciorowski
Jan 31 at 17:58
@KamilMaciorowski That's... wow. I feel a lot better about not getting it. Post it as an answer for others! I'll accept as soon as you do :)
– Nick Bull
Jan 31 at 18:52
@KamilMaciorowski That's... wow. I feel a lot better about not getting it. Post it as an answer for others! I'll accept as soon as you do :)
– Nick Bull
Jan 31 at 18:52
@KamilMaciorowski It works correctly! I'll leave it as an edit in the question for somebody, with the answer being rewarded for the proper explanation :)
– Nick Bull
Jan 31 at 20:54
@KamilMaciorowski It works correctly! I'll leave it as an edit in the question for somebody, with the answer being rewarded for the proper explanation :)
– Nick Bull
Jan 31 at 20:54
add a comment |
1 Answer
1
active
oldest
votes
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 thecommand_string
operand. Set the value of special parameter0
[…] from the value of thecommand_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
and3
single quotes should be taken literally.
${BB_USER}
and${APP_USER}
are in double quotes2
; "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.
add a comment |
Your Answer
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "3"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fsuperuser.com%2fquestions%2f1400663%2fsu-another-user-through-ssh-with-a-local-script%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
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 thecommand_string
operand. Set the value of special parameter0
[…] from the value of thecommand_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
and3
single quotes should be taken literally.
${BB_USER}
and${APP_USER}
are in double quotes2
; "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.
add a comment |
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 thecommand_string
operand. Set the value of special parameter0
[…] from the value of thecommand_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
and3
single quotes should be taken literally.
${BB_USER}
and${APP_USER}
are in double quotes2
; "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.
add a comment |
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 thecommand_string
operand. Set the value of special parameter0
[…] from the value of thecommand_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
and3
single quotes should be taken literally.
${BB_USER}
and${APP_USER}
are in double quotes2
; "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.
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 thecommand_string
operand. Set the value of special parameter0
[…] from the value of thecommand_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
and3
single quotes should be taken literally.
${BB_USER}
and${APP_USER}
are in double quotes2
; "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.
edited Jan 31 at 23:43
answered Jan 31 at 23:30
Kamil MaciorowskiKamil Maciorowski
28.7k156187
28.7k156187
add a comment |
add a comment |
Thanks for contributing an answer to Super User!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fsuperuser.com%2fquestions%2f1400663%2fsu-another-user-through-ssh-with-a-local-script%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Apologies! Local
– Nick Bull
Jan 31 at 17:08
Lol yes sorry. End of the day tiredness making me type things incorrectly. I'm sshing as root so no sudo
– Nick Bull
Jan 31 at 17:42
1
I'm not sure if this is what you want (so only a comment); and I'm not sure it will work; and keep in mind that running random code from the internet is dangerous (especially as root). This madness:
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" 'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
– Kamil Maciorowski
Jan 31 at 17:58
@KamilMaciorowski That's... wow. I feel a lot better about not getting it. Post it as an answer for others! I'll accept as soon as you do :)
– Nick Bull
Jan 31 at 18:52
@KamilMaciorowski It works correctly! I'll leave it as an edit in the question for somebody, with the answer being rewarded for the proper explanation :)
– Nick Bull
Jan 31 at 20:54