ТЛ; др
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" \
'su www -c "exec sh -s -- '"'${BB_USER}' '${APP_USER}'"'"' <"./server-user-setup.sh"
Внедрение кода возможно через переменные BB_USER
и APP_USER
.
Длинный ответ
Давайте проанализируем, что происходит. Это довольно сложно. Для ясности предположим:
DO_DROPLET_IP=ip
SSH_PRIVKEY_PATH=key
BB_USER=b-user
APP_USER=a-user
Оригинальный (рабочий) фрагмент
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" sh -s -- < \
"./server-root-setup.sh" "${BB_USER}" "${APP_USER}"
Все переменные раскрываются локально. Перенаправление локальное, и не важно, что оно посередине. С нашими предполагаемыми значениями переменных это эквивалентная команда:
<"./server-root-setup.sh" ssh root@"ip" -i "key" sh -s -- "b-user" "a-user"
ssh
получает следующие аргументы: root@ip
, -i
, key
, sh
, -s
, --
, b-user
, a-user
. Так как root@ip
выглядит как user@hostname
, первый следующий аргумент, не распознанный как option (например, -i
) или option-аргумент (например, key
является аргументом option для -i
), должен запускать массив операндов для построения дистанционная команда от. Указанный аргумент - sh
и это команда, которую ssh
пытается выполнить на удаленной стороне:
sh -s -- b-user a-user
Это запускает sh
который ожидает команды от своего стандартного ввода (из-за -s
). --
это соглашение POSIX, которое сообщает инструменту, что все, что следует, не является опцией (см. это, руководство 10). Таким образом, даже если наш ${BB_USER}
расширится до -f
или около того, sh
не посчитает это вариантом. Следующие операнды задаются как позиционные параметры оболочки ($1
, $2
). Stdin предоставляется ssh
, локальный файл ./server-root-setup.sh
в потоковом режиме.
Если файл и две соответствующие переменные были доступны на удаленной стороне, вы могли бы получить (почти) тот же результат с помощью следующей команды:
sh "./server-root-setup.sh" ${BB_USER} ${APP_USER}
Или, если в сценарии есть правильный шебанг:
./server-root-setup.sh ${BB_USER} ${APP_USER}
Таким образом, фрагмент - это действительно способ запустить локальный скрипт на удаленной стороне. Но обратите внимание, я намеренно не цитировал ${BB_USER}
или ${APP_USER}
. В оригинальном фрагменте они цитируются на локальной стороне, но это не имеет значения, потому что ssh
создает и передает команду как строку для анализа на удаленной стороне. Если любая из двух переменных содержит внутренние пробелы, удаленный sh
получит больше аргументов, чем вы ожидали; ведущие или конечные пробелы будут потеряны. Такие символы, как $
, "
, '
или \
могут вызвать проблемы, если только значения переменных не были специально разработаны для анализа (но тогда их нельзя было использовать локально для получения эквивалентного результата, поэтому я написал" почти ").
Хуже всего будет, если, например, ${APP_USER}
расширится до a-user& rogue_command
. На удаленной стороне вы получите
sh -s -- b-user a-user& rogue_command
Обратите внимание, что такое внедрение кода не может произойти, когда вы запускаете вещи локально (с кавычками или без них)
./server-root-setup.sh ${BB_USER} ${APP_USER}
потому что разделители как &
и ;
распознаются до раскрытия переменных, а не после. Но если вы расширяете переменные локально, а затем полученная строка снова анализируется на удаленной стороне, это совсем другая история. Это как удаленный eval
.
Я предполагаю, что ${BB_USER}
и ${APP_USER}
- это имена пользователей, довольно безопасные строки, поэтому все работает, несмотря на возможные общие проблемы. Но если эти переменные не полностью контролируются вами, такой подход небезопасен.
Ваша (неудачная) попытка
ssh root@"${DO_DROPLET_IP}" -i "${SSH_PRIVKEY_PATH}" su www -c sh -c -- \
"./server-user-setup.sh" "${BB_USER}" "${APP_USER}"
На этот раз перенаправления нет. Снова переменные раскрываются локально, и эквивалентная команда выглядит следующим образом:
ssh root@"ip" -i "key" su www -c sh -c -- "./server-user-setup.sh" "b-user" "a-user"
ssh
пытается запустить это на удаленной стороне:
su www -c sh -c -- ./server-user-setup.sh b-user a-user
Обратите внимание, что все аргументы теперь являются аргументами su
. В этот момент sh
- это просто аргумент-опция для опции -c
, аналогично --
это аргумент-опция для другой опции -c
(поэтому здесь не указывается конец опции). Мои тесты показывают, что из нескольких параметров -c
в su
действует только последний. Это означает, что -c sh
отбрасывается, su
использует любую оболочку, указанную в (удаленном) /etc/passwd
для пользователя www
. Это может или не может быть sh
. Допустим, это some-shell
. Это то, что будет дальше (как пользователь www
):
some-shell -c -- # but wait, it's not all
с остальными операндами su
, так
some-shell -c -- ./server-user-setup.sh b-user a-user
Если some-shell
- это обычная оболочка типа sh
или bash
, она ведет себя одинаково. Здесь -c
не принимает параметр-аргумент (см. Спецификацию), поэтому --
указывает конец параметров. В этом случае следующий аргумент не может быть принят в качестве опции, поэтому мы можем опустить --
:
some-shell -c ./server-user-setup.sh b-user a-user
Это как
sh -c [-abCefhimnuvx] [-o option]... [+abCefhimnuvx] [+o option]... command_string [command_name [argument...]]
или (отказываясь от всего, что мы не используем)
some-shell -c command_string command_name argument
Так ./server-user-setup.sh
является command_string, b-user
command_name и a-user
является аргументом.
-c
Чтение команд из операнда command_string
. Задайте значение специального параметра 0
[…] из значения операнда command_name
и позиционных параметров ($1
, $2
и т.д.) В последовательности от оставшихся операндов аргумента. [...]
По сути, удаленная some-shell
пытается прочитать (source) ./server-user-setup.sh
со строкой a-user
доступной как $1
. Специальный параметр 0
(теперь со значением b-user
) - это то, что оболочка считает своим собственным именем. Имя используется в сообщениях об ошибках, подобных этому:
b-user: ./server-user-setup.sh: Permission denied
Очевидно, что на удаленной стороне находится ./server-user-setup.sh
но пользователь www
не может его прочитать.
Мой подход (все еще небезопасно):
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
Последняя строка (комментарий) просто для перечисления цитат в предыдущей строке. Чтобы понять, как работает команда, нам нужно "расшифровать" это безумное цитирование. Важные вещи:
- Все в
1
и 3
одинарных кавычках следует понимать буквально.
${BB_USER}
и ${APP_USER}
в двойные кавычки 2
; "внутренние" одинарные кавычки относятся к строке в кавычках, они не препятствуют раскрытию переменных.
- Везде, где закрывающая кавычка непосредственно предшествует открывающей кавычке (например, кавычки выше
12
), строки в кавычках будут объединены.
Зная это, мы можем сказать, что команда с расширенными переменными выглядит примерно так:
<"./server-user-setup.sh" ssh root@"ip" -i "key" 'su www -c "exec sh -s -- Xb-userX Xa-userX"'
где X
обозначает (только для нас, здесь и сейчас) символ одинарной кавычки, который принадлежит строке, начинающейся с su
и заканчивающейся последней "
. Извините, я не мог выразить это более ясно. Надеюсь, это будет менее загадочно, когда мы увидим, что получит ssh
:
root@ip
, -i
, key
, su www -c "exec sh -s -- 'b-user' 'a-user'"
Последний аргумент содержит двойные кавычки и одинарные кавычки. Именно эта строка ssh
будет запускаться на удаленной стороне. Примечание. Я позаботился о том, чтобы передать всю строку как один аргумент в ssh
, чтобы инструмент не создавал строку из нескольких аргументов и пробелов; Таким образом, я полностью контролирую строку. В общем, это было бы важно, например, если бы ${BB_USER}
расширился до чего-либо с завершающим пробелом (в этом случае исходный фрагмент не получится).
Команда, которая выполняется на удаленной стороне:
su www -c "exec sh -s -- 'b-user' 'a-user'"
Поведение su
уже обсуждалось. Обратите внимание, что вся строка в кавычках является опцией-аргументом для -c
. Если бы не кавычка, -s
был бы вариантом su
. Вот что работает от имени пользователя www
:
some-shell -c "exec sh -s -- 'b-user' 'a-user'"
Обратите внимание, что кавычки в вызове su
также приводят к тому, что нет command_name
и argument
(ов) (сравните аргумент some-shell -c command_string command_name argument
где-то выше). Тогда some-shell
просто запускается:
exec sh -s -- 'b-user' 'a-user'
Это будет работать без exec
, но, поскольку нам больше не нужна some-shell
, exec
может быть хорошей идеей. Это заставляет sh
заменить some-shell
, поэтому вместо some-shell
и sh
как его потомка остается только sh
, как если бы мы запускали some-shell
мы имели
sh -s -- 'b-user' 'a-user'
Теперь это очень похоже на исходный фрагмент кода (sh -s -- b-user a-user
). Благодаря дополнительным кавычкам, возможные пробелы или несколько других проблемных символов из ${BB_USER}
или ${APP_USER}
могут ничего не сломать. Однако '
или "
будет. Внедрение кода все еще возможно. Рассмотрим расширение ${APP_USER}
до "& rogue_command;"
, Первое, что нужно запустить на удаленной стороне, будет:
su www -c "exec sh -s -- 'b-user' '"& rogue_command;"'"
Неважно, some-shell
выдаст ошибку, rogue_command
все равно будет работать. Убедитесь, что ваши переменные расширены до безопасных строк.