Главная / Блог / Python для CTF: автоматизируем перебор, сокеты и парсинг флагов — тулкит на requests и pwntools

14 мин.00

Python для CTF: автоматизируем перебор, сокеты и парсинг флагов — тулкит на requests и pwntools

Python для CTF: автоматизируем перебор, сокеты и парсинг флагов — тулкит на requests и pwntools

Python для CTF: автоматизируем перебор, сокеты и парсинг флагов

Два CTF-сценария, где руки бесполезны: веб-форма с PIN-кодом на 10 000 комбинаций и TCP-сервис, который кидает арифметическое выражение с окном ответа в две секунды. Первый закрывается скриптом на requests за 30-60 секунд, второй — десятью строками на pwntools. И вот тут разница между участником, который завис на первом веб-таске, и тем, кто собрал шесть флагов за четыре часа — не в объёме знаний Python, а в конкретном наборе приёмов: отправить POST, вытащить CSRF-токен из HTML, подключиться к сокету, распарсить ответ регуляркой, найти offset переполнения. Ниже — рабочие скрипты для каждого сценария, типичные ошибки при автоматизации CTF на Python и объяснение, когда тащить requests, когда pwntools, а когда хватит голого socket.

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

  • ОС: Linux (Kali, Ubuntu 20.04+), macOS. Windows работает, но pwntools под Windows — боль: функция process() для локальных бинарей не поддерживается, доступен только remote(). Если сидите на Windows — ставьте WSL2, не тратьте время на костыли.
  • Python: 3.8 и выше.
  • RAM: 2 ГБ хватит для скриптов. Если планируете гонять gdb с pwndbg или GEF параллельно — от 4 ГБ.
  • Установка зависимостей: создайте виртуальное окружение через python3 -m venv ctf-env, активируйте командой source ctf-env/bin/activate и поставьте пакеты: pip install requests beautifulsoup4 pwntools. Виртуальное окружение — страховка: если что-то сломалось, удалили папку и начали заново.

По статусу поддержки: requests и pwntools — живые проекты с тысячами звёзд на GitHub и регулярными обновлениями. BeautifulSoup4 — стабильная библиотека, которая практически не меняется. Все три безопасно тянуть в рабочие скрипты.

[Применимо: все категории CTF. Для pwn-категории на Windows — только remote-подключения, локальная отладка бинарей требует Linux или WSL2.]

Python-тулкит для CTF: requests, pwntools или голый socket

Выбор инструмента определяется категорией таска и протоколом. Ошибка на этом этапе — тратить время на борьбу с неподходящей библиотекой вместо решения задачи. Таблица ниже за десять секунд даёт ответ, что импортировать.

Критерий requests pwntools socket (stdlib)
Протокол HTTP/HTTPS Raw TCP, SSH, процессы Raw TCP/UDP
Категория CTF web pwn, network, crypto network (редко)
Управление сессией/куками Нативно через Session Нет (не HTTP) Вручную
Упаковка адресов p32/p64 Нет Да Нет
Генерация шеллкода Нет Да (shellcraft) Нет
Анализ ELF-бинарей Нет Да (класс ELF) Нет
Поиск offset переполнения Нет Да (cyclic/cyclic_find) Нет
Кривая обучения Низкая Средняя Высокая для CTF-задач
Когда НЕ использовать pwn/network задачи Простые HTTP-запросы Всегда, если pwntools доступен

Короткое правило: если в условии таска URL с http:// — тащите requests. Если написано nc challenge.ctf.com 1337 — это pwntools. Голый socket покрывает теоретически то же, что pwntools, но без удобств: нет recvuntil(), нет p32(), нет cyclic(). Брать его стоит только для понимания основ или когда pwntools нельзя поставить — а такое бывает крайне редко.

Смешивать оба в одном скрипте — нормально. Таск может требовать сначала вытащить адрес с веб-страницы через requests.get(), а потом отправить payload по TCP через remote(). Импортируйте оба и используйте каждый для своей задачи.

Брутфорс на Python: requests и парсинг флагов из ответов

Брутфорс формы авторизации — техника Password Guessing (T1110.001, тактика Credential Access по MITRE ATT&CK). В CTF она встречается на этапе Initial Access: организаторы дают веб-приложение с формой логина и словарь или подсказку вроде «пароль — четырёхзначный PIN». Задача — подобрать комбинацию и попасть на страницу с флагом.

Логика скрипта для брутфорса на Python укладывается в четыре действия: загрузить словарь, для каждого пароля отправить POST-запрос, проверить ответ на признак успеха, остановиться при нахождении.

Три момента, без которых скрипт не заработает:

Сессия. Объект requests.Session() сохраняет куки между запросами. Без него каждый POST — новый визитор для сервера: CSRF-токен протухает, сессия авторизации не сохраняется, а капча (если есть) запрашивается заново.

Имена полей формы. Вытаскиваете из HTML: открываете страницу логина, F12 в браузере, находите <form> и смотрите атрибуты name у <input>. Типичные варианты: username и password, но попадаются user, login, pass — копируйте точное значение.

Условие успеха. В CTF работает проверка от обратного: если в теле ответа нет строки «Invalid», «Wrong» или «Error» — вход, скорее всего, удался. Альтернатива — r.status_code: код 302 (редирект) после POST обычно означает успешную авторизацию.

Скрипт ниже решает сразу две типичные задачи: перебор пароля и обход CSRF-защиты. Перед каждой попыткой отправляется GET для получения свежего токена из скрытого поля формы:

s = requests.Session()
url = "http://ctf.example.com/login"
for pwd in open("passwords.txt"):
    pwd = pwd.strip()
    soup = BeautifulSoup(s.get(url).text, "html.parser")
    token = soup.find("input", {"name": "csrf_token"})["value"]
    r = s.post(url, data={"username": "admin", "password": pwd, "csrf_token": token})
    if "Invalid" not in r.text:
        print(f"[+] Пароль: {pwd}")
        break

Идём по файлу построчно, убираем переносы через strip(), извлекаем CSRF-токен через BeautifulSoup и подставляем в POST. Без этого шага форма с CSRF-защитой вернёт 403 или «Invalid token». Словарь на 10 000 строк прогоняется за 30-90 секунд в зависимости от задержки сервера. Двойной запрос (GET + POST) на каждую попытку замедляет перебор вдвое, но без него брутфорс защищённых форм просто не работает.

Автоматический парсинг флагов из ответов

После успешного входа флаг обычно спрятан где-то в HTML. Стандартные форматы: flag{...}, CTF{...}, codeby{...}. Надёжный способ — регулярка: re.search(r'flag\{[^}]+\}', r.text) найдёт флаг в любом месте страницы, включая комментарии и вложенные теги. Если формат известен из правил соревнования (а он указан почти всегда), регулярка быстрее ручного ковыряния в HTML.

Ещё один приём: организаторы CTF любят прятать флаги в атрибутах тегов. Если флаг лежит в <div data-flag="flag{secret}">, BeautifulSoup вытащит его через soup.find("div")["data-flag"]. Проверяйте не только текст страницы, но и атрибуты — tag.get_text() для текста и tag["attr"] для атрибутов.

Сбор данных из ответов в цикле — Automated Collection (T1119, тактика Collection по MITRE ATT&CK): скрипт программно собирает информацию из каждого HTTP-ответа.

Ограничения и edge cases:

  • Rate limiting — сервер блокирует IP после N неудачных попыток. В CTF встречается редко, но бывает. Обход: time.sleep(0.3) между запросами.
  • JavaScript-рендеринг — если форма работает через AJAX и DOM-манипуляции, простой POST не сработает. Нужен headless-браузер (Selenium, Playwright), но в подавляющем большинстве CTF формы — стандартный HTML.
  • Нестандартные ответы — иногда разница между верным и неверным паролем не в тексте, а в длине ответа (len(r.text)) или заголовках (r.headers). Добавьте логирование длины и кода статуса, если проверка по тексту не даёт результата.

[Применимо: CTF web-задачи, внутренний пентест без WAF. Не применимо: внешний пентест с Cloudflare, Captcha, rate limiting.]

Работа с сокетами через pwntools для начинающих

Категории pwn и network в CTF работают через raw TCP: подключаешься к серверу на конкретном порту, получаешь текстовый или бинарный вывод, отправляешь payload. Библиотека requests тут бесполезна — она заточена под HTTP. По MITRE ATT&CK: Python для эксплуатации — T1059.006 (Execution), эксплуатация уязвимого сервиса — Exploit Public-Facing Application (T1190, Initial Access).

Соединение создаётся вызовом remote("host", port) для удалённого сервера и process("./binary") для локального бинарника. Оба возвращают объект tube с одинаковым интерфейсом — и вот это ключевое: разрабатываете эксплойт локально, затем переключаетесь на удалённую цель заменой одной строки.

Методы приёма данных: recvline() читает одну строку (до \n), recvuntil(b":") — всё до указанного разделителя, recv(n) принимает ровно n байт, recvall() читает до разрыва соединения. Для отправки: send(data) отправляет данные как есть, sendline(data) добавляет \n — и это критично для программ, ожидающих ввод по Enter.

Характерный сценарий автоматизации CTF на Python: сервер на порту 1337 отправляет математическое выражение и ждёт ответ за 2 секунды. Руками не успеть:

from pwn import *
r = remote("ctf.example.com", 1337)
for _ in range(100):
    line = r.recvuntil(b"= ?").decode()
    expr = line.split(":")[1].replace("= ?", "").strip()
    result = str(eval(expr))  # пример для демонстрации концепции
    r.sendline(result.encode())
flag = r.recvall().decode()
print(flag)

Подключаемся к серверу, в цикле принимаем строку до маркера = ?, вырезаем арифметическое выражение, вычисляем результат и отправляем обратно. После 100 раундов сервер отдаёт флаг через recvall(). Вся операция — секунды. Руками — часы.

Чтение адресов и отладка через debug-режим

В pwn-тасках сервер часто печатает адрес функции или leak адреса libc — типичный вывод: setvbuf is at: 0x7f1234567890. Паттерн парсинга: r.recvuntil(b"at: "), затем addr_str = r.recvline().strip() забирает остаток строки, addr = int(addr_str, 16) конвертирует hex в число. Этот приём — стандарт для задач с ASLR leak, где адреса рандомизированы при каждом запуске и их нужно перехватывать в рантайме.

Самый полезный отладочный инструмент: context.log_level = 'debug'. Эта строка заставляет pwntools печатать каждый отправленный и принятый байт. Видите, что сервер реально отправил и что ваш скрипт вернул. Без этого — вслепую гадаете, почему recvuntil() повис.

Самая частая проблема новичков: скрипт зависает на recvline(). В 90% случаев причина — программа ждёт пользовательского ввода, а не отправляет данные. Скрипт вызывает recv, а сервер уже остановился и ждёт. Решение: таймаут — recvline(timeout=5) вернёт пустую строку вместо бесконечного ожидания. Или r.clean() для сброса буфера перед следующей операцией.

Ещё ловушка: recvuntil(b"prompt") ищет точное совпадение байт. Сервер шлёт b"Prompt" с заглавной или добавляет пробел после двоеточия, а в скрипте recvuntil(b"prompt:") — и зависание. Всегда проверяйте реальный вывод через debug-режим.

Написание эксплойтов на Python: cyclic, packing и ELF

Для pwn-категории pwntools даёт инструменты, которые превращают часы ручной работы в минуты. Три вещи: поиск offset для переполнения буфера, упаковка адресов и анализ бинарей без внешних утилит.

Поиск offset через cyclic. Классическая задача: выяснить, сколько байт нужно отправить, чтобы перезаписать instruction pointer (EIP на x86, RIP на x64). Без pwntoolspattern_create/pattern_offset из Metasploit. С pwntools — два вызова. cyclic(200) генерирует уникальный паттерн длиной 200 байт (каждая 4-символьная подпоследовательность уникальна). cyclic_find(crash_value) по значению из дампа возвращает точный offset. Запускаете бинарь через process("./vuln"), отправляете cyclic(200), ждёте краш, читаете program counter из p.corefile.pc и подставляете в cyclic_find(). Результат — число, указывающее, где в буфере начинается перезапись адреса возврата.

Предусловие для x64: по умолчанию cyclic() генерирует 4-байтовые подпоследовательности (для x86). На 64-битной архитектуре нужно context.arch = 'amd64' перед вызовом — иначе offset будет неверным. Я на этом терял по полчаса, пока не вбил себе в привычку ставить контекст первой строкой.

Упаковка адресов. p32() и p64() заменяют struct.pack() без необходимости запоминать коды форматирования. p32(0xdeadbeef) возвращает b'\xef\xbe\xad\xde' (little-endian). Обратная операция: u32(b'\xef\xbe\xad\xde')0xdeadbeef. Если context.binary установлен, pack() автоматически выбирает разрядность и endianness.

Анализ бинарей через ELF. Класс ELF загружает бинарный файл: exe = ELF('./vuln', checksec=True). Параметр checksec=True сразу покажет защиты (NX, PIE, RELRO, canary) — от этого зависит вся стратегия. Адрес функции win: exe.symbols['win']. GOT-запись для puts: exe.got['puts']. PLT-трамплин: exe.plt['puts']. Подставляете значения в payload без ручного разбора objdump или readelf.

Format string и ROP-цепочки

Для задач с Return-Oriented Programming класс ROP принимает список ELF-объектов: rop = ROP([exe]). Он сканирует бинарь на пригодные гаджеты. Для передачи аргумента функции на x64 нужен гаджет pop rdi; ret — метод rop.find_gadget(['pop rdi', 'ret']) находит его адрес.

Предусловия: ROP работает, если NX включён (иначе проще лить шеллкод в стек) и PIE выключен (иначе адреса рандомизированы и нужен leak базы). Проверяйте через checksec до того, как строить цепочку.

Ограничение: pwntools ищет гаджеты линейным сканированием — хватает для стандартных случаев. Для нестандартных гаджетов (фильтрация по типу, сложные цепочки) ROPgadget или Ropper дают расширенный поиск — у Ropper метод searchGadgets(binary, instructionCount=5) позволяет контролировать длину гаджетов и фильтровать по произвольным критериям.

Для format string уязвимостей есть класс FmtStr. Инициализация принимает функцию, которая отправляет форматную строку на сервер и возвращает ответ: fmtstr = FmtStr(execute_fmt, offset=N). find_offset() определяет offset автоматически. write(addr, data) формирует payload для записи произвольных данных по адресу. Это заменяет ручное вычисление %n-записей — занятие, которое на соревновании легко ошибочно и отнимает десятки минут.

Шаблон скрипта для CTF: от локали до remote

Каждый pwn-таск начинается с boilerplate: подключение, настройка контекста, переключение между локальной и удалённой целью. Вместо писать это с нуля — шаблон. Команда pwn template ./binary --host ctf.example.com --port 1337 --quiet генерирует готовый скрипт с аргументами, но он избыточен. Минимальный вариант, покрывающий 90% задач:

from pwn import *
exe = ELF("./vuln", checksec=True)
context.binary = exe
def conn():
    if args.REMOTE:
        return remote("ctf.example.com", 1337)
    return process([exe.path])
p = conn()
# --- payload construction here ---
p.interactive()

python3 exploit.py REMOTE — подключение к серверу. Без аргумента — локальный запуск бинаря. context.binary = exe автоматически настраивает архитектуру, endianness и разрядность — больше не нужно вручную указывать context.arch. Завершающий p.interactive() переключает в интерактивный режим: если эксплойт сработал и вы получили shell, команды вводятся прямо из терминала.

Для отладки замените process([exe.path]) на gdb.debug(exe.path, 'break main\ncontinue') — бинарь запустится под GDB с предзагруженным скриптом. В связке с tmux: context.terminal = ['tmux', 'splitw', '-h'] откроет GDB в соседней панели.

Автоматизация CTF на Python: ошибки, которые стоят флагов

Собранные из реальных соревнований проблемы — каждая стоила от 10 минут до нескольких часов.

Десинхронизация буфера. Сервер отправляет три строки перед запросом ввода, а скрипт вызывает recvline() только дважды. Третья строка остаётся в буфере и ломает следующий recvuntil() — он находит разделитель не там, где ожидалось. Решение: перед критичным приёмом данных — r.clean() для очистки буфера или точный подсчёт строк. И context.log_level = 'debug' — увидите каждый байт в обе стороны.

send() вместо sendline(). send() отправляет данные как есть. sendline() добавляет \n. Если программа на сервере читает через fgets() или input(), ей нужен символ новой строки. Без него программа будет ждать бесконечно, а скрипт — висеть на следующем recv(). Классика.

bytes vs str. Pwntools работает с bytes, не со строками. r.sendline("hello") вместо r.sendline(b"hello") выдаст предупреждение или ошибку. Правило: всё, что уходит в send()/sendline() — в байтах. Всё из recv() — тоже байты, декодируйте через .decode() только когда нужно работать с текстом.

Неверный контекст для cyclic. На x64 cyclic_find() по умолчанию ищет 4-байтовые подпоследовательности, а RIP — 8-байтовый регистр. context.arch = 'amd64' перед использованием cyclic() и cyclic_find(), иначе offset будет неправильный и payload улетит мимо.

eval() за пределами CTF. В примере с математическим challenge-response используется eval(). В CTF это безопасно — вы контролируете источник данных. В реальных скриптах — прямой путь к RCE на собственной машине: сервер может подсунуть __import__('os').system('rm -rf /') вместо арифметики. Для продакшена — ast.literal_eval() или собственный парсер.

Забытый checksec. Начинать pwn-таск без checksec — как ехать по навигатору с завязанными глазами. Canary включён — нужно его утечь. PIE включён — нужен leak базы. NX выключен — можно лить шеллкод в стек. Десять секунд на checksec экономят час на выборе неправильного вектора.

Место скриптов для CTF в цепочке атаки

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

  1. Initial Access — брутфорс форм на requests (Password Guessing, T1110.001), эксплуатация веб-уязвимостей через параметры POST, подключение к уязвимому TCP-сервису через remote().
  2. Exploitation — отправка payload для переполнения буфера, format string атаки через FmtStr, ROP-цепочки через ROP, шеллкод через shellcraft.
  3. Flag Extraction — автоматический парсинг флагов регуляркой re.search(), Automated Collection (T1119) из ответов сервера или файловой системы после получения shell.

Что предшествует скриптингу: разведка — чтение условия, скачивание бинаря, анализ через file и strings, проверка защит через checksec. Что следует после: сдача флага.

В реальном пентесте тот же тулкит применим. По данным IBM X-Force Threat Intelligence Index 2025, атаки с использованием действительных учётных данных выросли на 71% за год — брутфорс и перебор credential'ов остаются рабочим вектором. Разница в том, что в пентесте добавляются WAF, rate limiting, EDR, логирование — которых в CTF обычно нет. Бинарь без canary и PIE — учебный сценарий. Но навык работы с pwntools и понимание механики переполнения переносятся на реальные задачи с минимальной адаптацией: меняются адреса и offset'ы, принципы те же.

И вот тут неудобная правда, которую в CTF-комьюнити обычно обходят стороной: скрипты решают примерно 20% задачи. Остальные 80% — понимание того, что происходит внутри бинаря, веб-приложения или протокола. Копирование шаблонов из чужих writeup'ов без понимания, зачем p64() упаковывает адрес в little-endian и почему recvuntil() застрял на конкретном байте, приводит к одному: скрипт работает на одном таске и разваливается на следующем, потому что автор не понимает, что поменялось. На каждом крупном CTF это видно: участник берёт чужой exploit.py, меняет адрес win-функции, payload не срабатывает — и человек теряется. А проблема может быть в выравнивании стека, во включённом PIE, в изменённой версии libc. Смотреть нужно не в Python-скрипт, а в GDB — на стек, на регистры, на то, как данные ложатся в память. Python-тулкит из requests и pwntools — множитель скорости, а не замена понимания. Если множить ноль, результат предсказуем. Построчный разбор каждого скрипта из этой статьи полезнее, чем бездумное копирование двадцати шаблонов из GitHub. Каждый recvuntil() — точка синхронизации с программой, каждый p64() — конкретное представление числа в памяти конкретной архитектуры. Когда эти вещи становятся рефлексами, CTF-задачи решаются за минуты. Если хочешь не просто writeup, а пройти всю атаку самому от разведки до shell — на WAPT эту цепочку проходят в течение двух модулей с лабами.

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

Поделиться

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

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

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