• 📣 БЕСПЛАТНЫЙ ВЕБИНАР WAPT & SQLiM: взламываем веб как профессионалы. Ждем вас 3 апреля 19:00 (МСК). регистрация здесь. Что разберём: Поиск уязвимостей через фаззинг, Реальные SQL- и командные инъекции (с выводом RCE), Эскалация привилегий после взлома, Разбор похожих задач из курсов WAPT и SQLiM.

    >>> Подробнее <<<

Статья Создание AWScanner: Пошаговое руководство по сканированию IP AWS для безопасности

Введение
В процессе работы в Red Team часто приходится проводить разведку облачных сервисов. Для этого я решил написать инструмент AWScanner, который автоматически собирает и анализирует диапазоны IP Amazon Web Services с помощью Masscan и TLS-сканирования. В этой статье я разберу ход своих мыслей при создании сканера, объясню каждую функцию, чтобы даже новичок понял, как это работает, чтобы он смог создавать в будущем свои инструменты по ИБ.
Советую ознакомиться ещё с мой статьёй в которой разбирается сканирование облачных сервисов Статья - Жёстко сканируем и хакаем облачные сервисы Amazon

Определение целей

Когда я задумался о написании сканера, у меня были следующие требования:
  1. Скорость - облака масштабируемы, IP-диапазоны огромны, поэтому необходимо использовать быстрые инструменты.
  2. Автоматизация - минимальное количество ручных действий.
  3. Информативность - важно не только найти открытые порты, но и извлечь полезную информацию, например, домены сертификатов.

Структура скрипта

Я сделал AWScanner в виде командной строки с интерактивным интерфейсом. Пользователь может вводить команды, такие как list_regions для просмотра доступных регионов, scan <region> <rate> для запуска сканирования.
Это сделало инструмент простым в использовании, особенно для тех, кто только начинает заниматься пентестингом.

Реализация функций

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

Цветной вывод и удобство использования

Сначала добавил поддержку цветного вывода, чтобы пользователю было проще ориентироваться:
Python:
class Colors:
    RED = '\033[91m'
    GREEN = '\033[92m'
    RESET = '\033[0m'
    UNDERLINE = '\033[4m'
Это простой класс для цветного вывода в терминале. Например, зелёный цвет (GREEN) используется для успешных сообщений, а красный (RED) - для ошибок. Это делает интерфейс более читаемым.

Список регионов

Отфильтровал в огромнос json файле при помощи команд для обработки строк, таких как jq, awk и прочее.
Python:
AVAILABLE_REGIONS = [
    "af-south-1", "ap-east-1", "ap-east-2", ..., "us-west-2"
]

Функция print_available_regions
Python:
def print_available_regions():
    print("Available regions:")
    for region in AVAILABLE_REGIONS:
        print(f" {Colors.GREEN}-{Colors.RESET} {region}")
Эта функция просто выводит список регионов с зелёным тире перед каждым, чтобы было красиво.
Это помогает пользователю увидеть, какие регионы доступны.

Функция run_command
Python:
def run_command(command):
    try:
        subprocess.run(command, shell=True, check=True, stderr=subprocess.DEVNULL)
    except subprocess.CalledProcessError as e:
        print(f"{Colors.RED}[ERROR]{Colors.RESET} Command failed: {command}")
        sys.exit(1)
Эта функция выполняет команды в оболочке и обрабатывает ошибки. Если команда не сработает, она выведет красное сообщение и завершит скрипт.

check=True - если команда завершится с кодом возврата != 0 (ошибка), Python вызовет исключение CalledProcessError.
stderr=subprocess.DEVNUL - перенаправляет стандартный поток ошибок (stderr) в /dev/null (ошибки не выводятся).

Функция is_file_empty
Python:
def is_file_empty(file_path):
    return not os.path.exists(file_path) or os.path.getsize(file_path) == 0
Эта функция проверяет, существует ли файл и пустой ли он. Например, если после сканирования файл с открытыми IP пуст, мы не будем пытаться его обрабатывать.
os.path.exists(file_path) - Возвращает False, если файл не существует
os.path.getsize(file_path) == 0 - Если файл существует, проверяет его размер.

Функция scan_region
Python:
def scan_region(region, rate):
    ip_ranges_command = (
        f"wget -qO- https://ip-ranges.amazonaws.com/ip-ranges.json | "
        f"jq '.prefixes[] | select(.region == \"{region}\") | .ip_prefix' -r | "
        f"sort -u > {region}-range.txt"
    )
run_command(ip_ranges_command)
Загружаем IP-диапазоны для региона с помощью wget и jq, сохраняем в файл.

wget загружает JSON-файл с публичными IP-диапазонами AWS.
  • jq фильтрует данные;
  • prefixes[] - перебирает массив CIDR-блоков;
  • select(.region == "region") - выбирает блоки, привязанные к указанному региону (например, eu-central-1);
  • ip_prefix - извлекает значение поля (например, 52.93.244.0/24);
  • -r - выводит результат в raw-формате (без кавычек).
  • sort -u удаляет дубликаты и сохраняет уникальные значения в файл {region}-range.txt;
Python:
    print(f"{Colors.GREEN}[+]{Colors.RESET} IP ranges for {region} saved to {region}-range.txt")

    masscan_command = f"masscan -iL {region}-range.txt -oL {region}-range.masscan -p 443 --rate {rate}"
    run_command(masscan_command)
    print(f"{Colors.GREEN}[+]{Colors.RESET} Masscan results saved to {region}-range.masscan")
Запускаем masscan для сканирования этих IP на порт 443 с заданной скоростью и выводим, что результат сохранён в файл
  • -iL - указывает файл со списком целей (CIDR-блоков из {region}-range.txt);
  • -oL - сохраняет результаты в файл {region}-range.masscan;
  • -p 443 - сканирует только порт 443;
  • --rate - задает скорость сканирования (пакетов/секунду).
Python:
    tls_open_command = f"awk '/open/ {{print $4}}' {region}-range.masscan > {region}-range.tlsopen"
    run_command(tls_open_command)
    print(f"{Colors.GREEN}[+]{Colors.RESET} Open TLS IPs saved to {region}-range.tlsopen")
Извлекаем IP с открытым портом 443 с помощью awk.
  • awk парсит файл {region} - range.masscan;
  • /open/ - ищет строки с состоянием open;
  • print $4 - извлекает 4-е поле (IP-адрес);
  • Результат сохраняется в {region}-range.tlsopen.

Python:
    if not is_file_empty(f"{region}-range.tlsopen"):
        tls_scan_command = f"cat {region}-range.tlsopen | tls-scan --port=443 -o {region}-range-tlsinfo.json"
        run_command(tls_scan_command)

        convert_to_csv(region)
    else:
        print(f"{Colors.RED}[WARNING]{Colors.RESET} TLS open IPs file is empty, skipping CSV conversion.")
Тут собираем данные о сертификатах (например, Common Name) для найденных IP.
  • tls-scan подключается к каждому IP из {region}-range.tlsopen на порту 443;
  • Извлекает информацию о TLS-сертификате (версия, алгоритмы, Common Name и др.);
  • Сохраняет результат в JSON-файл {region}-range-tlsinfo.json.
и дальше происходит конвертация в csv табличку

Функция convert_to_csv
Python:
def convert_to_csv(region):
    """Convert TLS info JSON to CSV format for different JSON structures."""
    json_file = f"{region}-range-tlsinfo.json"
    csv_file = f"{region}-range-tlsinfo.csv"

    if is_file_empty(json_file):
        print(f"{Colors.RED}[WARNING]{Colors.RESET} TLS info file {json_file} is empty or missing, skipping CSV conversion.")
        return

    # Attempt to convert JSON to CSV using various jq commands
    csv_commands = [
        f"jq -r '.[] | {{ip, commonName: .cert.subject.commonName}} | [.ip, .commonName] | @csv' {json_file} > {csv_file}",
        f"jq -r '.[] | {{ip, commonName: .cert.subject.commonName}} | [.ip, .commonName] | @csv' {json_file} > {csv_file}",
        f"jq -r '[.ip, .cert.subject.commonName] | @csv' {json_file} > {csv_file}",
        f"jq -r '[.ip, .commonName] | @csv' {json_file} > {csv_file}"
    ]

    for command in csv_commands:
        try:
            run_command(command)
            print(f"{Colors.GREEN}[+]{Colors.RESET} TLS info converted to CSV and saved to {csv_file}")
            return
        except:
            continue  # Try the next command if the current one fails

    print(f"{Colors.RED}[ERROR]{Colors.RESET} Failed to convert {json_file} to CSV.")
Эта функция преобразует JSON в CSV, но я добавил несколько вариантов команд jq, потому что структура JSON может отличаться на разных регионах. Например, иногда поле называется .cert.subject.commonName, а иногда просто .commonName. Поэтому я сделал массив csv_commands, я его сделал, потому что одной командой не получилось спарсить и перевести все запросы в табличку (да, я тестировал на большинстве регионах, чтобы отследить ошибки в моей программе).

Основная функция main
Python:
def main():
    if os.geteuid() != 0:
        print(f"{Colors.RED}[ERROR]{Colors.RESET} You must run this script as the root user.")
        sys.exit(1)

    while True:
        print(f"{Colors.UNDERLINE}awscan{Colors.RESET} > ", end='')
        user_input = input()

        if user_input == "help":
            print_help()
        elif user_input == "list_regions":
            print_available_regions()
        elif user_input.startswith("scan"):
            _, region, rate = user_input.split()
            if region not in AVAILABLE_REGIONS:
                print(f"{Colors.RED}[ERROR]{Colors.RESET} You must specify a correct region.")
                continue
            scan_region(region, int(rate))
        elif user_input == "exit":
            print("See you later!")
            exit(0)
Это главная функция, которая запускает интерактивный цикл. Сначала проверяет, запущен ли скрипт от root, затем ждёт ввода команд. Если пользователь введёт scan eu-central-1 10000, скрипт вызовет scan_region с этими параметрами.
Python:
_, region, rate = user_input.split()
Разбивает строку по пробелам и присваивает значения переменным:
_ - игнорирует первый элемент ("scan"), так как он не нужен.
region - сохраняет название региона (например, us-west-1).
rate - сохраняет скорость сканирования (например, 10000).

Функция print_help
Python:
def print_help():
    """Display help information for commands."""
    print(f"""Description: AWS IP range scanner with Masscan and TLS analysis made by q1ncite
Core commands
=============

       Command                     Description
    -------------                 -------------
    list_regions                    Show available regions for scanning.
    scan <region> <rate>            Start scan of region (e.g., {Colors.RED}scan eu-central-1 10000{Colors.RESET}).
""")
Эта функция выводит помощь, показывая доступные команды.

Таблица: Основные функции и их назначение

ФункцияНазначение
print_available_regionsВыводит список доступных регионов AWS.
run_commandВыполняет команды в оболочке и обрабатывает ошибки.
is_file_emptyПроверяет, пустой ли файл или не существует.
scan_regionВыполняет сканирование региона: загрузка IP, masscan, анализ TLS.
convert_to_csvПреобразует JSON с информацией TLS в CSV, учитывая разные структуры.
mainЗапускает интерактивный цикл и обрабатывает команды пользователя.
print_helpПоказывает помощь с описанием команд.


Заключение

Я надеюсь, что этот подробный разбор поможет вам, особенно если вы только начинаете, понять, как создавать подобные инструменты.
Разработка AWScanner была интересным процессом, который помог мне глубже понять Python, т.к я не программист.
Этот скрипт можно улушать! Как? Предлагайте идеи или форкайте проект :)

Исходники и дополнения:
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!