Главная / Блог / Python для CTF: пишем первые скрипты для автоматизации атак

13 мин.00

Python для CTF: пишем первые скрипты для автоматизации атак

Python для CTF: пишем первые скрипты для автоматизации атак

Python для CTF: пишем первые скрипты для автоматизации атак

Первый CTF, задача web-категории на 100 очков: форма логина, в описании — «пароль четырёхзначный, только цифры». Десять тысяч комбинаций. Можно сидеть и вбивать вручную, но таймер тикает, а кофе остывает. Скрипт на Python из 10 строк решает задачу за 40 секунд — и вот с этого момента CTF перестаёт быть квестом на терпение и становится соревнованием в программировании. Собственно, про эти первые скрипты и поговорим: брутфорс через HTTP, работа с сокетами и парсинг ответов. Три сценария, которые закрывают основную массу задач начального уровня.

Что нужно для скриптинга CTF на Python

Требования к окружению

Перед тем как писать первую строчку кода, убедитесь, что окружение готово:

  • ОС: Linux (Kali Linux — всё предустановлено), macOS, или Windows с WSL. Чистый Windows работает для requests, но pwntools капризничает без WSL.
  • Python: версия 3.8 и выше. Проверяем: python3 --version.
  • RAM: 2 ГБ минимум — CTF-скрипты нетребовательны, основная нагрузка идёт на сеть.
  • Сеть: стабильное соединение с доступом к CTF-серверу.
  • Библиотеки: pip install requests pwntools. Если pwntools не ставится (бывает на macOS ARM), для задач из этой статьи хватит стандартного модуля socket.

Библиотека requests (активно поддерживается, один из самых скачиваемых Python-пакетов) — для HTTP-запросов: GET, POST, работа с куками и сессиями. Pwntools (open-source проект, поддерживается командой Gallopsled) — швейцарский нож CTF-игрока: удобная обёртка над сокетами, упаковка данных, генерация пейлоадов.

Python-скрипты или готовые инструменты

Справедливый вопрос: зачем писать код, если есть Burp Suite и sqlmap? Burp перехватывает HTTP-трафик и подходит для ручного анализа. Sqlmap автоматизирует SQL-инъекции (OWASP A03:2021, Injection). Но на CTF с таймером эти инструменты закрывают только часть задач. Сервер присылает математическое выражение и ждёт ответ за 2 секунды — Burp тут бесполезен. Нужно подключиться по TCP на нестандартный порт и отправить бинарные данные — sqlmap мимо.

Подход Плюсы Ограничения Когда не использовать
Python + requests Полный контроль логики, сессии, парсинг Нужно писать код под задачу Когда достаточно ручного запроса в браузере
Python + pwntools CTF-ориентированный, удобные обёртки Не ставится на Windows без WSL Чисто веб-задачи без сокетов
Burp Suite Визуальный интерфейс, перехват трафика Плохо автоматизирует кастомную логику PPC-задачи с TCP-сокетами
Bash + curl Не нужна установка Нет сессий, сложный парсинг ответов Многоступенчатые задачи с state

Python для CTF — это клей между вашим пониманием задачи и её решением: разобрались, что нужно сделать, и пишете ровно тот код, который это выполняет. По классификации MITRE ATT&CK использование Python-интерпретатора для атакующих действий описывается техникой Python (T1059.006, Execution).

Брутфорс на Python: скрипт для перебора паролей

Брутфорс — техника Brute Force (T1110, Credential Access), точнее подвид Password Guessing (T1110.001). На CTF она встречается в задачах web-категории: подобрать пароль к форме логина или PIN-код к API-эндпоинту.

[Применимо: CTF jeopardy, web-категория. На реальном внешнем пентесте — только по согласованию, с контролем rate limit. На внутреннем — при тестировании парольной политики]

Сценарий задачи

Веб-страница с формой логина. Логин известен — admin. Пароль — четырёхзначный PIN. В HTML-форме два поля: username и password, метод POST, action — /login. При неправильном пароле сервер возвращает страницу с текстом «Invalid password».

import requests

url = "http://ctf.example.com/login"
for pin in range(10000):
    password = f"{pin:04d}"  # 0000, 0001, ..., 9999
    r = requests.post(url, data={"username": "admin", "password": password})
    if "Invalid" not in r.text:
        print(f"Пароль найден: {password}")
        print(r.text[:200])  # первые 200 символов — обычно тут флаг
        break

Разбор построчно

Импорт requests — единственная внешняя зависимость. Цикл for pin in range(10000) перебирает числа от 0 до 9999. Форматирование f"{pin:04d}" добавляет ведущие нули: число 7 становится строкой "0007". Метод requests.post() отправляет POST-запрос с данными формы в словаре data — эквивалент заполнения полей в браузере и нажатия «Отправить». Проверка if "Invalid" not in r.text — простейший способ отличить успех от неудачи: нет слова «Invalid» — пароль подошёл.

Тут есть подвох. Это компромисс простоты: надёжнее дополнительно сравнивать r.status_code, длину r.text или искать маркер успеха (например, "Welcome"). Иначе любой нестандартный ответ сервера (429, 500, редирект) станет ложным позитивом. На первых CTF это некритично, но привычку проверять статус-код лучше вырабатывать сразу.

Вывод r.text[:200] показывает начало страницы — в CTF-задачах флаг обычно лежит прямо в теле ответа.

Что менять под реальную задачу

На практике задача редко бывает такой чистой. Сервер может вернуть 302 вместо 200 при успешном входе — тогда проверяйте r.status_code или используйте allow_redirects=False в requests.post(). Если форма защищена CSRF-токеном, его нужно вытащить из HTML перед каждым запросом — об этом в разделе про парсинг.

Для словарного перебора вместо генерации PIN-кодов подключается файл. Заменяете цикл на чтение: for password in open("rockyou.txt") и добавляете password = password.strip() для удаления переноса строки. Классический rockyou.txt (утечка RockYou, 2009) содержит около 14,3 млн паролей. По данным Have I Been Pwned, только утечка Exploit.In 2016 года содержала 593 млн уникальных email-адресов с паролями — словарные атаки работают не только на CTF.

Если сервер отдаёт JSON вместо HTML, меняете проверку: r.json()["status"] == "error" вместо "Invalid" not in r.text. Заголовок User-Agent тоже стоит добавить: headers={"User-Agent": "Mozilla/5.0"} — некоторые серверы фильтруют запросы без него.

Ограничения брутфорс-скрипта

Скрипт отправляет запросы последовательно — примерно 10-20 запросов в секунду в зависимости от latency. Для 10 000 комбинаций хватает, для миллиона — медленно. Ускорить можно через concurrent.futures.ThreadPoolExecutor или aiohttp, но на CTF это редко нужно: если задача требует перебора миллиона вариантов, скорее всего подход неверный и надо искать другую уязвимость.

На реальном пентесте Python-брутфорс упирается в защиты: rate limiting (блокировка IP после N попыток), CAPTCHA, WAF, блокировка аккаунта. На CTF начального уровня этих защит почти нет. В задачах на 300+ очков бывает rate limiting — тогда добавляйте time.sleep(0.5) между запросами.

Socket и Python: подключаемся к CTF-сервису напрямую

Не все CTF-задачи — веб-формы. Категории PPC (Professional Programming and Coding) и pwn работают через чистый TCP: сервер слушает на порту, вы подключаетесь, получаете задание, отправляете ответ. HTTP тут ни при чём — это уровень ниже, сырые сокеты. По MITRE ATT&CK взаимодействие через протоколы ниже прикладного уровня — Non-Application Layer Protocol (T1095, Command and Control), а сканирование портов для обнаружения таких сервисов — Network Service Discovery (T1046, Discovery).

[Применимо: CTF jeopardy, категории pwn/ppc/crypto. На внешнем пентесте — при взаимодействии с нестандартными сервисами. На внутреннем — для тестирования проприетарных TCP-протоколов]

Сценарий задачи

PPC-задача: сервер на ctf.example.com:1337 присылает строку вида Solve: 847 + 293, ждёт ответ в течение 2 секунд. Правильный ответ — следующий пример. После 50 правильных ответов подряд — отдаёт флаг. Вручную не успеть: ввести 50 ответов за 100 секунд с переключением между калькулятором и терминалом — нереально.

from pwn import remote

conn = remote("ctf.example.com", 1337)
for _ in range(50):
    line = conn.recvline().decode().strip()   # "Solve: 847 + 293"
    expr = line.split(": ")[1]                 # "847 + 293"
    result = eval(expr)                        # 1140
    conn.sendline(str(result).encode())
print(f"Флаг: {conn.recvline().decode()}")
conn.close()

Разбор по строкам

remote() из pwntools создаёт TCP-соединение — аналог socket.connect(), но с удобными обёртками для приёма и отправки. conn.recvline() читает данные до символа \n и возвращает байтовую строку. Декодируем из байтов в текст через .decode() и убираем пробельные символы через .strip().

Парсинг: line.split(": ")[1] разрезает текст по двоеточию с пробелом и берёт вторую часть — само выражение. eval() вычисляет строковое выражение как Python-код: передали "847 + 293", получили 1140.

На CTF это допустимо, если вы уверены, что сервер присылает только арифметику. Но безопаснее разобрать выражение руками: a, op, b = expr.split(); result = {"+": int(a)+int(b), "-": int(a)-int(b), "*": int(a)*int(b)}[op]. Для более сложных выражений подойдёт ast.literal_eval или библиотека simpleeval. В боевом коде eval() использовать нельзя — если противник контролирует входные данные, через eval() можно выполнить произвольный код на вашей машине.

conn.sendline() отправляет ответ с автоматическим \n — большинство CTF-серверов ожидают перевод строки. После 50 итераций забираем флаг финальным conn.recvline().

Если pwntools не установлен

Всё то же самое делается на стандартном socket: создаёте сокет через s = socket.socket(socket.AF_INET, socket.SOCK_STREAM), подключаетесь через s.connect(("ctf.example.com", 1337)), принимаете данные через s.recv(1024), отправляете через s.send(answer.encode() + b"\n"). Работает, но без удобств pwntools: нет recvline(), нужно самому обрабатывать буфер и следить за тем, чтобы строка пришла целиком.

Ловушки при работе с сокетами

Неполный приём данных. Сервер может отправить строку несколькими TCP-пакетами — один вызов recv() не гарантирует, что вы получили всё сообщение. Pwntools решает это за вас: recvline() дожидается \n. На чистом socket приходится накапливать буфер в цикле, пока не встретится нужный разделитель. Сокеты — анархисты, они не знают про «сообщения», только про поток байтов.

Тайм-аут. Если скрипт считает слишком долго, сервер закрывает соединение. В pwntools тайм-аут задаётся через conn.timeout = 5. На стандартном сокете — через s.settimeout(5). Не забывайте обрабатывать socket.timeout.

Кодировка. Pwntools работает с байтами (bytes), не со строками (str). Отправляете conn.sendline(str(result).encode()), не conn.sendline(str(result)). Забыли .encode() — получите TypeError на первой же итерации.

Парсинг HTTP-ответов: автоматизация CTF задач на Python

Многие задачи web-категории требуют не просто отправить один запрос, а выстроить цепочку: получить страницу, вытащить из неё скрытое значение (токен, cookie, часть флага), использовать это значение в следующем запросе. По MITRE ATT&CK это Automated Collection (T1119, Collection) и Web Protocols (T1071.001, Command and Control).

[Применимо: CTF jeopardy, web-категория. На внешнем пентесте — при автоматизации тестирования веб-приложений. На внутреннем — при скраппинге данных из корпоративных порталов]

Сценарий задачи

Сервер отдаёт страницу с формой. В форме — скрытое поле token со случайным значением, которое меняется при каждом запросе. Нужно отправить POST с правильным токеном и правильным ответом на вопрос, который тоже меняется. Без скрипта не решить — токен живёт 3 секунды.

import requests, re

session = requests.Session()
r = session.get("http://ctf.example.com/challenge")
token = re.search(r'name="token" value="([^"]+)"', r.text).group(1)
question = re.search(r'What is (\d+ [+\-*] \d+)', r.text).group(1)
data = {"token": token, "answer": str(eval(question))}
r2 = session.post("http://ctf.example.com/challenge", data=data)
print(r2.text)

Разбор построчно

Ключевой элемент — requests.Session(). Сессия автоматически сохраняет cookies между запросами: если сервер выставил Set-Cookie при GET, эти cookies подставятся в POST. Без сессии пришлось бы вручную вытаскивать cookies через r.cookies и передавать их дальше.

Модуль re (регулярные выражения) — основной инструмент парсинга на CTF. Паттерн r'name="token" value="([^"]+)"' ищет в HTML атрибут name="token" и захватывает значение value в скобочную группу. .group(1) возвращает содержимое первой группы — сам токен. Второй regex вытаскивает математическое выражение из текста страницы. Результат вычисляется через eval() и отправляется вместе с токеном.

Альтернативы регулярным выражениям

Для простого HTML с предсказуемой структурой re хватает за глаза. Для сложного HTML с вложенными тегами надёжнее BeautifulSoup: ставится через pip install beautifulsoup4, импортируется как from bs4 import BeautifulSoup. Вытащить токен: soup = BeautifulSoup(r.text, "html.parser"), затем soup.find("input", {"name": "token"})["value"]. Работает надёжнее regex, но тащит дополнительную зависимость.

Если сервер отдаёт JSON (всё чаще в современных CTF), используйте r.json() вместо парсинга текста. Возвращает Python-словарь: data = r.json() и обращаетесь к полю как data["token"]. Никаких регулярных выражений — чистая работа со структурой данных.

Место Python-скриптов в цепочке атаки

Каждый скрипт из статьи занимает конкретное место в kill chain. На CTF цепочка обычно линейная: разведка → эксплуатация → получение флага. На реальном пентесте этапов больше, но принцип тот же.

Разведка (Discovery). Перед тем как писать брутфорс-скрипт на Python, нужно понять, что за сервис перед вами. На CTF порт и протокол обычно указаны в описании задачи. На пентесте для этого используется Network Service Discovery (T1046) — сканирование портов тем же nmap.

Получение доступа (Credential Access). Брутфорс-скрипт — Password Guessing (T1110.001). Работает, когда есть точка входа (форма логина, SSH-порт, API), но нет пароля. Результат — учётные данные для следующего этапа.

Исполнение (Execution). Сам Python-скрипт — T1059.006. На CTF интерпретатор Python — основной рабочий инструмент. На пентесте Python используется для доставки пейлоадов, автоматизации сбора данных, написания exploit-скриптов.

Сбор данных (Collection). Парсинг ответов — Automated Collection (T1119). Скрипт автоматически извлекает нужные данные из ответов сервера: токены, фрагменты флага, подсказки для следующего шага.

Понимание этой цепочки помогает и на CTF: если задача не решается брутфорсом, возможно, вы пропустили разведку. Перечитайте описание задачи, посмотрите HTTP-заголовки через r.headers, проверьте cookies через r.cookies — иногда флаг или подсказка спрятаны прямо там.

Ограничения и когда Python-скрипты не работают

Rate limiting и anti-brute-force. Задачи средней и высокой сложности могут содержать защиту: блокировка IP после N попыток, exponential backoff, временная блокировка аккаунта. Обходы: замедление скрипта через time.sleep(), смена подхода (может, задача решается SQL-инъекцией, а не перебором), или поиск утечки в другом месте. Ротация IP через прокси на CTF обычно запрещена правилами.

WAF и фильтрация. Web Application Firewall может блокировать автоматизированные запросы: нет User-Agent, аномально частые запросы, подозрительные паттерны в теле. Добавление заголовков решает простейшие фильтры, но полноценный WAF обходится уже не брутфорсом, а логической уязвимостью в приложении.

Бинарные протоколы и pwn. Когда сервер общается не текстом, а бинарными данными (категория pwn), одного recv/send недостаточно. Нужно разбираться в формате: struct.pack() и struct.unpack() для упаковки чисел в байты, понимать little-endian и big-endian. Pwntools помогает функциями p32(), p64(), u32(), u64(), но это тема отдельного разбора.

Многопоточность — не панацея. Заливать тысячи одновременных соединений на CTF-сервер — путь к бану. Если задача хранит state (сессия, последовательность ответов), параллелизм ломает логику. Начинайте с однопоточного скрипта и добавляйте потоки только когда это реально необходимо.

Время — главный ограничитель. На CTF обычно 24-48 часов на все задачи. Написать скрипт, обрабатывающий все edge cases, — роскошь. Практический подход: минимальный скрипт, запуск, наблюдение за ошибками, добавление обработки по ходу. Итерация быстрее проектирования.

Три подхода из этой статьи — requests.post() для HTTP-брутфорса, remote() из pwntools для сокетов, re.search() для парсинга ответов — закрывают основную массу задач начального уровня. Остальное требует криптографии (pycryptodome), реверс-инжиниринга (Ghidra, GDB), форензики (binwalk) или эксплуатации бинарных уязвимостей, где Python — обёртка над шеллкодом.

Одна ошибка повторяется у новичков с завидной регулярностью: попытка освоить все CTF-категории одновременно. На первых трёх-четырёх соревнованиях разумнее сосредоточиться на web и PPC — именно тут скриптинг CTF на Python даёт максимальную отдачу. Когда базовая автоматизация станет привычкой и синтаксис requests.post() перестанет требовать подглядывания в документацию, переходите к pwn и crypto.

По моему опыту, переход от «копирую чужие скрипты из writeup» к «пишу свои за 5 минут» занимает около двух месяцев — по 2-3 задачи в неделю. Решать сложные задачи не нужно. Нужно решать простые, но каждый раз писать скрипт, даже если задачу можно решить руками. Это формирует рефлекс: увидел форму — открыл requests, увидел порт — открыл pwntools. Остальное нарабатывается количеством решённых задач. Если хотите структурировать этот процесс и не тыкаться в одиночку — на IB Basics в Codeby School берут с любого старта и ведут до первых реальных задач, без академического тона.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...