Перейти к содержимому

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

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

На первом 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.txt Python подставит их автоматически.
  • 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.