На работе я работал над проектом, который позволит нам получить повышенную оболочку на компьютере, которым мы владеем, независимо от того, находится ли устройство локально или в доме сотрудника. Наше решение EDR предлагает тип «живого ответа», но ему сильно не хватает возможностей и удобства использования. Однако наш EDR может отправить файл в конечную точку и выполнить его.

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

Как это работает? Я решил применить нетрадиционный подход к модели клиент-сервер с обратной оболочкой. По сути, я применяю архитектуру приложения чата со сквозным шифрованием к обратной оболочке. В моей обратной оболочке есть три узла: клиентская обратная оболочка, клиентский оператор/«слушатель» и всегда активный сервер, который просто соединяет два клиентских соединения.

Вот что происходит в потоке… Сначала обратная оболочка подключается к серверу и аутентифицирует себя, а затем сервер начинает прослушивать порт оператора. Затем оператор подключается к серверу и аутентифицирует себя, а сервер соединяет два соединения. Далее оператор инициирует обмен ключами с обратной оболочкой. Как только ключи шифрования установлены, обратная оболочка запускает оболочку, и БАМ! Где угодно поменять оболочку.

Идея была проста по сравнению с реализацией. Я давно не сталкивался с такой сложной программой. Может быть, с тех пор, как я выбрал компиляторы на первом курсе колледжа. Проблемы с этой обратной оболочкой были неожиданными, и они начались и закончились моим непониманием буферов ввода-вывода.

Моя первая проблема возникла, когда я впервые реализовал базовую архитектуру без какого-либо шифрования. Я использовал объекты файла сокета на стороне клиента для ввода-вывода сокета и не понимал, что запись в буфер файла сокета не отправляет данные автоматически. Через некоторое время я обнаружил, что мне нужно очистить файловый объект после записи.

Я также столкнулся с некоторыми проблемами с буферизацией в моей первоначальной реализации сервера. Мои первые несколько попыток требовали отправки символа новой строки для очистки буфера. В конце концов, я применил черную магию GNU и использовал «cat», чтобы соединить два набора файловых дескрипторов сокетов. Код ниже должен помочь прояснить, что я имею в виду:

fromTarget = targetConnection.makefile("rb")
toTarget = targetConnection.makefile("wb") 
fromOperator = operatorConnection.makefile("rb") 
toOperator = operatorConnection.makefile("wb") 
targetToOperator = subprocess.Popen("cat",
                                    stdin=fromTarget, 
                                    stdout=toOperator, 
                                    stderr=toOperator) 
operatorToTarget = subprocess.Popen("cat",
                                    stdin=fromOperator, 
                                    stdout=toTarget, 
                                    stderr=toTarget)

В конце концов у меня все заработало, как я и планировал, хотя и без шифрования. Добавление шифрования оказалось самой сложной частью проекта.

Сначала я думал, что смогу просто использовать ssl-библиотеку Python, обернуть сокет в слой TLS, и все будет работать так же, как и раньше. Я быстро понял, что это глупое предположение. Использование TLS с двумя отдельными соединениями и последующее их взаимодействие друг с другом создало сценарий Вавилонской башни, в котором ни одна из сторон не могла прочитать сообщения друг друга.

Пытаясь решить эту проблему, я нырнул в довольно глубокую кроличью нору. Я переписал реализацию сервера так, чтобы он читал и затем пересылал сообщения, а не просто передавал их друг другу с помощью cat. Как только я заработал, я столкнулся со странной проблемой. Когда оболочка запускалась, она отправляла заголовок оболочки (дерьмо о версии Microsoft и Windows), но не подсказку. Когда я отправлял команду, она отображала приглашение, а затем вывод. Ниже приведен пример того, что я имею в виду:

Connecting to reverse shell... 
Microsoft Windows [Version 10.0.19041.685] 
(c) 2020 Microsoft Corporation. All rights reserved. 
whoami 
C:\{path to cwd}>mydomain\cjmay

При устранении неполадок я выяснил, что по какой-то причине обратная оболочка даже не отправляла приглашение, пока не получила команду. Поскольку я ничего не менял ни на одном из клиентов с тех пор, как он работал, я решил, что это должна быть проблема с сетевым буфером на сервере. Я очень старался, читал всевозможные онлайн-форумы и несколько раз переписывал сервер, прежде чем понял, что библиотека Python ssl странно взаимодействует с буферами операционной системы и иногда не сообщает программе, что в буфере есть данные. Это даже ломает такие вещи, как использование select в асинхронном процессе получения.

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

Это был мой первый раз, когда я писал обмен ключами для сетевой программы, поэтому мне потребовалось некоторое обучение, чтобы реализовать это. Как только я разобрался с этим, я вернулся к читабельному общению между клиентами. Сразу же мое сердце упало. Кажется, у меня была та же ошибка, что и с библиотекой ssl, где подсказка отображалась после того, как я отправил команду. Но потом я заметил одну небольшую разницу. На этот раз оболочка, казалось, отправляла приглашение до того, как получила команду!

Connecting to reverse shell... 
Performing key exchange... 
Microsoft Windows [Version 10.0.19041.685] 
(c) 2020 Microsoft Corporation. All rights reserved.
whoami 
C:\{path to cwd}>whoami 
mydomain\cjmay

Я провел несколько различных тестов, и это оказалось правдой. Не только оболочка отправляла подсказку до того, как получила команду, но и оператор получал ее! Так почему не печатали? После нескольких попыток и чтения я, наконец, понял это. Был ДРУГОЙ буфер, который блокировал вывод. Это было печатное заявление! Поскольку подсказка не заканчивалась символом новой строки, оператор печати буферизовал все ранее напечатанные символы в этой строке, пока не получил новую строку.

Оказывается, вы можете отключить буферизацию оператора печати, добавив flush=True к аргументам вашей функции печати. После долгого процесса разочарования и роста у меня наконец-то появился работающий продукт.

Connecting to reverse shell... 
Performing key exchange... 
Microsoft Windows [Version 10.0.19041.685] 
(c) 2020 Microsoft Corporation. All rights reserved. 
C:\{path to cwd}>whoami 
mydomain\cjmay

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

  1. Не бойтесь полностью изменить свой код, когда вы действительно застряли
  2. Надеюсь, ты никогда не окажешься в буферном аду

Первоначально опубликовано на https://www.palehat.net 5 января 2021 г.