D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор Snow
Источник https://xss.is
НАЧАЛО ТУТ
В этой части у нас:
• Проверяем архитектуру на прочность – попытка впихнуть невпихуемое – прикручиваем модуль сбора сетевых ресурсов.
• Вспомогательные утилиты – рассмотрим логгер, обработку аргументов командной строки, дополнительные инструменты, скрывающие функционал под капотом и позволяющие писать
красивый и расширяемый код. (Оставил только парсер аргументов, т.к. остальное достаточно примитивно реализовано и мне стыдно это показывать людям)
• А что будет если ... Размышляем на тему возможности расширения продукта для сбора информации не только при прямом подключении.
• Повторная проверка архитектуры на прочность. Прикручиваем модуль для массовой обработки сетей.
• Кирпич, кирпич, еще кирпич – а оно не работает. Или как я боролся с превращением VPS в кирпич после первого подключения к VPN.
• Опять про архитектуру. Продолжаем проектировать модуль взаимодействия с VPN.
• Красивый видос с результатами работы.
Поехали.
1 ShareEnumerator
В прошлый раз мы спроектировали модуль сбора дампов. Сейчас нам необходимо сюда же добавить сбор информации об общих сетевых ресурсах. Естественно, что обзовем мы его не иначе как ShareEnumerator.
Опять начинаем рассуждать вслух.
Для начала необходимо понять что мы хотим. Затем подумать над тем как это реализовать с учетом уже имеющейся архитектуры. Какие инструменты мы будем для этого использовать.
1.1 Учите матчасть товарищи.
Как и в случае с предыдущей задачей было бы наивно полагать, что я первый, кому это понадобилось. Поэтому начинаем гуглить. На гитхабе различных Shareenumerator’ов пруд пруди. Нам они, по большей части, не интересны. У нас своя коза на лисапеде. Но вот подглядеть инструменты которыми пользовались их авторы – совсем не грех.
После изучения нескольких проектов становится понятно, что задачу можно разбить на две подзадачи:
• Собрать список компьютеров в сети.
• Опросить каждый найденный компьютер по протоколу SMB на предмет наличия шар.
Смотрим внимательно на пункт 1 и понимаем, что эта часть у нас есть замечательный LDAPDataCollector, который умеет очень многое. В том числе и получать список хостов. На данном этапе задачу можно
считать решенной, останется только подумать как это потом аккуратно вставить в код, чтобы ничего не сломать.
Внимательно смотрим на второй пункт. Вроде бы ничего сложного. Осталось только понять что это за самба такая и с чем ее едят. Открываем вики и видим следующее: SMB (сокр. от англ. Server Message Block) — сетевой
протокол прикладного уровня для удалённого доступа к файлам, принтерам и другим сетевым ресурсам, а также для межпроцессного взаимодействия.
Тут же описан принцип его работы:
SMB — это протокол, основанный на технологии клиент-сервер, который предоставляет клиентским приложениям простой способ для чтения и записи файлов, а также запроса служб у серверных программ в различных типах сетевого окружения. Серверы предоставляют файловые системы и другие ресурсы (принтеры, почтовые сегменты, именованные каналы и т. д.) для общего доступа в сети. Клиентские компьютеры могут иметь у себя свои носители информации,
но также имеют доступ к ресурсам, предоставленным сервером для общего пользования.
Возникает извечный вопрос: «Что делать?». У нас опять есть два пути. Писать свою реализацию взаимодействия с самбой – задача прикольная и сложная. Оно конечно хорошо, но перед тем, как сделать что-либо я всегда задаю себе другой вопрос: «а нахрена?». Если внятного ответа не удалось получить, то значит оно мне и не надо особо. Ладно, это всё лирика. Делать то что? Опять лезем в гугл. Точнее на гитхаб. И там находим вот такую штуку https://github.com/ShawnDEvans/smbmap.
Вроде бы она даже умеет делать то, что нам нужно. Пытаемся понять. После этой строчки:
Python: Скопировать в буфер обмена
Мой мозг сломался. Ушел в ребут. А там почти 2к строк кода. По возвращении в реальность я внимательно посмотрел в раздел импорта. И обнаружил что ребята активно используют impacket. Как известно, люди делятся на 3 основных категории:
• умные – учатся на чужих ошибках, у них всегда хорошо. В эту категорию очень сложно попасть
минуя остальные. Но это, опять же, к теме не относится.
• дураки – учатся на своих ошибках. Есть даже гипотеза что опытный дурак может стать умным
• долбо*бы – эти вообще не обучаемые.
1.2 Проектируем модуль сбора шар
Поскольку список компутеров мы уже умеем получать, то логично его передать в конфиг нашего будущего продукта. Под продуктом, в данном контексте, мы понимаем наследник класса AbstractProduct,
который мы спроектировали в первой части. Это с одной стороны. А с другой - «а нахрена?». Попробуем разобраться с теми параметрами, которые мы должны получить для получения списка
доступных сетевых ресурсов.
Поехали. У нас получится два блока параметров.
1. Параметры LDAPDataCollector
• domain name;
• domain controller address;
• username;
• password;
• флаг для подключения через SSL. По умолчанию установим его в False и пока что забудем
о нем.
2. Параметры для шар.
Давайте опять рассуждать вслух. Что нам может понадобиться и нужно ли оно нам здесь и сейчас? Предполагаем, что полный путь (имя шары мы уже получили).
• Проверка прав на чтение и запись. Причем каждую проверку мы будем выполнять отдельно. Поэтому автоматически у нас появляется два флага: is_check_read_access_enabled
и is_check_write_access_enabled
• количество потоков – если компутеров в сети много, то очень желательно работать в несколько потоков. Но если там сидит какой-то злой авер, то надо постараться не разбудить его. И контроль данного параметра нам в этом немножко поможет.
• таймаут – причина та же, что и пунктом выше. Устанавливаем желаемый временной интервал между запросами.
• формат файла сохранения результатов – полученные результаты надо куда-то сохранять. Как я буду это делать – пока не знаю, но точно знаю что результаты должны быть пригодны или для последующей машинной обработки, или же представлены в удобочитаемом виде. То есть в итоге у нас будет минимум 3 выходных формата: csv и json – для последующей машинной обработки, а также xlsx – если мы захотим посмотреть результаты глазками.
Итого у нас получилось два не пересекающихся набора параметров.
И куда же тут, спрашивается, пристроить список компутеров, если мы его получаем в процессе работы, а не передаем из файла? Правильный ответ – нечего ему в конфиге делать. У нас есть набор параметров, позволяющий его получить. Этого более чем достаточно. Но тут вот какая штука. При изучении документации класса
SmbConnection выяснилось, что авторизоваться мы можем двумя способами:
1. используя логин и пароль – вот вам и ответ на часть вопроса;
2. через Kerberos – этот случай я рассматривать здесь не буду, но в проекте он реализован;
Для авторизации через кербу придется тянуть еще параметры. Итак, конфиг для модуля сбора шар у нас будет следующий:
Python: Скопировать в буфер обмена
1.2.1 ShareFinder. Шары есть? А если найду?
Как уже говорилось ранее, собирать шары мы будем используя протокол SMB. Опять задаемся вопросом: «Что необходимо для решения задачи?» и пробуем на него ответить. Очевидно, что нам
понадобится:
1. установить подключение к серверу;
2. авторизоваться;
3. выполнить запрос;
4. сохранить результаты для последующей обработки;
5. отключиться.
Еще надо бы не забыть про многопоточность и таймауты.
Поехали.
Объявим новый класс и обзовем его ShareFinder. Ему в конструктор мы передадим наш конфиг. Там же, в конструкторе, объявим поле domain_computers в виде списка. Его мы заполним снаружи, когда отработает коллектор.
Еще нам понадобится некая структура данных, для сохранения результатов. Очевидно, что это у нас будет словарь, ключом которого будет имя компутера, а значением – список принадлежащих ему шар.
Получится примерно следующее:
Python: Скопировать в буфер обмена
Теперь про многопоточность. Воспользуемся стандартным пулом потоков. Вот что говорит его документация.
Python: Скопировать в буфер обмена
То есть именно сюда мы передадим количество потоков. А что собственно мы будем делать в потоках. Правильно – опрашивать каждый компутер на предмет шар и записывать результаты в общую коллекцию результатов. Не забыв, конечно, при этом про то, что мы работаем с общим ресурсом, а значит каждый поток должен уметь блокировать ресурс на запись.
Для реализации этого функционала нам понадобится метод, назовем его worker. который мы скормим нашему пулу.
В итоге получилось примерно следующее.
Python: Скопировать в буфер обмена
Как видите, таймауту тоже нашлось применение.
Теперь рассмотрим сам метод worker. Он достаточно простой, поэтому просто приведу его код.
Python: Скопировать в буфер обмена
На этом реализацию можно считать законченной.
1.3 Конфигурация строителя и сборка продукта.
1.3.1 ShareEnumModeConfigurator.
Нам осталось указать строителю параметры сохранения результата. Здесь будет минимум кода.
Python: Скопировать в буфер обмена
1.3.2 ShareEnumBuilder.
Мы строили, строили и наконец построили.
Здесь тоже ничего сложного. Мы сначала попросим LdapDataCollector собрать список компутеров в сети. Затем обратимся к ShareFinder’у с просьбой собрать шары. А результат запишем в продукт, который сам уже знает как у куда его сохранить. На всё нам понадобится всего 20 строк. Магия, однако.
Python: Скопировать в буфер обмена
В завершение рассмотрим метод save продукта ShareEnumerator. Ему на вход прилетает результат в следующем формате:
JSON: Скопировать в буфер обмена
Здесь мы тоже уложились в 15 строк. Опять магия? Нет. Всего лишь грамотно спроектированная архитектура.
Python: Скопировать в буфер обмена
1.3. Выводы.
Модуль перечисления общих сетевых ресурсов внешне не похож на дампы, описанные в предыдущей статье. Тем не менее нам удалось без особых проблем встроить его в имеющуюся архитектуру.
Это означает, что подход был выбран верный. Архитектура может и не идеальная, но достаточно устойчивая. Нам удалось разделить реализацию и бизнес-правила. На этом мы не заканчиваем.
После рассмотрения реализации вспомогательных утилит мы попробуем расширить продукт и использовать его для массового сканирования сетей.
2 ArgumenParser. Метод определенного интеграла в действии.
Метод определенного интеграла – любую сложную задачу необходимо разбить на конечное множество простых подзадач, каждая из которых имеет константное время выполнения. Еще его называют «разделяй и властвуй» или декомпозиция. Попробуем и здесь его применить.
Как я уже говорил в первой части, реализация разбора аргументов командной строки во многих опенсорсных проектах оставляет желать лучшего. Пример я выше приводил. Такой хоккей нам не нужен. Поэтому будем делать всё по науке.
Поехали.
2.1 Наброски архитектуры.
Для начала вспомним что у нас есть несколько режимов работы, каждый из которых просит свой набор параметров. Для дампов эти параметры будут одни и те же, за исключением типа самого дампа. Для сбора шар у нас добавятся дополнительные параметры, о чем я писал выше. Всю эту кашу необходимо каким-то образом расхлебать, не нарушив основной архитектуры. Но об этом чуть позже. А пока зададим себе следующий вопрос. С чего начинается любая программа? Правильно. Со справки или мануала. Очень важно чтобы обычный пользователь, впервые использующий наш
продукт и набравший в консоли
Bash: Скопировать в буфер обмена
Получил максимально четкое и подробное описание всех режимов работы приложения, а не просто красивую ASCII картинку, которую так любят все опенсорсники. Поэтому наш модуль должен уметь делать красивую справку.
Что еще? По хорошему это должен быть инструмент, работающий по принципу: «Один раз настроил и забыл». Следовательно он должен иметь некий инструмент настройки. Хмм... А у нас ведь уже есть конфигуратор. А что если ... Мысль вроде дельная, но об этом чуть позже.
Еще нам нужно чтобы под капотом можно было задавать произвольные наборы параметров и группировать их по некоему общему признаку. То есть у нас будет, как минимум, группы параметров для дампов и шар. Но что нам взбредет в голову завтра – большой секрет. Значит мы должны и здесь заложить возможность расширения.
Идем дальше. Хорошо, параметры мы распарсили. Что с ними делать? Вернуть. А что возвращать? Безразмерный кортеж? Стоп. У нас же для каждого режима есть конфиги. Почему бы не возвращать их. В этом уже просматривается некий смысл. То есть мы разобрали параметры, сформировали конфиг и тут же проверили его валидность. Если нам сунули фуфло, то мы об этом сразу и заявляем, а не передаем это дальше. Отсюда просится еще и корректная обработка ошибок. Ладно, достаточно слов. Начнем пожалуй.
2.2 Разбор аргументов командной строки.
У питона, при всей моей не любви к нему, есть одно преимущество. Огромное количество пакетов, причем многие из них очень качественные. То есть их можно взять и использовать из коробки. Что мы и сделаем. В основу нашего парсера аргументов положим стандартный питоновский парсер. Кому интересно – читайте документацию. Я же здесь расскажу о том, как я в очередной раз впихнул внешне не впихуемое. А именно – встроил парсер в архитектуру, таким образом, что прогнав его через конфигуратор мы получили готовый к использованию продукт.
2.2.1 Настраиваем стандартный питоновский парсер под наши нужды
Очевидно, что у нас будет две группы параметров – обязательные: всё, что относится к LDAP и опциональные – дополнительные параметры, например, того же ShareEnumerator’a. Итак, объявим класс ArgsParserConfigurator, производный от AppConfigurator. У него будет ровно одно собственное поле. Это объект класса argsparse.ArgumentParser. Его мы и будем настраивать.
Конструктор у нас будет такой:
Python: Скопировать в буфер обмена
Теперь переопределим метод базового класса setup и посмотрим на часть его реализации. Она предельно проста.
Python: Скопировать в буфер обмена
На примере метода __setup_required_params() рассмотрим настройку обязательных параметров.
Python: Скопировать в буфер обмена
Что мы собственно говоря сделали. Вынесли обязательные аргументы в отдельную группу. Для каждого параметра указали тип, описание, имя флага, добавили справку. Аналогичную работу нужно провести для опциональных параметров. По сути в качестве готового продукта мы взяли стандартный парсер и настроили его в конфигураторе. Конфигуратор мы тоже будем создавать с помощью фабрики. Для этого добавим в нее еще один метод.
Python: Скопировать в буфер обмена
Возможно не самое удачное архитектурное решение, но давайте посмотрим как оно впишется в
общую картину.
2.2.2 Добываем конфиги
После настройки парсера мы уже почти готовы к работе. Осталось только добавить немного магии и получить из аргументов конфиги. Чем мы собственно и займемся. Чтобы вся порнография настройки парсера не торчала наружу мы ее прикроем чутка. И поместим уже настроенный парсер в класс который так и назовем ConfigMiner. А на вход его конструктора передадим наш уже настроенный парсер.
Python: Скопировать в буфер обмена
Этот класс должен уметь делать ровно две вещи:
1. дать команду парсеру на разбор;
2. собрать из полученных параметров нужный конфиг и быть готовым вернуть его по первому
запросу.
Собственно всё. Ниже я привел его код и пример его использования. Думаю на этом достаточно про парсер. Конструктор:
Python: Скопировать в буфер обмена
Метод старт, который запускает последовательно обе перечисленные задачи.
Python: Скопировать в буфер обмена
И, наконец, пример использования всего этого добра.
Python: Скопировать в буфер обмена
2.3 Выводы.
Не смотря на то, что работа с аргументами командной строки, как вы могли видеть, – то еще удовольствие, нам удалось относительно безболезненно встроить ее (обработку аргументов командной строки) не нарушая целостности архитектуры приложения. Возможно решение и не самое удачное, но оно рабочее и большую часть времени реализации заняло прописывание справок для каждого параметра, нежели непосредственно написание кода.
3 Одна сеть хорошо, а много лучше.
Ну вот и получился уже вполне вменяемый продукт, которым можно пользоваться и людям не стыдно показать. Впска с любым линуксом на борту,proxychains до сети и побежали.
Для тестов – это однозначно плюс. А если приложить чуть усилий, то можно настроить удаленную отладку и вообще красиво получится.
Однако, что делать, если сетей много и большинство из них – это фортики и циски. Конечно, можно подключаться руками через официальные клиенты. Оно таки да, можно. Если у вас 3-5 сетей в неделю проходит. И вам достоверно известно, они 100% валидные: учетка живая и юзер в домене хотя бы. В противном случае, когда сети идут из логов – у меня рука отвалилась мышкой тыкать через два часа.
Что делать?
Давайте рассуждать вслух. Что мы имеем? У нас есть продукт, который абсолютно не зависит от метода подключения. То есть мне не важно как осуществлено подключение. Это хорошо. Теперь посмотрим какие методы подключения возможны. Рассмотрим два основных:
1. VPN
2. Proxy
Начнем со второго. Здесь все должно быть относительно просто. Если у нас есть список доступов через прокси, то мы можем написать внешний скрипт, который будет генерировать конфиг для proxychains и подключаться через него. Должно получиться нечто подобное:
Код: Скопировать в буфер обмена
Поскольку этот способ менее актуальный, я не реализовал его. Гораздо больше нас интересует воз-
можность подключения через VPN. Посмотрим что тут можно сделать.
3.1 OpenConnect. Что это такое и с чем его едяcт.
Дабы не разводить тут дальнейшую лабуду про выбор инструментов. Остановимся на OpenConnect. Отличный продукт, поддерживающий несколько протоколов, в частности:
• Cisco’s AnyConnect SSL VPN;
• Fortinet Fortigate SSL VPN;
• Palo Alto Networks GlobalProtect SSL VPN;
Есть и другие, но я их не тестировал, слишком редкие звери. Поправьте если я не прав, но в живой природе я их не встречал. Отлично – это то, что нужно. Открываем документацию и видим, что для подключения нам необходимо указать несколько параметров, таких как:
• username;
• password;
• address;
• vpn group – необязательный параметр, указывающий принадлежность пользователя к какой-
либо группе;
• protocol – протокол, по которому будет выполняться подключение. По умолчанию используется Cisco anyconnect. Остальные необходимо указывать явно. Например, –protocol=fortinet
Отлично. Берем первый попавшийся доступ, устанавливаем openconnect на впску и пробуем подключиться. У нас получилась примерно такая строка для подключения:
Bash: Скопировать в буфер обмена
Запускаем. Попросил пароль. Вводим. Делаем заметку о том, что надо автоматизировать ввод пароля. Еще просит проверить отпечаток. Говорим, что ок, тоже ставим пометку про автоматизацию. Подключились. Пробуем запуститься. Ура. Заработало. Отлично. Теперь дело за малым – автоматизировать всё.
3.2 Автоматизация подключения.
Не вдаваясь особо в детали расскажу про основные проблемы возникшие при автоматизации подключения.
Поехали.
Запускаться мы будем через subprocess.
3.2.1 Ввод пароля и отпечатка
Здесь получилось обойтись относительно малой кровью, за что отдельная благодарочка разработчикам продукт. Для ввода пароля воспользуемся встроенным флагом –passwd-on-stdin.
С отпечатком чуть сложнее. Но не более. Мы просто вызовем подключение дважды. Первый раз вычленим из ответа сам отпечаток и запомним его, а во время второго запуска передадим его как
параметр. В итоге получится следующая строка:
Флаг -b (background) запускает openconnect в фоне и позволяет выполнять произвольные операции при подключении.
3.2.2 Отключение.
Отлично. Мы подключились, даже отработали. А вот как теперь отключиться? Да вали его нахрен, да и всё – это пришедшее в голову первое решение. Так и сделаем. Запускаем htop. Ищем процесс и убиваем его. Ура. Вроде всё ок. Но это ручками. А вот как автоматизировать. А тут очередная похвала разработчикам продукта. Они решили эту проблему за меня – если указать еще и флаг –pid-file, то он любезно запишет туда свой PID. Нам останется лишь прочитать его и грохнуть.
А строка подключения у нас теперь такая:
3.2.3 Большой облом. Или как я построил большую индейскую национальную избу из окирпиченных VPS.
Вау. Всё круто, теперь делов-то. Написать пару классов, заложившись, как обычно, под возможность расширения и будет нам счастье. «Ага, щаззз», подумали про себя разработчики фортика и злобно так засмеялись.
Я набросал скрипт, который подключается через openconnect. Выполняет пару рутинных операций и отключается. В файл накидал доступов для тестирования разных протоколов. Стартую. Первая циска пошла. Всё ок. Вторая, третья. Всё работает. Пошел покурить, думаю: "приду, воткну всё это в проект и вопрос закрыт". «А вот фигвам», сказали разработчики фортика.
По возвращении обнаружил, что впска не подает признаков жизни. В логах видно, что несколько цисок я обработал корректно, а на фортике чет сломалось. Ну я решил, что это глюк какой. Пробую подключиться с другого терминала. Тут будет картинка про фигвам из простоквашино. Да что за хрень? Меняю свои впны. Не работает. Нет подключения, хоть ты тресни. Ладно, хрен с тобой, золотая рыбка. Делаем реинсталл. Запускаем повторно тот же файл, но добавляем больше логов. Те же яйца, только в профиль. После подключения к фортику перестает подавать признаки жизни. Раз 15 я бился башкой об эти кирпичи. Безрезультатно. Это уже начинает напрягать. Вооружаемся гуглом, обкладываемся манами по сетям, впнам, openconnect’у.
В общем через некоторое время удалось выяснить, что многие впны меняют настройки сети, делая ее недоступной из внешнего мира. Поэтому и кирпич вместо впски при удачном подключении. Что делать? Опущу свои приключения на пути поиска решения. Это было очень интересно, но описание займет много времени.
А вот решение оказалось достаточно простым. Если подключаться напрямую из впски, то окирпичивание ее гарантировано. А вот если взять контейнер и всю работу с подключением поместить в него, то всё будет окей. То есть я взял LXD-container. закинул свой софт в него, а контейнер на впску. Всё. Теперь если впн и убьет мне подключение, то доступ к впс. а следовательно и к контейнеру у меня останется. Теперь пришла моя очередь радоваться жизни.
Я использовал этот контейнер.
3.2.4 Выводы.
С помощью гугла и какой-то матери нам удалось более-менее корректно реализовать автоматизацию подключения и отключения. Лично я узнал очень много нового о принципах работы сетей и впнов, а также особенностях
реализации тех или иных моментов. Удалось совместить ранее изученную теорию и практику.
3.3 Проектирование архитектуры и реализация
После всех танцев с бубнами нам удалось получить минимально рабочую версию модуля внешнего подключения. Теперь ее необходимо довести до ума и встроить в проект, ничего при этом не сломав.
Поехали.
3.3.1 Connector.
На данном этапе мы умеем работать с openconnect. Это хорошо. А что если нам понадобится использовать протокол, не поддерживаемый им? А там еще что-то говорилось про прокси. Сейчас это не нужно, но потом может и понадобится. При этом я опять понятия не имею как это будет выглядеть. Опять сюда просится фабрика коннекторов. Давайте прикинем базовый функционал коннектора:
• Подключение – тут всё очевидно. Нам понадобится, как минимум, 3 параметра, которые будут
обязательно присутствовать для любого типа соединения. Это адрес, имя пользователя и пароль.
• Отключение – эта штука должна уметь отключаться и корректно восстанавливать параметры
сети, если впн их сломал.
• Обработка ошибок – если что-то пошло не так, мы должны получить об этом максимально подробную информацию.
Вот у нас и готов интерфейс базового класса.
Python: Скопировать в буфер обмена
А вот и его реализация в классе OpenConnector.
Python: Скопировать в буфер обмена
Как видим, у нас опять получился достаточно читаемый и компактный код, не содержащий ничего лишнего. добавим его в проект и продолжим радоваться жизни.
3.3.2 Интеграция в проект
У нас добавился новый модуль. А следовательно добавились и новые аргументы командной строки. Это единственный модуль в коде которого нам придется сделать изменения. И у нас на это есть действительно веская причина. Мы же помним про SOLID.
Если совсем коротко, то я добавил в аргументы тип подключения и метод получения аргументов - из командной строки или из файла. В файле мы храним наши впны. В идеале, конечно, сделать конфиг файл, а к нему конфигуратор с гуем, но это совсем другая история. В зависимости от выбранного режима подключения мы либо начинаем работать сразу, предполагая, что подключение уже установлено и если что-то с ним не так, то это уже не наша проблема, либо
же сначала подключаемся указанным способом и, в случае успешного подключения, начинаем работать. При возникновении ошибки тщательно пишем ее в логи и берем следующий набор параметров для подключения.
Вот и вся магия.
3.4 Выводы.
Одно видео вместо тысячи слов.
Если посоветуете другой файл-сервер - перезалью.
За сим позвольте откланяться.
Источник https://xss.is
НАЧАЛО ТУТ
В этой части у нас:
• Проверяем архитектуру на прочность – попытка впихнуть невпихуемое – прикручиваем модуль сбора сетевых ресурсов.
• Вспомогательные утилиты – рассмотрим логгер, обработку аргументов командной строки, дополнительные инструменты, скрывающие функционал под капотом и позволяющие писать
красивый и расширяемый код. (Оставил только парсер аргументов, т.к. остальное достаточно примитивно реализовано и мне стыдно это показывать людям)
• А что будет если ... Размышляем на тему возможности расширения продукта для сбора информации не только при прямом подключении.
• Повторная проверка архитектуры на прочность. Прикручиваем модуль для массовой обработки сетей.
• Кирпич, кирпич, еще кирпич – а оно не работает. Или как я боролся с превращением VPS в кирпич после первого подключения к VPN.
• Опять про архитектуру. Продолжаем проектировать модуль взаимодействия с VPN.
• Красивый видос с результатами работы.
Поехали.
1 ShareEnumerator
В прошлый раз мы спроектировали модуль сбора дампов. Сейчас нам необходимо сюда же добавить сбор информации об общих сетевых ресурсах. Естественно, что обзовем мы его не иначе как ShareEnumerator.
Опять начинаем рассуждать вслух.
Для начала необходимо понять что мы хотим. Затем подумать над тем как это реализовать с учетом уже имеющейся архитектуры. Какие инструменты мы будем для этого использовать.
1.1 Учите матчасть товарищи.
Как и в случае с предыдущей задачей было бы наивно полагать, что я первый, кому это понадобилось. Поэтому начинаем гуглить. На гитхабе различных Shareenumerator’ов пруд пруди. Нам они, по большей части, не интересны. У нас своя коза на лисапеде. Но вот подглядеть инструменты которыми пользовались их авторы – совсем не грех.
После изучения нескольких проектов становится понятно, что задачу можно разбить на две подзадачи:
• Собрать список компьютеров в сети.
• Опросить каждый найденный компьютер по протоколу SMB на предмет наличия шар.
Смотрим внимательно на пункт 1 и понимаем, что эта часть у нас есть замечательный LDAPDataCollector, который умеет очень многое. В том числе и получать список хостов. На данном этапе задачу можно
считать решенной, останется только подумать как это потом аккуратно вставить в код, чтобы ничего не сломать.
Внимательно смотрим на второй пункт. Вроде бы ничего сложного. Осталось только понять что это за самба такая и с чем ее едят. Открываем вики и видим следующее: SMB (сокр. от англ. Server Message Block) — сетевой
протокол прикладного уровня для удалённого доступа к файлам, принтерам и другим сетевым ресурсам, а также для межпроцессного взаимодействия.
Тут же описан принцип его работы:
SMB — это протокол, основанный на технологии клиент-сервер, который предоставляет клиентским приложениям простой способ для чтения и записи файлов, а также запроса служб у серверных программ в различных типах сетевого окружения. Серверы предоставляют файловые системы и другие ресурсы (принтеры, почтовые сегменты, именованные каналы и т. д.) для общего доступа в сети. Клиентские компьютеры могут иметь у себя свои носители информации,
но также имеют доступ к ресурсам, предоставленным сервером для общего пользования.
Возникает извечный вопрос: «Что делать?». У нас опять есть два пути. Писать свою реализацию взаимодействия с самбой – задача прикольная и сложная. Оно конечно хорошо, но перед тем, как сделать что-либо я всегда задаю себе другой вопрос: «а нахрена?». Если внятного ответа не удалось получить, то значит оно мне и не надо особо. Ладно, это всё лирика. Делать то что? Опять лезем в гугл. Точнее на гитхаб. И там находим вот такую штуку https://github.com/ShawnDEvans/smbmap.
Вроде бы она даже умеет делать то, что нам нужно. Пытаемся понять. После этой строчки:
Python: Скопировать в буфер обмена
Код:
if not args.file_content_search:
if not args.dlPath and not args.upload and not args.delFile and not args.list_drives and not args.command and not args.version and not args.signing:
Мой мозг сломался. Ушел в ребут. А там почти 2к строк кода. По возвращении в реальность я внимательно посмотрел в раздел импорта. И обнаружил что ребята активно используют impacket. Как известно, люди делятся на 3 основных категории:
• умные – учатся на чужих ошибках, у них всегда хорошо. В эту категорию очень сложно попасть
минуя остальные. Но это, опять же, к теме не относится.
• дураки – учатся на своих ошибках. Есть даже гипотеза что опытный дурак может стать умным
• долбо*бы – эти вообще не обучаемые.
1.2 Проектируем модуль сбора шар
Поскольку список компутеров мы уже умеем получать, то логично его передать в конфиг нашего будущего продукта. Под продуктом, в данном контексте, мы понимаем наследник класса AbstractProduct,
который мы спроектировали в первой части. Это с одной стороны. А с другой - «а нахрена?». Попробуем разобраться с теми параметрами, которые мы должны получить для получения списка
доступных сетевых ресурсов.
Поехали. У нас получится два блока параметров.
1. Параметры LDAPDataCollector
• domain name;
• domain controller address;
• username;
• password;
• флаг для подключения через SSL. По умолчанию установим его в False и пока что забудем
о нем.
2. Параметры для шар.
Давайте опять рассуждать вслух. Что нам может понадобиться и нужно ли оно нам здесь и сейчас? Предполагаем, что полный путь (имя шары мы уже получили).
• Проверка прав на чтение и запись. Причем каждую проверку мы будем выполнять отдельно. Поэтому автоматически у нас появляется два флага: is_check_read_access_enabled
и is_check_write_access_enabled
• количество потоков – если компутеров в сети много, то очень желательно работать в несколько потоков. Но если там сидит какой-то злой авер, то надо постараться не разбудить его. И контроль данного параметра нам в этом немножко поможет.
• таймаут – причина та же, что и пунктом выше. Устанавливаем желаемый временной интервал между запросами.
• формат файла сохранения результатов – полученные результаты надо куда-то сохранять. Как я буду это делать – пока не знаю, но точно знаю что результаты должны быть пригодны или для последующей машинной обработки, или же представлены в удобочитаемом виде. То есть в итоге у нас будет минимум 3 выходных формата: csv и json – для последующей машинной обработки, а также xlsx – если мы захотим посмотреть результаты глазками.
Итого у нас получилось два не пересекающихся набора параметров.
И куда же тут, спрашивается, пристроить список компутеров, если мы его получаем в процессе работы, а не передаем из файла? Правильный ответ – нечего ему в конфиге делать. У нас есть набор параметров, позволяющий его получить. Этого более чем достаточно. Но тут вот какая штука. При изучении документации класса
SmbConnection выяснилось, что авторизоваться мы можем двумя способами:
1. используя логин и пароль – вот вам и ответ на часть вопроса;
2. через Kerberos – этот случай я рассматривать здесь не буду, но в проекте он реализован;
Для авторизации через кербу придется тянуть еще параметры. Итак, конфиг для модуля сбора шар у нас будет следующий:
Python: Скопировать в буфер обмена
Код:
class ShareEnumConfig(AppConfig):
def __init__(self, is_ignore_hidden_shares_enabled: bool
, is_check_read_access_enabled: bool
, is_check_write_access_enabled: bool
, thread_count: int
, force_no_dns: bool
, timeout: int
, outfile_type: str
, unique_path_length: int
, comment_length: int
, is_need_use_kerberos: bool
, aes_key: str
, no_pass: bool
, auth_hashes: str
, dc_ip: str
, username: str
, password: str
, domain_name: str):
super().__init__()
self.domain_name = domain_name
self.is_ignore_hidden_shares_enabled = is_ignore_hidden_shares_enabled
self.is_check_read_access_enabled = is_check_read_access_enabled
self.is_check_write_access_enabled = is_check_write_access_enabled
self.thread_count = thread_count
self.force_no_dns = force_no_dns
self.timeout = timeout
self.outfile_type = outfile_type
self.outfile = ''
self.unique_path_length = unique_path_length
self.comment_length = comment_length
self.is_need_use_kerberos = is_need_use_kerberos
self.no_pass = no_pass
self.auth_hashes = auth_hashes
self.aes_key = aes_key
self.dc_ip = dc_ip
self.username = username
self.password = password
1.2.1 ShareFinder. Шары есть? А если найду?
Как уже говорилось ранее, собирать шары мы будем используя протокол SMB. Опять задаемся вопросом: «Что необходимо для решения задачи?» и пробуем на него ответить. Очевидно, что нам
понадобится:
1. установить подключение к серверу;
2. авторизоваться;
3. выполнить запрос;
4. сохранить результаты для последующей обработки;
5. отключиться.
Еще надо бы не забыть про многопоточность и таймауты.
Поехали.
Объявим новый класс и обзовем его ShareFinder. Ему в конструктор мы передадим наш конфиг. Там же, в конструкторе, объявим поле domain_computers в виде списка. Его мы заполним снаружи, когда отработает коллектор.
Еще нам понадобится некая структура данных, для сохранения результатов. Очевидно, что это у нас будет словарь, ключом которого будет имя компутера, а значением – список принадлежащих ему шар.
Получится примерно следующее:
Python: Скопировать в буфер обмена
Код:
class ShareFinder:
def __init__(self, config: ShareEnumConfig):
self.config = config
self.result = defaultdict(list)
self.domain_computers = list()
Теперь про многопоточность. Воспользуемся стандартным пулом потоков. Вот что говорит его документация.
Python: Скопировать в буфер обмена
Код:
concurrent. futures. thread. ThreadPoolExecutor
def __init__(self,
max_workers: int | None = None,
thread_name_prefix: str = "",
initializer: (...) -> object | None = None,
initargs: tuple[Any, ...] = ()) -> None
Initializes a new ThreadPoolExecutor instance. Args: max_workers: The maximum number of threads that can be used to execute the given calls. thread_name_prefix: An optional name prefix to give our threads. initializer: A callable used to initialize worker threads. initargs: A tuple of arguments to pass to the initializer.
То есть именно сюда мы передадим количество потоков. А что собственно мы будем делать в потоках. Правильно – опрашивать каждый компутер на предмет шар и записывать результаты в общую коллекцию результатов. Не забыв, конечно, при этом про то, что мы работаем с общим ресурсом, а значит каждый поток должен уметь блокировать ресурс на запись.
Для реализации этого функционала нам понадобится метод, назовем его worker. который мы скормим нашему пулу.
В итоге получилось примерно следующее.
Python: Скопировать в буфер обмена
Код:
def run(self):
auth_lm_hash = ""
auth_nt_hash = ""
if self.config.auth_hashes is not None:
if ":" in self.config.auth_hashes:
auth_lm_hash = self.config.auth_hashes.split(":")[0]
auth_nt_hash = self.config.auth_hashes.split(":")[1]
else:
auth_nt_hash = self.config.auth_hashes
results = {}
# Setup thread lock to properly write in the file
lock = threading.Lock()
with ThreadPoolExecutor(max_workers=min(self.config.thread_count, len(self.domain_computers))) as tp:
for computer in self.domain_computers:
tp.submit(self.worker, computer, self.config.domain_name, self.config.username,
self.config.password, computer, auth_lm_hash, auth_nt_hash, self.result, lock)
time.sleep(self.config.timeout)
Как видите, таймауту тоже нашлось применение.
Теперь рассмотрим сам метод worker. Он достаточно простой, поэтому просто приведу его код.
Python: Скопировать в буфер обмена
Код:
def worker(self, target_name, domain, username, password, address, lmhash, nthash, results, lock):
if self.config.force_no_dns:
target = target_name
else:
target_ip = nslookup.Nslookup(dns_servers=[self.config.dc_ip], verbose=True).dns_lookup(target_name).answer
if len(target_ip) != 0:
target = target_ip[0]
else:
return
try:
smb_client = self.init_smb_session(target, domain, username, password, address, lmhash, nthash)
resp = smb_client.listShares()
for share in resp:
# SHARE_INFO_1 structure (lmshare.h)
# https://docs.microsoft.com/en-us/windows/win32/api/lmshare/ns-lmshare-share_info_1
share_name = share['shi1_netname'][:-1]
share_comment = share['shi1_remark'][:-1]
share_type = share['shi1_type']
share_path = "\\".join(['', '', target, share_name])
unique_path = "\\".join(['', '', target, share_name, ''])
permission = []
self.__check_share_read_access(permission, share_path)
self.__check_share_write_access(permission, share_path)
try:
lock.acquire()
if target_name not in results.keys():
results[target_name] = []
results[target_name].append(
{
"share": share_name,
"computer": target_name,
"hidden": (True if share_name.endswith('$') else False),
"uncpath": unique_path,
"comment": share_comment,
"permission": ', '.join(permission),
"type": {
"stype_value": share_type,
"stype_flags": self.STYPE_MASK(share_type)
}
}
)
self.__print_share_info(address, share_comment, share_name)
finally:
lock.release()
self.result = results
except Exception as e:
print(e)
1.3 Конфигурация строителя и сборка продукта.
1.3.1 ShareEnumModeConfigurator.
Нам осталось указать строителю параметры сохранения результата. Здесь будет минимум кода.
Python: Скопировать в буфер обмена
Код:
class ShareEnumModeConfigurator(AppConfigurator):
def __init__(self, share_enum_config: ShareEnumConfig, domain_name: str, out_dir: str = 'evil-corp'):
super().__init__(domain_name, 'shares', out_dir)
self.config = share_enum_config
def setup(self):
self.create_out_dirs()
out_type = self.config.outfile_type
if out_type == 'txt':
outfile = 'shares.txt'
elif out_type == 'json':
outfile = 'shares.json'
elif out_type == 'yaml':
outfile = 'shares.yaml'
elif out_type == 'csv':
outfile = 'shares.csv'
elif out_type == 'xlsx':
outfile = 'shares.xlsx'
else:
DumpLogger.print_warning(
f'An unsupported output file format is specified. The default value ("*.csv") will be used')
outfile = 'shares.csv'
self.config.out_filename = os.path.join(self._current_mode_out_dir, outfile)
def create_out_dirs(self):
super().create_out_dirs()
1.3.2 ShareEnumBuilder.
Мы строили, строили и наконец построили.
Здесь тоже ничего сложного. Мы сначала попросим LdapDataCollector собрать список компутеров в сети. Затем обратимся к ShareFinder’у с просьбой собрать шары. А результат запишем в продукт, который сам уже знает как у куда его сохранить. На всё нам понадобится всего 20 строк. Магия, однако.
Python: Скопировать в буфер обмена
Код:
class ShareEnumBuilder(AbstractBuilder):
def __init__(self, app_config: ShareEnumConfig, data_collector: LdapDataCollector):
super().__init__(data_collector)
self.app_config = app_config
def build_product(self) -> AbstractProduct | None:
DumpLogger.print_title('SHARE ENUMERATOR: build product')
try:
self.data_collector.get_domain_computers_full_info()
domain_computers = self.data_collector.domain_computers
share_finder = ShareFinder(self.app_config)
share_finder.domain_computers = domain_computers
share_finder.run()
share_enumerator = ShareEnumerator()
share_enumerator.results = share_finder.result
self.is_build_completed = True
return share_enumerator
except Exception as err:
self.error_message = f'share enumeration failed with error {err}'
self.setup_incomplete_product(err, self.error_message)
return None
В завершение рассмотрим метод save продукта ShareEnumerator. Ему на вход прилетает результат в следующем формате:
JSON: Скопировать в буфер обмена
Код:
results = [
{'share': 'ADMIN$'
, 'computer': 'DELL5490-AIO.HQ.EVIL.CORP.COM'
, 'hidden': True
, 'uncpath': '\\\\DELL5490-AIO.HQ.EVIL.CORP.COM\\ADMIN$\\'
, 'comment': 'Remote Admin'
, 'permission': ''
, 'type': {'stype_value': 2147483648, 'stype_flags': ['STYPE_DISKTREE', 'STYPE_TEMPORARY']}},
{'share': 'C$', 'computer': 'DELL5490-AIO.HQ.EVIL.CORP.COM', 'hidden': True,
'uncpath': '\\\\DELL5490-AIO.HQ.EVIL.CORP.COM\\C$\\', 'comment': 'Default share', 'permission': 'r,w',
'type': {'stype_value': 2147483648, 'stype_flags': ['STYPE_DISKTREE', 'STYPE_TEMPORARY']}},
{'share': 'ADMIN$', 'computer': 'DELLACHAT-UFF.HQ.EVIL.CORP.COM', 'hidden': True,
'uncpath': '\\\\DELLACHAT-UFF.HQ.EVIL.CORP.COM\\ADMIN$\\', 'comment': 'Remote Admin', 'permission': 'r',
'type': {'stype_value': 2147483648, 'stype_flags': ['STYPE_DISKTREE', 'STYPE_TEMPORARY']}},
{'share': 'C$', 'computer': 'DELLACHAT-UFF.HQ.EVIL.CORP.COM', 'hidden': True,
'uncpath': '\\\\DELLACHAT-UFF.HQ.EVIL.CORP.COM\\C$\\', 'comment': 'Default share', 'permission': 'r',
'type': {'stype_value': 2147483648, 'stype_flags': ['STYPE_DISKTREE', 'STYPE_TEMPORARY']}},
{'share': 'ADMIN$', 'computer': 'DELLFRANK-AIO.HQ.EVIL.CORP.COM', 'hidden': True,
'uncpath': '\\\\DELLFRANK-AIO.HQ.EVIL.CORP.COM\\ADMIN$\\', 'comment': 'Remote Admin', 'permission': 'r',
'type': {'stype_value': 2147483648, 'stype_flags': ['STYPE_DISKTREE', 'STYPE_TEMPORARY']}},
{'share': 'IPC$', 'computer': 'DELLFRANK-AIO.HQ.EVIL.CORP.COM', 'hidden': True,
'uncpath': '\\\\DELLFRANK-AIO.HQ.EVIL.CORP.COM\\IPC$\\', 'comment': 'Remote IPC', 'permission': 'r',
'type': {'stype_value': 2147483651, 'stype_flags': ['STYPE_IPC', 'STYPE_TEMPORARY']}},
]
Здесь мы тоже уложились в 15 строк. Опять магия? Нет. Всего лишь грамотно спроектированная архитектура.
Python: Скопировать в буфер обмена
Код:
def save(self, app_config: ShareEnumConfig):
out_type = app_config.outfile_type
filename = app_config.out_filename
if out_type == 'txt':
FileHelper.save_results_to_txt(self.results, filename)
elif out_type == 'json':
FileHelper.save_results_to_json(self.results, filename)
elif out_type == 'yaml':
FileHelper.save_results_to_yaml(self.results, filename)
elif out_type == 'csv':
FileHelper.save_share_enumeration_results_to_csv(self.results, filename)
elif out_type == 'xlsx':
FileHelper.save_results_to_xlsx(self.results, filename)
else:
DumpLogger.print_warning('An unsupported output file format is specified. The default value will be used')
1.3. Выводы.
Модуль перечисления общих сетевых ресурсов внешне не похож на дампы, описанные в предыдущей статье. Тем не менее нам удалось без особых проблем встроить его в имеющуюся архитектуру.
Это означает, что подход был выбран верный. Архитектура может и не идеальная, но достаточно устойчивая. Нам удалось разделить реализацию и бизнес-правила. На этом мы не заканчиваем.
После рассмотрения реализации вспомогательных утилит мы попробуем расширить продукт и использовать его для массового сканирования сетей.
2 ArgumenParser. Метод определенного интеграла в действии.
Метод определенного интеграла – любую сложную задачу необходимо разбить на конечное множество простых подзадач, каждая из которых имеет константное время выполнения. Еще его называют «разделяй и властвуй» или декомпозиция. Попробуем и здесь его применить.
Как я уже говорил в первой части, реализация разбора аргументов командной строки во многих опенсорсных проектах оставляет желать лучшего. Пример я выше приводил. Такой хоккей нам не нужен. Поэтому будем делать всё по науке.
Поехали.
2.1 Наброски архитектуры.
Для начала вспомним что у нас есть несколько режимов работы, каждый из которых просит свой набор параметров. Для дампов эти параметры будут одни и те же, за исключением типа самого дампа. Для сбора шар у нас добавятся дополнительные параметры, о чем я писал выше. Всю эту кашу необходимо каким-то образом расхлебать, не нарушив основной архитектуры. Но об этом чуть позже. А пока зададим себе следующий вопрос. С чего начинается любая программа? Правильно. Со справки или мануала. Очень важно чтобы обычный пользователь, впервые использующий наш
продукт и набравший в консоли
Bash: Скопировать в буфер обмена
program.exe --help
Получил максимально четкое и подробное описание всех режимов работы приложения, а не просто красивую ASCII картинку, которую так любят все опенсорсники. Поэтому наш модуль должен уметь делать красивую справку.
Что еще? По хорошему это должен быть инструмент, работающий по принципу: «Один раз настроил и забыл». Следовательно он должен иметь некий инструмент настройки. Хмм... А у нас ведь уже есть конфигуратор. А что если ... Мысль вроде дельная, но об этом чуть позже.
Еще нам нужно чтобы под капотом можно было задавать произвольные наборы параметров и группировать их по некоему общему признаку. То есть у нас будет, как минимум, группы параметров для дампов и шар. Но что нам взбредет в голову завтра – большой секрет. Значит мы должны и здесь заложить возможность расширения.
Идем дальше. Хорошо, параметры мы распарсили. Что с ними делать? Вернуть. А что возвращать? Безразмерный кортеж? Стоп. У нас же для каждого режима есть конфиги. Почему бы не возвращать их. В этом уже просматривается некий смысл. То есть мы разобрали параметры, сформировали конфиг и тут же проверили его валидность. Если нам сунули фуфло, то мы об этом сразу и заявляем, а не передаем это дальше. Отсюда просится еще и корректная обработка ошибок. Ладно, достаточно слов. Начнем пожалуй.
2.2 Разбор аргументов командной строки.
У питона, при всей моей не любви к нему, есть одно преимущество. Огромное количество пакетов, причем многие из них очень качественные. То есть их можно взять и использовать из коробки. Что мы и сделаем. В основу нашего парсера аргументов положим стандартный питоновский парсер. Кому интересно – читайте документацию. Я же здесь расскажу о том, как я в очередной раз впихнул внешне не впихуемое. А именно – встроил парсер в архитектуру, таким образом, что прогнав его через конфигуратор мы получили готовый к использованию продукт.
2.2.1 Настраиваем стандартный питоновский парсер под наши нужды
Очевидно, что у нас будет две группы параметров – обязательные: всё, что относится к LDAP и опциональные – дополнительные параметры, например, того же ShareEnumerator’a. Итак, объявим класс ArgsParserConfigurator, производный от AppConfigurator. У него будет ровно одно собственное поле. Это объект класса argsparse.ArgumentParser. Его мы и будем настраивать.
Конструктор у нас будет такой:
Python: Скопировать в буфер обмена
Код:
class ArgsParserConfigurator(AppConfigurator):
def __init__(self, domain_name: str = 'evil-corp.com', out_dir: str = 'evil-corp'):
super().__init__(domain_name, 'args parser', out_dir)
self.parser = argparse.ArgumentParser(
description='A pentest tool for obtaining a variety of information about the network AD with LDAP',
usage='%(prog)s '
f' Collects the following information'
'about users and computers on the network:\n'
f'{DumpLogger.highlight_green("MINIDUMP")} mode:\n'
f'\t--{DumpLogger.highlight_green("Authentication mechanism")};\n'
f'\t--{DumpLogger.highlight_green("List of domain users")};\n'
f'\t--{DumpLogger.highlight_green("List of domain admins")};\n'
f'\t--{DumpLogger.highlight_green("List of enterprise admins")};\n'
f'\t--{DumpLogger.highlight_green("List of domain controllers")};\n'
f'\t--{DumpLogger.highlight_green("List of domain trusts")};\n'
f'\t--{DumpLogger.highlight_green("List of servers")}, including the host name,'
' name and version of the operating system for each server;\n'
f'\t--{DumpLogger.highlight_green("List of users PC")}, including the host name,'
' name and version of the operating system for each computer;\n'
f'\t--{DumpLogger.highlight_green("OS statistic:")} '
f'a list of computers for each operating system version;\n'
'********************************************************************************************\n'
f'{DumpLogger.highlight_green("FULLDUMP")} mode:'
f' repeating a functional of MINIDUMP mode with additional information:\n'
f'\t--{DumpLogger.highlight_green("List of domain groups")}\n'
f'\t--{DumpLogger.highlight_green("List of domain organization")} units\n'
f'\t--{DumpLogger.highlight_green("List of domain subnets")}\n'
'********************************************************************************************\n'
f'{DumpLogger.highlight_green("SHARES")}:'
f' collect information about network shared resources'
'and save the results in the specified format.\n'
f'See {DumpLogger.highlight_green("SHARES")} mode help for more information\n'
'********************************************************************************************\n'
f'{DumpLogger.highlight_green("FASTDUMP")}: collects count of users, '
f'count of computers and list of groups of which the user is a member '
)
self.parser.version = 'ADDumper version: 0.3.0-beta'
Теперь переопределим метод базового класса setup и посмотрим на часть его реализации. Она предельно проста.
Python: Скопировать в буфер обмена
Код:
def setup(self):
self.__setup_required_params()
self.__setup_optionals_params()
Python: Скопировать в буфер обмена
Код:
def __setup_required_params(self):
required_params = self.parser.add_argument_group(f'{DumpLogger.highlight_blue("required arguments")}')
required_params.add_argument( '-m', '--mode', dest='mode', type=str, required=True,
help=f'the current version supports the following operating modes: '
f'{DumpLogger.highlight_green("MINIDUMP, FULLDUMP, KERB, SHARES, FASTDUMP")}.'
)
ldap_params = self.parser.add_argument_group(DumpLogger.highlight_blue('LDAP params'))
ldap_params.description = f'{DumpLogger.highlight_warning("LDAP connection params. Required.")}'
ldap_params.add_argument( '-d', '--domain', dest='domain', type=str
, help='The name of domain (e.g. "test.local"). '
f'{DumpLogger.highlight_dark_blue("If the value is not set, it will be determined automatically")}',
required=True
)
ldap_params.add_argument( '-u', '--username', metavar='username', dest='username', type=str
, help=f'The user name, required parameter for the LDAP connection', required=True)
ldap_params.add_argument('-p', '--password', metavar='password', dest='password', type=str
, help='The user password required parameter '
, required=True
)
ldap_params.add_argument( '-ip', metavar='[ip-address]', dest='ip_address', type=str, required=True,
help='The IP address of the server (e.g. "192.168.13.169"). '
f'{DumpLogger.highlight_dark_blue("If the value is not set, it will be determined automatically")}',
default=None
)
Что мы собственно говоря сделали. Вынесли обязательные аргументы в отдельную группу. Для каждого параметра указали тип, описание, имя флага, добавили справку. Аналогичную работу нужно провести для опциональных параметров. По сути в качестве готового продукта мы взяли стандартный парсер и настроили его в конфигураторе. Конфигуратор мы тоже будем создавать с помощью фабрики. Для этого добавим в нее еще один метод.
Python: Скопировать в буфер обмена
Код:
class ConfiguratorsFactory:
@staticmethod
def create_args_parser_configurator(out_dir: str = 'evil-corp') -> ArgsParserConfigurator:
return ArgsParserConfigurator(out_dir=out_dir)
Возможно не самое удачное архитектурное решение, но давайте посмотрим как оно впишется в
общую картину.
2.2.2 Добываем конфиги
После настройки парсера мы уже почти готовы к работе. Осталось только добавить немного магии и получить из аргументов конфиги. Чем мы собственно и займемся. Чтобы вся порнография настройки парсера не торчала наружу мы ее прикроем чутка. И поместим уже настроенный парсер в класс который так и назовем ConfigMiner. А на вход его конструктора передадим наш уже настроенный парсер.
Python: Скопировать в буфер обмена
Код:
class ConfigMiner:
def __init__(self, parser: argparse.ArgumentParser):
self.base_dn = ''
self.args = None
self.domain_name: str = ''
self.out_dir: str = ''
self.program_mode = ProgramMode.UNKNOWN
self.parser = parser
self.ldap_config = None
self.kerb_config = None
self.fulldump_config = None
self.minidump_config = None
self.share_enumerator_config = None
self.fastdump_config = None
Этот класс должен уметь делать ровно две вещи:
1. дать команду парсеру на разбор;
2. собрать из полученных параметров нужный конфиг и быть готовым вернуть его по первому
запросу.
Собственно всё. Ниже я привел его код и пример его использования. Думаю на этом достаточно про парсер. Конструктор:
Python: Скопировать в буфер обмена
Код:
class ConfigMiner:
def __init__(self, parser: argparse.ArgumentParser):
self.base_dn = ''
self.args = None
self.domain_name: str = ''
self.out_dir: str = ''
self.program_mode = ProgramMode.UNKNOWN
self.parser = parser
self.ldap_config = None
self.kerb_config = None
self.fulldump_config = None
self.minidump_config = None
self.share_enumerator_config = None
Метод старт, который запускает последовательно обе перечисленные задачи.
Python: Скопировать в буфер обмена
Код:
def start(self) -> bool:
try:
args = self.parser.parse_args()
print(args)
except Exception as err:
DumpLogger.print_error_message(err.args)
return False
mode = args.mode
self.out_dir = args.out_dir
if not self.is_valid_program_mode(mode):
DumpLogger.print_error_message('ERROR: unknown program mode')
return False
domain_name = args.domain
self.base_dn = NetworkUtils.get_base_dn(domain_name)
if self.base_dn is None:
DumpLogger.print_error_message(f'invalid domain name:\t {domain_name}')
return False
self.domain_name = domain_name
try:
if not self.is_ldap_config_created(args, self.base_dn):
DumpLogger.print_error_message('can\'t create LDAP config')
return False
if self.program_mode == ProgramMode.MINI_DUMP:
self.make_minidump_config()
if self.program_mode == ProgramMode.FULL_DUMP:
self.make_fulldump_config()
if self.program_mode == ProgramMode.KERB:
self.make_kerb_config(args)
if self.program_mode == ProgramMode.SHARES:
self.make_share_finder_config(args)
if self.program_mode == ProgramMode.FAST_DUMP:
self.make_fastdump_config(args)
except Exception as error:
print(error.args)
return False
return True
И, наконец, пример использования всего этого добра.
Python: Скопировать в буфер обмена
Код:
def run(self) -> bool:
args_parser_configurator = ConfiguratorsFactory.create_args_parser_configurator()
args_parser_configurator.setup()
parser = args_parser_configurator.parser
miner = ConfigMiner(parser)
try:
if not miner.start():
DumpLogger.print_error_message('invalid arguments, use \'ADDumper --help\' for more information')
return False
except Exception as err:
DumpLogger.print_error('Can\'t create Argument parser', str(err))
return False
self.program_mode = miner.program_mode
self.out_dir = miner.out_dir
ldap_config: LdapConfig = miner.ldap_config
self.ldap_connection = source.core.ldap.ldap_connection.LdapConnection(ldap_config)
ldap_config.print()
if not self.ldap_connection.is_ldap_connection_established():
DumpLogger.print_error_message('failed to establish LDAP connection')
ldap_config.print()
return False
query_executor_config = ConfigFactory.create_ldap_query_executor_config(self.ldap_connection,
miner.base_dn)
query_executor = LdapQueryExecutor(query_executor_config)
data_collector_config = ConfigFactory.create_data_collector_config(query_executor)
data_collector = LdapDataCollector(data_collector_config)
self.domain_name = miner.domain_name
if program_mode == ProgramMode.KERB:
return self.run_kerberos_mode(miner.kerb_config, data_collector)
elif program_mode == ProgramMode.SHARES:
return self.run_share_enumeration(miner.share_enumerator_config, data_collector)
elif program_mode == ProgramMode.FULL_DUMP:
return self.run_fulldump_mode(miner.fulldump_config, data_collector)
elif program_mode == ProgramMode.MINI_DUMP:
return self.run_minidump_mode(miner.minidump_config, data_collector)
elif program_mode == ProgramMode.FAST_DUMP:
return self.run_fastdump_mode(miner.fastdump_config, data_collector)
else:
DumpLogger.print_error('invalid program mode', str(program_mode))
return False
2.3 Выводы.
Не смотря на то, что работа с аргументами командной строки, как вы могли видеть, – то еще удовольствие, нам удалось относительно безболезненно встроить ее (обработку аргументов командной строки) не нарушая целостности архитектуры приложения. Возможно решение и не самое удачное, но оно рабочее и большую часть времени реализации заняло прописывание справок для каждого параметра, нежели непосредственно написание кода.
3 Одна сеть хорошо, а много лучше.
Ну вот и получился уже вполне вменяемый продукт, которым можно пользоваться и людям не стыдно показать. Впска с любым линуксом на борту,proxychains до сети и побежали.
Для тестов – это однозначно плюс. А если приложить чуть усилий, то можно настроить удаленную отладку и вообще красиво получится.
Однако, что делать, если сетей много и большинство из них – это фортики и циски. Конечно, можно подключаться руками через официальные клиенты. Оно таки да, можно. Если у вас 3-5 сетей в неделю проходит. И вам достоверно известно, они 100% валидные: учетка живая и юзер в домене хотя бы. В противном случае, когда сети идут из логов – у меня рука отвалилась мышкой тыкать через два часа.
Что делать?
Давайте рассуждать вслух. Что мы имеем? У нас есть продукт, который абсолютно не зависит от метода подключения. То есть мне не важно как осуществлено подключение. Это хорошо. Теперь посмотрим какие методы подключения возможны. Рассмотрим два основных:
1. VPN
2. Proxy
Начнем со второго. Здесь все должно быть относительно просто. Если у нас есть список доступов через прокси, то мы можем написать внешний скрипт, который будет генерировать конфиг для proxychains и подключаться через него. Должно получиться нечто подобное:
Код: Скопировать в буфер обмена
Код:
Открываем файл, со списком прокси.
Читаем этот список.
for line in proxyList:
сгенерировать конфиг для proxychains
запустить скрипт через прокси
Поскольку этот способ менее актуальный, я не реализовал его. Гораздо больше нас интересует воз-
можность подключения через VPN. Посмотрим что тут можно сделать.
3.1 OpenConnect. Что это такое и с чем его едяcт.
Дабы не разводить тут дальнейшую лабуду про выбор инструментов. Остановимся на OpenConnect. Отличный продукт, поддерживающий несколько протоколов, в частности:
• Cisco’s AnyConnect SSL VPN;
• Fortinet Fortigate SSL VPN;
• Palo Alto Networks GlobalProtect SSL VPN;
Есть и другие, но я их не тестировал, слишком редкие звери. Поправьте если я не прав, но в живой природе я их не встречал. Отлично – это то, что нужно. Открываем документацию и видим, что для подключения нам необходимо указать несколько параметров, таких как:
• username;
• password;
• address;
• vpn group – необязательный параметр, указывающий принадлежность пользователя к какой-
либо группе;
• protocol – протокол, по которому будет выполняться подключение. По умолчанию используется Cisco anyconnect. Остальные необходимо указывать явно. Например, –protocol=fortinet
Отлично. Берем первый попавшийся доступ, устанавливаем openconnect на впску и пробуем подключиться. У нас получилась примерно такая строка для подключения:
Bash: Скопировать в буфер обмена
openconnect -b --protocol=anyconnect --user=test 196.204.212.170:443
Запускаем. Попросил пароль. Вводим. Делаем заметку о том, что надо автоматизировать ввод пароля. Еще просит проверить отпечаток. Говорим, что ок, тоже ставим пометку про автоматизацию. Подключились. Пробуем запуститься. Ура. Заработало. Отлично. Теперь дело за малым – автоматизировать всё.
3.2 Автоматизация подключения.
Не вдаваясь особо в детали расскажу про основные проблемы возникшие при автоматизации подключения.
Поехали.
Запускаться мы будем через subprocess.
3.2.1 Ввод пароля и отпечатка
Здесь получилось обойтись относительно малой кровью, за что отдельная благодарочка разработчикам продукт. Для ввода пароля воспользуемся встроенным флагом –passwd-on-stdin.
С отпечатком чуть сложнее. Но не более. Мы просто вызовем подключение дважды. Первый раз вычленим из ответа сам отпечаток и запомним его, а во время второго запуска передадим его как
параметр. В итоге получится следующая строка:
openconnect -b --passwd-on-stdin --servercert={fingerprint} --protocol=anyconnect --user=test 196.204.212.170:443
Флаг -b (background) запускает openconnect в фоне и позволяет выполнять произвольные операции при подключении.
3.2.2 Отключение.
Отлично. Мы подключились, даже отработали. А вот как теперь отключиться? Да вали его нахрен, да и всё – это пришедшее в голову первое решение. Так и сделаем. Запускаем htop. Ищем процесс и убиваем его. Ура. Вроде всё ок. Но это ручками. А вот как автоматизировать. А тут очередная похвала разработчикам продукта. Они решили эту проблему за меня – если указать еще и флаг –pid-file, то он любезно запишет туда свой PID. Нам останется лишь прочитать его и грохнуть.
А строка подключения у нас теперь такая:
openconnect -b --passwd-on-stdin --servercert={fingerprint} --pid-file=/tmp/openconnect.pid --protocol=anyconnect --user=test 196.204.212.170:443
3.2.3 Большой облом. Или как я построил большую индейскую национальную избу из окирпиченных VPS.
Вау. Всё круто, теперь делов-то. Написать пару классов, заложившись, как обычно, под возможность расширения и будет нам счастье. «Ага, щаззз», подумали про себя разработчики фортика и злобно так засмеялись.
Я набросал скрипт, который подключается через openconnect. Выполняет пару рутинных операций и отключается. В файл накидал доступов для тестирования разных протоколов. Стартую. Первая циска пошла. Всё ок. Вторая, третья. Всё работает. Пошел покурить, думаю: "приду, воткну всё это в проект и вопрос закрыт". «А вот фигвам», сказали разработчики фортика.
По возвращении обнаружил, что впска не подает признаков жизни. В логах видно, что несколько цисок я обработал корректно, а на фортике чет сломалось. Ну я решил, что это глюк какой. Пробую подключиться с другого терминала. Тут будет картинка про фигвам из простоквашино. Да что за хрень? Меняю свои впны. Не работает. Нет подключения, хоть ты тресни. Ладно, хрен с тобой, золотая рыбка. Делаем реинсталл. Запускаем повторно тот же файл, но добавляем больше логов. Те же яйца, только в профиль. После подключения к фортику перестает подавать признаки жизни. Раз 15 я бился башкой об эти кирпичи. Безрезультатно. Это уже начинает напрягать. Вооружаемся гуглом, обкладываемся манами по сетям, впнам, openconnect’у.
В общем через некоторое время удалось выяснить, что многие впны меняют настройки сети, делая ее недоступной из внешнего мира. Поэтому и кирпич вместо впски при удачном подключении. Что делать? Опущу свои приключения на пути поиска решения. Это было очень интересно, но описание займет много времени.
А вот решение оказалось достаточно простым. Если подключаться напрямую из впски, то окирпичивание ее гарантировано. А вот если взять контейнер и всю работу с подключением поместить в него, то всё будет окей. То есть я взял LXD-container. закинул свой софт в него, а контейнер на впску. Всё. Теперь если впн и убьет мне подключение, то доступ к впс. а следовательно и к контейнеру у меня останется. Теперь пришла моя очередь радоваться жизни.
Я использовал этот контейнер.
3.2.4 Выводы.
С помощью гугла и какой-то матери нам удалось более-менее корректно реализовать автоматизацию подключения и отключения. Лично я узнал очень много нового о принципах работы сетей и впнов, а также особенностях
реализации тех или иных моментов. Удалось совместить ранее изученную теорию и практику.
3.3 Проектирование архитектуры и реализация
После всех танцев с бубнами нам удалось получить минимально рабочую версию модуля внешнего подключения. Теперь ее необходимо довести до ума и встроить в проект, ничего при этом не сломав.
Поехали.
3.3.1 Connector.
На данном этапе мы умеем работать с openconnect. Это хорошо. А что если нам понадобится использовать протокол, не поддерживаемый им? А там еще что-то говорилось про прокси. Сейчас это не нужно, но потом может и понадобится. При этом я опять понятия не имею как это будет выглядеть. Опять сюда просится фабрика коннекторов. Давайте прикинем базовый функционал коннектора:
• Подключение – тут всё очевидно. Нам понадобится, как минимум, 3 параметра, которые будут
обязательно присутствовать для любого типа соединения. Это адрес, имя пользователя и пароль.
• Отключение – эта штука должна уметь отключаться и корректно восстанавливать параметры
сети, если впн их сломал.
• Обработка ошибок – если что-то пошло не так, мы должны получить об этом максимально подробную информацию.
Вот у нас и готов интерфейс базового класса.
Python: Скопировать в буфер обмена
Код:
class Connector:
def __init__(self, connection_type: ConnectionType):
self.CWD = ''
self.CSD_WRAPPER_PATH = ''
self.OPENCONNECT_PATH = 'openconnect'
self.connection_type = connection_type
self.password = ''
self.username = ''
self.address = ''
self.last_error = ''
@abstractmethod
def setup(self, username: str, password: str, address: str):
self.username = username
self.password = password
self.address = address
@abstractmethod
def is_connection_established(self) -> bool:
pass
@abstractmethod
def is_connection_broken(self) -> bool:
pass
А вот и его реализация в классе OpenConnector.
Python: Скопировать в буфер обмена
Код:
class Openconnector(Connector):
def __init__(self):
super().__init__(ConnectionType.OPENCONNECT)
self.vpn_group = ''
self.vpn_type = ''
self.gateway_address = ''
self.gateway_name = ''
def setup(self, username: str, password: str, address: str, vpn_group: str = '', vpn_type: str = 'anyconnect'):
super().setup(username, password, address)
self.vpn_group = vpn_group
self.vpn_type = vpn_type
def is_connection_established(self) -> bool:
success_connection_message = f'The connection to the {self.address} has been successfully established.'
try:
args = self.__setup_openconnect_args()
DumpLogger.print_param('openconnect args', str(args))
subprocess.run(args, stdout=subprocess.PIPE, input=f'{self.password}\n'.encode('utf-8'),
stderr=subprocess.PIPE, timeout=4)
return True
except Exception as err:
error_message = str(err)
if 'timed out after' in error_message:
DumpLogger.print_success(success_connection_message)
return True
DumpLogger.print_error('openconnector is_connection_established', str(err))
return False
def is_connection_broken(self) -> bool:
try:
DumpLogger.print_title('Openconnector is_connection_broken')
os.kill(self.__get_pid(), signal.SIGKILL)
return True
except Exception as err:
DumpLogger.print_error('Openconnector is_connection_broken', str(err))
return False
Как видим, у нас опять получился достаточно читаемый и компактный код, не содержащий ничего лишнего. добавим его в проект и продолжим радоваться жизни.
3.3.2 Интеграция в проект
У нас добавился новый модуль. А следовательно добавились и новые аргументы командной строки. Это единственный модуль в коде которого нам придется сделать изменения. И у нас на это есть действительно веская причина. Мы же помним про SOLID.
Если совсем коротко, то я добавил в аргументы тип подключения и метод получения аргументов - из командной строки или из файла. В файле мы храним наши впны. В идеале, конечно, сделать конфиг файл, а к нему конфигуратор с гуем, но это совсем другая история. В зависимости от выбранного режима подключения мы либо начинаем работать сразу, предполагая, что подключение уже установлено и если что-то с ним не так, то это уже не наша проблема, либо
же сначала подключаемся указанным способом и, в случае успешного подключения, начинаем работать. При возникновении ошибки тщательно пишем ее в логи и берем следующий набор параметров для подключения.
Вот и вся магия.
3.4 Выводы.
Одно видео вместо тысячи слов.
Если посоветуете другой файл-сервер - перезалью.
За сим позвольте откланяться.