
На NahamConCTF задача Flaskmetal Alchemist — time-based SQL injection через SQLAlchemy — сожрала три часа на подбор рабочего payload. А сам скрипт-эксплойт? 15 строк: requests.post() в цикле, замер elapsed.total_seconds(), посимвольное извлечение флага. Пропорция типичная: 80% времени уходит на «а что тут вообще происходит», и 20% — на код. Python закрывает три направления: автоматизацию HTTP-взаимодействия, низкоуровневую работу с байтами и решение типовых crypto-задач. Ниже — готовые шаблоны, которые можно скопировать, адаптировать под конкретный таск и складывать в свой репозиторий. Через пару соревнований этот репозиторий начнёт экономить часы.
Перед первым скриптом убедитесь, что всё на месте:
python3 --versionpip install requests pwntools pycryptodomerequests — HTTP-клиент с автоматическим управлением cookies и сессиями. pwntools (репозиторий gallopsled/pwntools на GitHub, активно поддерживается) — фреймворк для binary exploitation и TCP-взаимодействия. pycryptodome — криптографические примитивы: RSA, AES, DES и десятки других алгоритмов. Для задач с base64, hashlib, struct и binascii ничего ставить не нужно — всё в стандартной библиотеке Python.
Работаете на Windows без WSL и не хотите тянуть pwntools? Для TCP-задач подойдёт стандартный socket — возможностей меньше, зато зависимостей ноль.
Половина ошибок в первых CTF-скриптах — путаница между str и bytes. Python 3 жёстко разделяет текстовые строки (str, Unicode) и сырые байтовые данные (bytes). Попытка написать b"hello" + "world" выбросит TypeError. Это не баг — это защита от вас самих. В CTF данные почти всегда приходят в байтовом виде, и смешивать типы — верный путь к часу отладки на ровном месте.
Преобразования, которые нужны на каждом соревновании:
# hex → bytes → строка
flag_hex = "4354467b68656c6c6f7d"
flag_bytes = bytes.fromhex(flag_hex) # b'CTF{hello}'
flag_str = flag_bytes.decode('utf-8') # 'CTF{hello}'
# строка → bytes → hex → int
data = b'CTF{hello}'
as_hex = data.hex() # '4354467b68656c6c6f7d'
as_int = int.from_bytes(data[:3], 'big') # 4412742
back = as_int.to_bytes(3, 'big') # b'CTF'
Разберём по строкам. bytes.fromhex() принимает hex-строку без префикса 0x и возвращает байтовый объект. .decode('utf-8') превращает байты в читаемый текст — но только если данные действительно текстовые. Если в байтах бинарный мусор, .decode() упадёт с UnicodeDecodeError. Тогда берите .decode('latin-1') — этот кодек проглотит любой байт от 0x00 до 0xFF, маппя каждый на символ.
int.from_bytes() незаменим в crypto-задачах, где числа хранятся как байтовые последовательности. Второй аргумент — 'big' или 'little' — задаёт порядок байтов: big-endian — старший байт первый, little-endian — младший. В RSA-задачах на CTF почти всегда big-endian.
Для работы с бинарными форматами (сетевые пакеты, файловые заголовки) есть модуль struct. Вызов struct.unpack('>I', data[:4]) распакует первые 4 байта как unsigned int в big-endian формате. Символ > — big-endian, I — unsigned int (4 байта). Минимальная таблица форматов: B — 1 байт, H — 2 байта, I — 4 байта, Q — 8 байт. Для большинства CTF-форматов этого за глаза.
binascii дополняет картину: binascii.unhexlify(b'4354467b7d') возвращает b'CTF{}'. Разница с bytes.fromhex() — binascii-версия принимает и отдаёт bytes, что удобнее в цепочках, где всё остаётся в байтовом виде.
Для быстрой проверки гипотезы прямо в терминале: python3 -c "print(bytes.fromhex('4354467b7d'))" — без файла, без IDE, результат за секунду.
Большинство web-задач на CTF сводятся к одному из трёх сценариев: перебор параметров (brute-force PIN-кода, имени файла, значения cookie), отправка вредоносного payload (SQLi, SSTI, XXE) или анализ поведения сервера (timing attack, различия в длине ответов). Во всех трёх случаях базовый инструмент — requests.Session().
Зачем Session(), а не голый requests.get()? Сессия сохраняет cookies между запросами. В CTF это критично: часто нужно сначала залогиниться, получить session cookie, а потом слать запросы уже с ней. Без сессии придётся руками протаскивать cookies через каждый вызов — и это та ещё возня.
import requests
s = requests.Session()
url = "http://target.ctf:8080/login"
for pin in range(10000):
r = s.post(url, data={"pin": f"{pin:04d}"})
if "Invalid" not in r.text:
print(f"[+] PIN: {pin:04d}")
print(r.text[:200])
break
Скрипт перебирает четырёхзначные PIN-коды, отправляя POST на каждой итерации. Условие остановки — отсутствие слова "Invalid" в ответе. Форматирование f"{pin:04d}" добавляет ведущие нули: 0001, 0042, 0999. Срез .text[:200] выводит начало ответа — обычно там и лежит флаг.
Что менять под конкретную задачу:
data={"pin": ...} на нужные поля формы. Если сервер ждёт JSON — вместо data= пишите json={"key": "val"}, тогда requests сам выставит Content-Type: application/json"Invalid" not in r.text может быть r.status_code != 403, "flag" in r.text.lower() или len(r.text) > 100s.headers.update({"Authorization": "Bearer token123"}) добавит заголовок ко всем последующим запросам сессииДля задач с time-based SQLi (как в Flaskmetal Alchemist, описанной в writeup'е shellbr3ak) ключевое отличие — проверяем не содержимое ответа, а время. Автор того writeup'а использовал randomblob() в SQLite для создания искусственной задержки: если условие истинно, сервер генерирует 500 миллионов случайных байт, что занимает несколько секунд. Проверка в скрипте: if int(r.elapsed.total_seconds()) >= 3 — длинный ответ означает «символ угадан». По этой схеме флаг вытаскивается посимвольно.
Такие задачи — прямое отражение техники Exploit Public-Facing Application (T1190, Initial Access) по MITRE ATT&CK. Перебор параметров через автоматизированные HTTP-запросы — Password Cracking (T1110.002, Credential Access). Понимание этих паттернов на CTF даёт контекст для работы с реальными уязвимостями веб-приложений, тем, что OWASP относит к категории A03:2021 Injection.
Типовые грабли новичков при автоматизации CTF web-задач:
requests.post(data=...) шлёт application/x-www-form-urlencoded. Используйте json= вместо data=time.sleep(0.1) между итерациямиrequests.get(url, verify=False) отключает проверку. В CTF это нормальноr.text содержит мусор, попробуйте r.content (сырые байты) и декодируйте вручнуюWeb-скрипт — обычно не первый шаг. Типичная последовательность:
robots.txt, заглянуть в DevTools (cookies, заголовки, скрытые формы)Скрипт появляется на шаге 4, когда гипотеза уже подтверждена. Автоматизировать с нуля, не понимая что именно автоматизируешь — пустая трата времени. Я видел, как люди запускают sqlmap на каждую форму подряд, не заглянув в исходник страницы. Не надо так.
Когда задача выглядит как nc challenge.ctf 1337 — это TCP-сервис, не HTTP. Тут requests бесполезен. Нужен pwntools, заточенный под binary exploitation и сетевое взаимодействие.
Базовый паттерн: from pwn import * импортирует всё. r = remote('challenge.ctf', 1337) устанавливает TCP-соединение. r.recvline() читает строку от сервера, r.sendline(b'answer') отправляет ответ с переводом строки. Когда нужно прочитать «до конкретного маркера» — r.recvuntil(b': ') остановится на двоеточии с пробелом.
Для PPC-задач (Professional Programming and Coding — задачи, где нужно автоматизировать ответы на вопросы сервера) типичный цикл: получить строку, распарсить задание (математическое выражение, перевод из одной системы счисления в другую, реверс строки), отправить ответ. После N успешных раундов сервер выдаёт флаг. Завершающий вызов r.interactive() переключает в ручной режим — видите всё, что сервер шлёт после автоматической части.
Когда pwntools не нужен: если в задаче URL — берите requests. Если нестандартный протокол и нельзя поставить pwntools (ограниченная среда) — стандартный socket из stdlib.
| Инструмент | Преимущества | Ограничения | Когда использовать | Когда не использовать |
|---|---|---|---|---|
| requests | Cookies, редиректы, сессии — автоматически | Только HTTP/HTTPS | Web: SQLi, brute-force, SSRF | Binary, raw TCP |
| pwntools | TCP/UDP, send/recv, GDB-интеграция | Тяжёлый (~50 МБ), Windows — частично | Pwn, PPC, TCP-сервисы | Простые HTTP-задачи |
| socket (stdlib) | Ноль зависимостей, полный контроль | Ручная обработка буферов и таймаутов | Нестандартные протоколы, минимальная среда | Когда есть requests или pwntools |
Категория crypto на CTF для начинающих — в основном три типа задач: однобайтовый XOR-шифр, многослойное кодирование и задачи на хеши. Python покрывает все три стандартной библиотекой плюс PyCryptodome для продвинутых алгоритмов.
XOR-шифрование с одним ключевым байтом — классика CTF начального уровня. Каждый байт открытого текста XOR'ится с одним и тем же значением ключа (от 0 до 255). Для расшифровки достаточно перебрать все 256 вариантов — это мгновенно.
import base64
ciphertext = base64.b64decode("GhkWFRYMChcaFRYMFgod")
for key in range(256):
plaintext = bytes([b ^ key for b in ciphertext])
try:
text = plaintext.decode('ascii')
if text.isprintable():
print(f"Key=0x{key:02x}: {text}")
except UnicodeDecodeError:
continue
Что здесь происходит: bytes([b ^ key for b in ciphertext]) создаёт новый байтовый объект, XOR'я каждый байт шифротекста с кандидатом-ключом. .decode('ascii') пытается интерпретировать результат — если хотя бы один байт вне диапазона 0-127, исключение перехватывается и скрипт идёт дальше. .isprintable() дополнительно отсеивает управляющие символы.
Если формат флага известен (например, CTF{...}), фильтрация ещё проще: if plaintext.startswith(b'CTF{'). Для многобайтовых XOR-ключей (repeating-key XOR) подход другой: шифротекст разбивается на N подпоследовательностей по длине ключа, каждая расшифровывается как однобайтовый XOR. Длину ключа можно прикинуть через индекс совпадений или расстояние Хэмминга между блоками шифротекста.
На CTF часто встречается цепочка кодирований — по типу матрёшки: данные обёрнуты base64, внутри gzip-архив, а внутри XOR-шифрованный текст. Подход всегда одинаковый: снимаем слои один за другим.
Стандартная библиотека Python закрывает это полностью. base64.b64decode() снимает base64, gzip.decompress() распаковывает gzip, XOR-расшифровка — однострочник из предыдущего раздела. Сигнатура помогает определить следующий слой: если base64-данные начинаются с H4sIA — это gzip внутри base64 (байты \x1f\x8b\x08 в base64 кодируются именно так). Видите после декодирования ASCII-символы из набора A-Za-z0-9+/=? Скорее всего, ещё один слой base64.
Такие задачи — отражение реальной техники Deobfuscate/Decode Files or Information (T1140, Defense Evasion) по MITRE ATT&CK. Вредоносные payload'ы тоже прячутся за слоями кодирования — Standard Encoding (T1132.001). Навык быстрого снятия слоёв пригодится и в CTF, и в incident response.
Задачи вида «найдите строку, MD5 которой начинается с 00000» решаются тупым перебором: hashlib.md5(f"prefix{i}".encode()).hexdigest() в цикле. Модуль hashlib поддерживает MD5, SHA-1, SHA-256 и десятки других алгоритмов из коробки.
Когда стандартной библиотеки мало — RSA с конкретными параметрами, AES-CBC padding oracle, генерация ключей — подключается PyCryptodome: from Crypto.PublicKey import RSA для работы с RSA-ключами, from Crypto.Cipher import AES для блочных шифров. Но это уже уровень задач medium-hard, где одним шаблоном не обойтись — нужно понимать математику конкретного алгоритма.
Перед написанием скрипта определите тип задачи. Decision tree:
| Условие | Инструмент | Шаблон |
|---|---|---|
| В задаче URL или HTTP-endpoint | requests.Session() | Brute-force / payload injection |
В задаче nc host port |
pwntools remote() | PPC / binary interaction |
| Дан файл с закодированными данными | base64 + gzip + binascii | Многослойное декодирование |
| Дан шифротекст, предположительно XOR | bytes + XOR brute | Однобайтовый или repeating-key XOR |
| Нужно найти прообраз хеша | hashlib в цикле | Hash brute-force |
| Дан бинарный файл для разбора | struct.unpack() | Парсинг заголовков и полей |
| Даны n, e, c (RSA-параметры) | PyCryptodome + math | RSA-атаки (factorization, small e) |
Если задача не попадает ни в одну категорию — начните с python3 -c "..." в терминале. Однострочник часто заменяет полноценный скрипт на этапе проверки гипотезы.
Про организацию: заведите папку ctf-templates/ и после каждого CTF складывайте туда рабочие скрипты, очищенные от конкретных URL и значений. Через два-три соревнования у вас будет набор, покрывающий 60-70% задач без написания кода с нуля.
Шаблоны из этой статьи покрывают типовые CTF-задачи начального и среднего уровня. Где они не работают:
ROP(), p64(), shellcraft. Отдельная дисциплина с порогом входа выше, чем web или cryptotry/except с повторными попытками и time.sleep() между запросами, иначе скрипт ляжет на сетевом таймаутеГлавное ограничение — скрипты не заменяют понимание уязвимости. Если не понимаете, почему time-based SQLi работает или что делает XOR на уровне битов, шаблон не спасёт. Автоматизация ускоряет эксплуатацию, но не заменяет анализ. Сначала разберитесь в механике вручную, потом автоматизируйте.
Навык, который отличает CTF-игрока с опытом от новичка — не знание конкретных эксплойтов и не скорость набора кода. Это умение за тридцать секунд определить, к какому классу относится задача, и достать из своего репозитория нужный шаблон. На одном из CTF видел, как команда из двух человек обошла команду из восьми — у них был отлаженный набор из десяти скриптов-заготовок, а вторая команда каждый раз открывала пустой файл.
Проблема большинства «python для CTF» туториалов — они показывают решение конкретной задачи, а не переиспользуемый паттерн. Человек прочитал writeup, повторил код, решил ровно ту же задачу — и через неделю не может адаптировать скрипт под похожую, но чуть другую. Шаблоны в этой статье намеренно минимальны: чем меньше специфики конкретного CTF, тем проще переиспользовать. Начните с папки шаблонов и привычки чистить скрипты после каждого соревнования — убирать хардкод URL, заменять конкретные значения на переменные. Через три-четыре CTF соберётся инструментарий, который реально экономит время. Если вы в самом начале пути и хочется выстроить фундамент системно — на курсе IB Basics в Codeby School эту базу дают от сетей до первых практических задач.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...