Сканер директорий на Python за 40 строк: от первого GET-запроса до многопоточного перебора

На первом CTF я запустил dirb http://target.local и наблюдал, как по экрану бегут строки вывода. Нашёл /admin, /backup, даже /phpmyadmin — сдал флаг. Но когда в чате спросили «а что именно dirb делает на уровне HTTP?», дальше «ну, перебирает» объяснить не смог. Через неделю написал свой сканер директорий на Python — 40 строк, библиотека requests, немного concurrent.futures для скорости. После этого вывод dirb перестал быть магией, а HTTP-коды стали читаться как нормальный текст. Эта статья — тот самый путь: от одного GET-запроса до рабочего многопоточного инструмента. Без зубрёжки документации.
Зачем писать свой сканер директорий на Python
Перебор директорий (directory bruteforce) — автоматическая отправка HTTP-запросов к веб-серверу с подстановкой имён из заранее подготовленного списка слов. Такой список называется wordlist — текстовый файл, где каждая строка содержит потенциальное имя: admin, backup, config.php, .git. Цель — обнаружить скрытые страницы, панели администрирования, резервные копии и конфигурационные файлы, которые не видны в интерфейсе сайта, но доступны по прямой ссылке.
В терминологии фреймворка MITRE ATT&CK (открытая база тактик и техник, описывающая поведение атакующих; T-коды вроде T1595 — её идентификаторы) перебор директорий относится к фазе разведки — Reconnaissance. Атакующий прощупывает поверхность цели до начала эксплуатации. Бизнес-логика простая: найти забытую резервную копию базы данных или открытый .git-каталог — это часто готовые учётные данные без единого эксплойта.
Для перебора есть готовые инструменты: dirb, gobuster, ffuf, dirsearch. Все делают одно — отправляют запросы, читают коды ответа, фильтруют результат. И делают это быстро.
Зачем тогда самописный сканер? Три причины.
Понимание механики. Пока не напишешь requests.get(url) руками и не посмотришь, что приходит в response.status_code, HTTP остаётся абстракцией. Свой скрипт — это разобранный двигатель на столе: видна каждая деталь.
Гибкость под задачу. На реальном пентесте бывают нестандартные ситуации: сервер возвращает 200 на любой путь, нужно парсить тело ответа, добавить кастомный заголовок. В готовом инструменте это поиск нужного флага в man-странице. В своём скрипте — одна строка.
Навык автоматизации. Python для пентестера — не «знать синтаксис», а уметь быстро собрать инструмент под задачу. Сканер директорий — идеальный первый проект: достаточно простой для 40 строк, достаточно практичный для реальной работы.
Что делает requests Python при переборе директорий
Прежде чем писать код, разберём механику. Без понимания HTTP-кодов результаты сканирования превращаются в бессмысленный список URL.
Сканер берёт базовый URL (например, http://target.local) и wordlist. Для каждого слова склеивает полный адрес — http://target.local/admin — и отправляет GET-запрос. Сервер отвечает HTTP-кодом статуса. Какие коды важны:
| Код | Значение | Что это значит для сканера |
|---|---|---|
| 200 | OK | Ресурс существует и доступен — находка |
| 301/302 | Redirect | Перенаправление — директория скорее всего есть |
| 403 | Forbidden | Доступ запрещён — ресурс существует, но закрыт |
| 404 | Not Found | Ресурса нет — сканер пропускает |
| 500 | Server Error | Сервер упал на этом пути — может указывать на уязвимость |
Wordlist определяет половину результата. Короткий список из 100 слов пропустит редкие пути, огромный из 2 миллионов будет сканировать часами. Для старта хватит стандартного common.txt из набора SecLists (проект Daniel Miessler на GitHub) — около 4700 строк, покрывает типичные имена директорий и файлов на веб-серверах. В Kali Linux этот файл обычно лежит по пути /usr/share/wordlists/dirb/common.txt. Если работаешь не в Kali — скачай SecLists с GitHub и используй файл Discovery/Web-Content/common.txt.
Нюанс про качество wordlist: универсальные списки хороши для первичной разведки, но на реальном пентесте часто эффективнее собрать целевой wordlist — из исходников приложения, из JavaScript-файлов фронтенда, из robots.txt. Это уже следующий уровень, а для первого скрипта common.txt — то что нужно.
Пишем базовый скрипт перебора директорий на Python
Предпосылки: Python 3.6+ и установленная библиотека requests. Если requests ещё нет — в терминале выполни pip install requests. Проверить установку можно командой python3 -c "import requests; print(requests.__version__)" — должна вывести номер версии без ошибок.
Минимальный рабочий скрипт:
import requests, sys
url = sys.argv[1]
wordlist = open(sys.argv[2]).read().splitlines()
for path in wordlist:
target = f"{url.rstrip('/')}/{path}"
try:
r = requests.get(target, timeout=5)
if r.status_code != 404:
print(f"[{r.status_code}] {target}")
except requests.RequestException: pass
Что здесь происходит:
sys.argv[1]иsys.argv[2]— аргументы командной строки: URL цели и путь к файлу wordlist. При запускеpython3 scanner.py http://target.local wordlist.txtPython подставит их автоматически.open(...).read().splitlines()— читает wordlist в список строк. Каждая строка становится отдельным элементом списка.f"{url.rstrip('/')}/{path}"— склеивает URL и слово. Методrstrip('/')убирает лишний слеш, чтобы не получилосьhttp://target.local//admin.requests.get(target, timeout=5)— отправляет GET-запрос с таймаутом 5 секунд. Без таймаута скрипт может зависнуть на недоступном хосте.if r.status_code != 404— выводит всё, что не «не найдено». Грубый фильтр, но для начала работает.except requests.RequestException: pass— молча пропускает ошибки соединения: таймаут, разрыв связи, DNS-ошибку.
Запуск: python3 scanner.py http://target.local /usr/share/wordlists/dirb/common.txt
Ожидаемый вывод: строки вида [200] http://target.local/index.html и [403] http://target.local/.htaccess. Если вывод пустой — проверь URL в браузере и убедись, что wordlist не пуст (открой его текстовым редактором).
Скрипт работает. Но он медленный. Каждый запрос ждёт ответа, прежде чем отправить следующий. На wordlist из 4700 строк это минуты ожидания — один запрос за раз, последовательно. Терпение закончится раньше, чем wordlist.
Автоматизация пентеста: многопоточность в Python-сканере
Многопоточность (multithreading) — запуск нескольких задач параллельно внутри одного процесса. В Python есть ограничение под названием GIL (Global Interpreter Lock — глобальная блокировка интерпретатора): из-за него потоки не ускоряют вычисления на процессоре. Но для сетевых запросов GIL не помеха — пока один поток ждёт ответа от сервера, другой уже отправил свой запрос. Такие задачи называются I/O-bound: основное время тратится на ожидание ввода-вывода, а не на вычисления. Как сказано в официальной документации Python по модулю threading: «threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously».
На пальцах: ты стоишь в одной очереди и ждёшь, пока кассир обслужит предыдущего клиента. Многопоточность — это когда ты одновременно стоишь в 20 очередях. Каждая движется независимо.
Самый удобный способ добавить потоки — ThreadPoolExecutor из стандартного модуля concurrent.futures (входит в Python, ничего устанавливать не нужно). Вместо ручного создания потоков задаёшь пул воркеров и передаёшь задачи. Функция check(path) содержит ту же логику из базовой версии, но оформлена как функция с return вместо print:
from concurrent.futures import ThreadPoolExecutor, as_completed
# check(path) — та же логика: requests.get + фильтр по коду
with ThreadPoolExecutor(max_workers=20) as pool:
futures = [pool.submit(check, path) for path in wordlist]
for f in as_completed(futures):
result = f.result()
if result: print(result)
Что здесь происходит:
ThreadPoolExecutor(max_workers=20)— создаёт пул из 20 потоков. Одновременно летит до 20 запросов. Число 20 — разумный старт; на быстром сервере можно поднять до 50, на медленном или защищённом WAF (Web Application Firewall) — опустить до 10.pool.submit(check, path)— отправляет задачу в пул. Возвращает объектFuture— обещание результата, который будет доступен позже.as_completed(futures)— отдаёт результаты по мере готовности, не дожидаясь завершения всех задач. В блоге yasoob.me описан аналогичный подход:ThreadPoolExecutorс 10 воркерами дал ускорение примерно в 9 раз на 14 URL по сравнению с последовательным перебором.
На практике с wordlist из 4700 строк и 20 потоками разница ощутимая: вместо минут — секунды. Точное ускорение зависит от скорости ответа сервера, но порядок — от 5 до 20 раз быстрее базовой версии. Разница между «пойду чай заварю» и «уже готово».
Dirb на Python своими руками: доработки и сравнение
Скрипт из предыдущих разделов — рабочий, но учебный. Вот что стоит добавить, чтобы довести его до полноценного инструмента (все доработки укладываются в те самые 40 строк, если писать компактно):
User-Agent. По умолчанию requests отправляет заголовок User-Agent: python-requests/2.x.x. Любой WAF мгновенно вычислит автоматический скрипт. Добавь параметр headers={"User-Agent": "Mozilla/5.0 ..."} в вызов requests.get() — не гарантия скрытности, но шанс моментальной блокировки снижается.
Фильтр soft 404. Некоторые серверы возвращают 200 на любой путь, но с одинаковым телом — это soft 404 (мягкая ошибка). Код 200 есть, а реального контента нет. Решение: перед сканированием отправь запрос на заведомо несуществующий путь (например, /asdqwezxc123), запомни размер ответа через len(r.content) и в основном цикле пропускай результаты с таким же размером. Именно на soft 404 ломаются новички, которые привыкли доверять готовым инструментам и ни разу не заглядывали в response.text.
Расширения файлов. Базовый wordlist содержит имена без расширений. Для полного покрытия к каждому слову стоит добавлять .php, .bak, .old, .txt, .conf. Одна строка: paths = [f"{w}{ext}" for w in wordlist for ext in ["", ".php", ".bak"]].
HTTPS и сертификаты. При сканировании HTTPS-целей с самоподписанным сертификатом requests выбросит ошибку. Для тестовых стендов можно добавить verify=False, но на реальных целях лучше разобраться с сертификатом — иначе рискуешь пропустить MITM.
Как самописный скрипт соотносится с инструментами для fuzzing директорий:
| Критерий | Наш скрипт | dirb | gobuster | ffuf |
|---|---|---|---|---|
| Язык | Python | C | Go | Go |
| Скорость | Средняя | Средняя | Высокая | Очень высокая |
| Кастомная логика | Полная | Нет | Нет | Частично |
| Рекурсия | Нужно дописать | Нативно | Нет | Нет |
| Фильтры | Любые | По коду | По коду, размеру | По коду, размеру, regex |
| Когда НЕ подходит | Большие wordlist (2M+ строк) | Нестандартные ответы | Кастомная логика парсинга | Сложная фильтрация по телу |
| Порог входа | pip install requests | Есть в Kali | Один бинарник | Один бинарник |
Для скорости и рутинного web reconnaissance — ffuf или gobuster. Для нестандартных задач и обучения Python ИБ — свой код. В реальной работе пентестер использует и то, и другое: ffuf для массового перебора, свой скрипт — когда ffuf не справляется с кастомной логикой ответов.
Делай раз, делай два, делай три: запускаем скрипт для пентеста
Пошаговая инструкция для тех, кто хочет повторить прямо сейчас.
Предпосылки: Python 3.6+, установленный requests (pip install requests), файл wordlist (скачай SecLists с GitHub или используй встроенный в Kali). Тестовая цель — локальный DVWA, Juice Shop или CTF-стенд. Сканировать чужие серверы без письменного разрешения — статья 272 УК РФ.
Шаг 1. Создай файл scanner.py. Скопируй базовую версию из раздела выше, добавь строку from concurrent.futures import ThreadPoolExecutor, as_completed в начало, оберни блок try/except в функцию def check(path): с return вместо print, замени цикл for на блок с ThreadPoolExecutor. Сохрани.
Как понять, что получилось: открой файл и убедись, что есть import requests, функция check, блок with ThreadPoolExecutor. Запусти python3 scanner.py без аргументов — Python должен выдать IndexError: list index out of range (ему не хватает URL и wordlist в аргументах). Если вместо этого SyntaxError — проверь отступы.
Шаг 2. Запусти: python3 scanner.py http://твоя-цель /путь/к/wordlist.txt. Через несколько секунд в терминале появятся строки вида [200] http://цель/index.html, [403] http://цель/.htaccess, [301] http://цель/images.
Как понять, что работает: строки с кодами 200, 301, 403 — это находки. Если вывод пустой — проверь URL (должен открываться в браузере) и wordlist (должен содержать строки, по одной на строку).
Шаг 3. Запусти dirb http://твоя-цель /путь/к/wordlist.txt на том же стенде с тем же wordlist. Сравни результаты. Найденные пути должны в основном совпадать. Расхождения — повод разобраться: возможно, разница в обработке редиректов или таймаутов. Это и есть обучение.
Результат: ты видишь те же директории, что и профессиональный инструмент, но понимаешь каждую строку кода за результатом. Следующий шаг — модифицируй: добавь фильтр по размеру ответа, смени User-Agent, поставь max_workers=50 и замерь разницу в скорости.
Написать свой скрипт перебора директорий — не попытка заменить gobuster. Это способ перестать быть оператором чужих кнопок.
Я наблюдаю один и тот же паттерн: человек стабильно находит стандартные вещи готовыми инструментами, а потом натыкается на сервер с soft 404 или кастомным WAF — и впадает в ступор. За два года работы ни разу не заглянул в response.text. Инструмент думал за него. Путь «выучу все флаги ffuf» работает ровно до первого нестандартного сервера. Свой код работает всегда — потому что ты контролируешь каждую строку и можешь адаптироваться на ходу. Разница между «знаю как запустить» и «понимаю что происходит» — это разница между оператором и специалистом. Если этот подход откликается и хочется пройти базу ИБ системно, а не по одной статье за раз — IB Basics именно про «как разобраться», не про «запомните терминологию».
Эту тему и смежные навыки разбирают на практике в курсе «Python для пентестера» Codeby Academy.
