воскресенье, 20 января 2019 г.

Скрипт для перебора паролей IOS

Простоватая топология...
Всем привет.

Давненько я не писал, даже не знаю с чего начать. Короче, трудовая жизнь сделала "вжух" и как-то все немного поменялось... Обещаю рассказать подробнее, как все встанет на свои места. В любом случае, я лихо взял курс на автоматизацию, скриптинг, QA в сетях и прочее такое... Глядишь и до CI/CD доберусь. Но сегодня не столько про это, просто опишу решение конкретной задачи.

Ситуация

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


Задача

Нужно сделать так, чтобы не нужно было подбирать пароли. ) Как таковых решений тут может быть несколько, но самый разумный из них (ИМХО) "побегать" по устройством скриптом и либо настроить TACACS/RADIUS, либо просто прописать одну учетку на все устройства.

Для этого нужно:
  • Само функциональное ядро скрипта, которое может:
    • Проверять список учеток на девайсе 
    • Производить изменения, если удалось залогиниться
  • Возможность легко изменять
    • Список учеток
    • Список устройств для опроса
    • Список команд для отправки
  • Базовые аналитические функции
    • Какой статус на каждом устройстве после выполнения скрипта
    • Вывод из консоли в случае изменения
    • Суммарная информация (сколько удачно, сколько нет)
  • Возможность выводить информация в отдельный файл для последующий обработки
  • Хочется, чтобы скрипт работал относительно быстро, параллельно опрашивая сразу несколько устройств.

Действия

Ну, тут все просто, бери и пиши. 

В общих чертах

Для начала в общих чертах определимся как будет действовать скриптина. Я представлял себе это как некий консольный инструмент, в который скармливаешь список учеток из файла, список устройств и список команд для запуска. Далее происходит магия, после которой можно посмотреть что именно было изменено и вообще проанализировать некую статистику. Какие устройства доступны, какие нет, где подобрать учетку так и не удалось. Было бы еще неплохо иметь встроенную "пинговалку", чтобы сразу отсеять недоступные устройства.

Базовый функционал

Все "ядро" скрипта можно разделить на несколько функций.

devices_from_file

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


def devices_from_file(device_file):
    with open(device_file) as file:
        result = file.read().split('\n')
    return result[0:-1]



Открываем файл, читаем его и сразу делим по переносам строк. В результат отдается все элементы листа, кроме последнего. Это пустой элемент.

ping_ip_address

Тоже ничего особо сложного. Говорим что пиноговать, на выходе получаем словарь из статуса alive или dead и того, что пинговали.


def ping_ip_address(ip):
    pinger = subprocess.run(['ping', '-c', '2', '-n', ip], stdout=subprocess.DEVNULL)
    if pinger.returncode == 0:
        return {'alive':ip}
    else:
        return {'dead':ip}


Ping происходит средствами системы, в которую передается ряд аргументов. Вывод самой команды отправлен в devnull. Subprocess вернет код 0 в случае успеха. В таком случае сама функция отдаст словарь {alive: ip}, иначе получим словарь со статусом dead.

Мне, если честно, реализация не очень по душе.  Во-первых, используется ping средствами самой системы. Таким образом под Windows такая реализация работать не будет. Во-вторых, результат функции в виде словарей мне как-то режет глаз. С другой стороны, все работает, а вопросы кроссплатформенности передо мной особо и не стояли.

connect_and_send

Как раз то, ради чего все затевалось. Функция немного сложней. На выход подаем адрес, файл с учетками в формате YAML и файл с командами. На выходе получаем словарь с ключом в виде переданного функции хоста, а вот статус зависит от результата функции. Статус может принимать такие значения:
  • вывод команд, которые были отправлены на устройство
  • 'No creds found', если подобрать логин-пароль так и не удалось
  • 'Timeout' отдается в случае, если на устройство просто не получилось попасть



def connect_and_send(host, creds_file, command_file):
    with open(creds_file) as file:
        creds = yaml.load(file)
    creds_product = list(itertools.product(creds['usernames'], creds['passwords']))
    output = {}
    for creds in creds_product:
        device_params = {'device_type': 'cisco_ios',
                        'ip': host,
                        'username': creds[0],
                        'password': creds[1],
                        'secret': creds[1],
                        'timeout': 10}
        try:
            with netmiko.ConnectHandler(**device_params) as ssh:
                ssh.enable()
                result = ssh.send_config_from_file(command_file)
            output[host] = result
            break
        except netmiko.ssh_exception.NetMikoAuthenticationException:
            output[host] = 'No creds found'
        except netmiko.ssh_exception.NetMikoTimeoutException:
            output[host] = 'Timeout'
            break
    return output


Первым делом открываем YAML файл с учетками и "перегоняем" его содержимое в Python структуры. Файл представляет из себя словарь с двумя ключами username и password. Значение каждого ключа - список логинов и паролей соотвественно. 

В YAML это выглядит примерно так:
usernames:
    - misha
    - katya
    - petya
    - sasha
passwords:
    - SuperSecretPass1
    - G00D$ecurePa$$password2
    - WTF300
    - ILoveMyWife

Далее формируется лист creds_product, которые представляет из себя все возможные комбинации логинов и паролей. Далее, для каждой такой комбинации формируется словарь device_params, необходимый для работы модуля netmiko, с помощью которого и происходит подключение. Далее начинается перебор паролей. Происходит попытка подключиться, перейти в enable и выполнить команду. Если попытка увенчалась успехом, функция записывает вывод команд в словарь и заканчивает выполнение. Есть обработка двух исключений. Первый связанный собственно с неправильными учетными данными, в результате которого записывается статус 'No creds found' и функция переходит к началу выполнения пытаясь подключится с другими учетками. В случае таймаута функция просто заканчивает цикл после 10 секунд записывая соответсвующий статус.

Concurrent futures...

Ох и нравится мне название этого модуля. Ясно, что нужно каким-то образом параллелить выполнение операций, потому как изначально предполагается, что устройств будет много. В  Python немало способов это сделать я выбрал concurrent futures, в том числе и из-за названия. Смысл модуля довольно прост, он похож на функцию map, которая применяет функцию к каждому элементу последовательности и возвращает итерируемый объект. Только в нашем случае функции будут выполнятся в нескольких тредах или процессах.

ping_ip_addresses

Здесь функция ожидает лист IP адресов. Далее этот лист "мапится" к функции ping_ip_address и выполняется сразу несколькими процессами (30 по умолчанию). После завершения работы всех функций мы получаем итератор, каждый элемент которого представляет из себя словарь {status: ip}. Далее функция просто разбирает каждый такой словарь и формирует лист, в котором два словаря. В одном из них alive адреса, в другом dead. Ах да, в то время как пингуются девайсы крутится "спиннер", мы же современные ребята... ) За этот функционал отвечает модуль Halo.

def ping_ip_addresses(ips, limit = 30):
    with Halo(text=' | Pinging devices...', spinner=dots) as spinner:
        with concurrent.futures.ProcessPoolExecutor(max_workers=limit) as executor:
            pinger_result = list(executor.map(ping_ip_address, ips))
    spinner.stop_and_persist('DONE')
    ip_list = {'alive':[], 'dead':[]}
    for item in pinger_result:
        if 'alive' in item.keys():
            ip_list['alive'].append(item['alive'])
        else:
            ip_list['dead'].append(item['dead'])
    return ip_list



connect_and_send_parallel

Смысл точно такой же, как и в предидущей функции. Сопоставляем connect_and_send с листом хостов. Тут даже проще, на выходе имеем просто итератор. Про спиннер не забываем.

def connect_and_send_parallel(hosts, creds_file, command_file, limit = 30):
    with Halo(text='| Connecting to devices and sending commands...', spinner=dots) as spinner:
        with concurrent.futures.ThreadPoolExecutor(max_workers=limit) as executor:
            grabber = list(executor.map(connect_and_send,
                                        hosts,
                                        itertools.repeat(creds_file),
                                        itertools.repeat(command_file)))
        spinner.stop_and_persist('DONE')
    return grabber

Обвязка

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

argparse

Для парсинга аргументов будем использовать старый добрый argparse. Особо описывать тут нечего, пройдемся лишь по аргументам, на которых я остановился.

  -h, --help          Тут все понятно
  -d DEVICE_FILE      Путь к файлику с устройствами
  -l DEVICE_LIST      Список устройств можно указать и списком
  -c CREDS_FILE       Путь до файла с учетками
  -r COMMAND_FILE     Путь к файлу с командами
  -t CONNECT_THREADS  Количество тредов для SSH
  -p PING_PROCESS     Количество процессов для ping
  --ping              Включает проверку пингом (по умолчанию)
  --no-ping           Отключает "попингуйку"
  --debug             Пишет всякую полузную информация в YAML файлик
  --no-debug          Выключет дебаг (по умолчанию)
  --brief             Включает короткое отображение, без вывода команд
  --no-brief          По умолчанию, выводится таблица с выводом с каждого устройства

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

Получение списка устройств:
если устройства заданы вручную списком,
  то список преобразуется в list
если устройства заданы файлом,
  то формируется лист средствами функции devices_from_file

Первичная проверка pingом:
если ping проверка пропускается,
  то список девайсов из предыдущих условий скармливается функции connect_and_send_parallel
иначе
  сначала проверяем устройства с помощью ping_ip_addresses
  если есть живые устройства
    проверяем список недоступных и предупреждаем пользователя
  иначе
    информируем пользователя о том, что подключаться не к чему

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

Статистическая информация:
Из сокращенного варианта результата считаем количество уникальных статусов и выводим

Запись данных в файл:
Если указан debug ключ, пишем в файл всякую полезную информацию.
  

Результат

Итогом всей работы является скрипт, который каждый желающий может посмотреть здесь. Там же находится краткая инструкция по использованию.

Конечно, каждое изменение в логике я тестировал в лабе, но для финального тестирования я подготовил 50 роутеров в EVE-NG...


Далее, я написал скрипт, который настраивает на каждом из них случайно взятый из списка логин и пароль. Там простой expect, особо рассматривать нечего. После прогона скрипта я приступил к тестированию.



Тестирование

1. Записываем все устройства в data/devices

2. Составялем файл с логинами и паролями

3. Составляем список команд для отправки на устройства


Простой тест по 50 девайсам без дополнительных ключей


Вывод - длинная простыня консольного выхлопа со статистикой (около 5 минут выполнения).

Включим отображение краткой информации (--brief) и запись информации в debug.yaml ключом (--debug).


Вывод - краткая информация со статусами (время выполнение практически аналогичное)


Плюс мы имеем YAML файл со всей необходимой информацией, которую можно легко обработать Python. Файл не с этой сессии.



Над чем поработать?

1. Можно подумать над сменой принципа пинговалки на более кроссплатформенный.
2. Отработка случая, когда для enable режима другой пароль
3. Поддержка других типов устройств
4. Добавить вариант с отображением только статистики без вывода вообще
5. Распараллелить не только опрос устройств, но и перебор учетных данных. Уже пробовал, но тут довольно быстро упираешься в ограничение линий VTY.

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

Спасибо за внимание. 

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

Комментариев нет:

Отправить комментарий