Скрипт для поиска IDOR уязвимостей на Python: автоматизация перебора параметров

CVE-2025-13526 — IDOR в WordPress-плагине OneClick Chat to Order, все версии до 1.0.8 включительно. Порядковый order_id в URL, ноль проверок авторизации, CVSS 7.5 (HIGH). Для массовой выгрузки данных покупателей — имена, email, телефоны, адреса, содержимое заказов — хватило бы скрипта на Python в 15 строк. Таких уязвимостей десятки в каждом bug bounty scope, и ни один DAST-сканер их не ловит: IDOR — логическая ошибка, а не техническая. Здесь — рабочий подход к автоматизации поиска IDOR через перебор параметров запроса на Python: от минимального скрипта до анализа ответов и границ, за которыми метод перестаёт работать.
Что такое IDOR и зачем автоматизировать поиск
IDOR (Insecure Direct Object Reference — небезопасная прямая ссылка на объект) — уязвимость, при которой приложение принимает идентификатор от пользователя и отдаёт объект из базы без проверки прав доступа. Запрос GET /api/documents/1234 возвращает документ. Замена ID на 1235 — чужой документ. Сервер не спрашивает «а тебе можно?».
В OWASP API Security Top 10 (открытый рейтинг угроз для API — если ещё не видели, стоит пробежаться хотя бы по заголовкам) эта проблема называется BOLA — Broken Object Level Authorization, нарушенная авторизация на уровне объекта. Первая позиция рейтинга (API1:2023). В классическом OWASP Top 10 для веб-приложений IDOR входит в категорию A01:2021 Broken Access Control — по данным OWASP, 94% протестированных приложений имели ту или иную форму нарушения контроля доступа.
Технически IDOR описывается как CWE-639 (Authorization Bypass Through User-Controlled Key): система не мешает одному пользователю получить данные другого через подмену идентификатора. Если говорить языком MITRE ATT&CK (открытая база тактик и техник атак; T-коды — её идентификаторы), эксплуатация IDOR соотносится с техникой Exploit Public-Facing Application (T1190, тактика Initial Access), а массовый сбор данных через подмену ID — с Data from Information Repositories (T1213, тактика Collection).
Почему DAST-сканеры (автоматические сканеры вроде OWASP ZAP или Acunetix) IDOR не находят? Они заточены под технические ошибки — SQL-инъекции, XSS — отправляют вредоносные payload и анализируют ответ. IDOR — логический баг. Чтобы его обнаружить, нужно сравнить: «этот ответ содержит данные другого пользователя, а не мои». Сканер не знает, кому принадлежит объект с id=124, потому что ему не с чем сравнивать. Python-скрипт с правильной логикой сравнения ответов справляется лучше — вы сами задаёте критерии: чей email искать, какую длину ответа считать подозрительной.
Требования к окружению
Перед написанием и запуском скрипта подготовьте рабочее место. Без этих компонентов код не заработает или вы рискуете нарушить закон.
- Python 3.10+ — любая ОС (Linux, macOS, Windows). Проверка:
python3 --version. - Библиотека requests — HTTP-клиент для Python, повсеместно используемый для автоматизации пентеста на Python. Установка:
pip install requests. - Целевое приложение — тестируйте только на учебных стендах или в рамках bug bounty с явным разрешением. Для практики: OWASP Juice Shop (запуск через Docker:
docker run -p 3000:3000 bkimminich/juice-shop, минимум 2 ГБ RAM) или DVWA. - Два тестовых аккаунта — Attacker и Victim. Зарегистрируйте оба в приложении, от имени Victim создайте объекты: заказы, файлы, записи в профиле. Это сгенерирует разные ID для тестирования.
- Токен авторизации — скопируйте из браузера: DevTools (F12) → вкладка Network → любой запрос к API → заголовок
Authorizationили значение cookiesession.
[Применимо: внешний пентест, внутренний пентест, bug bounty. Работает на legacy и modern приложениях с порядковыми числовыми ID.]
Перебор параметров запроса: базовый Python-скрипт для поиска IDOR
Идея простая: отправляем HTTP-запросы от имени пользователя A (Attacker) с ID объектов пользователя B (Victim) и смотрим, что возвращает сервер. Если в ответе — данные Victim, а не ошибка 403/404, перед нами IDOR.
Работает если: приложение использует порядковые числовые ID (user_id=1, user_id=2…); авторизация передаётся через заголовок или cookie; сервер возвращает данные в теле ответа.
Не работает если: идентификаторы — UUID v4 (128 бит случайных данных, перебор невозможен); сервер проверяет принадлежность объекта текущему пользователю; WAF блокирует последовательные запросы.
import requests
base_url = "https://target.com/api/users/{}"
headers = {"Authorization": "Bearer TOKEN_ATTACKER"}
baseline_len = len(requests.get(base_url.format(1), headers=headers).text)
for uid in range(2, 100):
r = requests.get(base_url.format(uid), headers=headers)
if r.status_code == 200 and abs(len(r.text) - baseline_len) > 50:
print(f"[IDOR?] uid={uid} status={r.status_code} len={len(r.text)}")
Что происходит на каждом шаге:
import requests— подключает HTTP-библиотеку.base_url— шаблон URL с{}на месте ID. Подставьте реальный эндпоинт целевого приложения.headers— токен авторизации Attacker. Именно от его имени идут все запросы. Скопируйте из DevTools браузера: вкладка Network → заголовок Authorization.baseline_len— длина ответа на запрос с вашим собственным ID. Точка отсчёта: если ответ с чужим ID существенно отличается по длине — сигнал.- Цикл
range(2, 100)— перебирает ID от 2 до 99. Диапазон подбирайте под конкретное приложение. requests.get(...)— отправляет GET-запрос с подставленным ID.- Условие: статус 200 (данные отданы) И длина ответа отличается от baseline более чем на 50 символов. Порог 50 — защита от ложных срабатываний из-за CSRF-токенов и временных меток, которые меняются между запросами.
print(...)— выводит ID-кандидаты в консоль.
Ожидаемый вывод (если IDOR присутствует): [IDOR?] uid=3 status=200 len=847. Пусто в консоли — сервер либо проверяет авторизацию (все ответы 403), либо отдаёт одинаковое тело независимо от ID. Второй случай требует анализа содержимого, а не длины.
Анализ ответов — как отличить настоящий IDOR от шума
Сравнение длины ответа — грубый первичный фильтр. На практике ложные срабатывания встречаются регулярно:
- Сервер вернул 200 с
{"error": "access denied"}в теле — длина похожа на реальный ответ, но данных нет. - Сервер при любом ID возвращает ваш собственный профиль — длина совпадает, IDOR нет, проверка авторизации подменяет запрошенный ID на ваш.
- Сервер отдаёт пустой
{}для несуществующих ID — короткий ответ, тоже не уязвимость.
Более надёжный подход — искать конкретные маркеры Victim в теле ответа: email, телефон, имя. Вы знаете эти данные, потому что сами создали аккаунт Victim при подготовке окружения.
import requests
def scan_idor(url_tpl, headers, id_range, victim_markers):
for uid in id_range:
r = requests.get(url_tpl.format(uid), headers=headers)
if r.status_code == 200:
body = r.text.lower()
found = [m for m in victim_markers if m.lower() in body]
if found:
print(f"[+] IDOR uid={uid}: найдено {found}")
Вызов: scan_idor(base_url, headers, range(1, 200), ["victim@mail.com", "+79001234567"]). Если скрипт выведет [+] IDOR uid=42: найдено ['victim@mail.com'] — вы подтвердили: от имени Attacker читаются данные Victim. Верифицированный IDOR, готовый для включения в репорт.
Три уровня достоверности результата:
| Сигнал | Достоверность | Действие |
|---|---|---|
| status 200, длина отличается | Низкая | Проверить тело вручную в Burp Repeater |
| status 200, маркеры Victim в теле | Высокая | Зафиксировать, повторить дважды |
| status 200, полный JSON-профиль Victim | Подтверждённый IDOR | Документировать со скриншотами |
Полезный приём: перед основным циклом отправьте запрос с заведомо несуществующим ID (например, 99999999) и сохраните status_code и len(text) как «шаблон отсутствия». Если ответ на реальный uid=42 отличается от этого шаблона — объект существует и вы получили к нему доступ.
Фаззинг POST-запросов и JSON-параметров API на Python
GET-параметры в URL — самый очевидный вектор. Но идентификаторы часто прячутся в теле POST/PUT/PATCH-запросов. По практике bug bounty, IDOR встречается в REST API, GET-параметрах и теле POST-запросов примерно с равной частотой.
Для фаззинга POST-запросов замените requests.get(url.format(uid)) на requests.post(url, json={"user_id": uid}, headers=headers). Структуру JSON подсмотрите в DevTools (вкладка Network → конкретный запрос → Payload) или перехватите через Burp Suite.
Где прячутся идентификаторы — проверяйте каждый вектор:
- URL path:
/api/orders/123— подставляйте через.format()в сегмент пути. - Query string:
?document_id=456— черезparams={"document_id": uid}вrequests.get(). - JSON body:
{"owner_id": 789}— черезjson={"owner_id": uid}вrequests.post(). - HTTP-заголовки:
X-User-Id: 123— встречается в микросервисных архитектурах. Передаётся черезheaders["X-User-Id"] = str(uid). - Cookie:
user_id=123— черезcookies={"user_id": str(uid)}. - GraphQL:
query { user(id: 124) { email } }— передаётся в теле POST-запроса как JSON.
Обнаружили IDOR на чтение — проверьте тот же эндпоинт на запись: отправьте PUT или PATCH с чужим ID и изменёнными данными. IDOR write (Data Manipulation, T1565) серьёзнее по импакту и поднимает severity репорта с P3 до P2.
Для ускорения перебора на нескольких эндпоинтах одновременно — concurrent.futures.ThreadPoolExecutor, стандартный модуль Python для многопоточности. Аккуратнее с нагрузкой: 10 потоков без time.sleep() между запросами могут вызвать блокировку от WAF или бан IP.
Когда скрипт не работает: предусловия и ограничения
UUID v4 вместо числовых ID. Пространство 2^122 значений — перебор невозможен за любое реальное время. Но UUID могут утекать: в JavaScript-файлах фронтенда, в ответах других API-эндпоинтов, в email-уведомлениях, в публичных профилях. Обнаружив чужой UUID, подставить его в запрос можно тем же скриптом — без цикла, точечно.
WAF и rate limiting. Сервер блокирует после N запросов в секунду — скрипт получит серию 429 (Too Many Requests) или бан IP. Первое решение: time.sleep(0.5) между итерациями. Для обхода более агрессивного ограничения нужны ротация IP через прокси-цепочки — это уже выходит за рамки базового скрипта.
Приложения с корректной авторизацией (ABAC/RBAC). Если сервер на каждый запрос проверяет принадлежность объекта — все ответы будут 403 или содержать идентичное тело с ошибкой. Скрипт корректно это покажет: ни одного срабатывания. Это хороший знак — авторизация работает.
Когда Burp Suite эффективнее. Расширение Autorize (ставится из BApp Store) автоматически подменяет сессионные токены на лету для каждого запроса, проходящего через прокси. Удобнее при тестировании десятков эндпоинтов с разными форматами. Python-скрипт для веб-пентеста выигрывает при глубоком тестировании одного эндпоинта с кастомной логикой анализа ответов. Я обычно начинаю с Autorize для разведки, а потом дописываю скрипт под конкретный эндпоинт, где нужна нестандартная логика сравнения.
Реальные CVE с IDOR: что находят при переборе параметров
Два примера из NVD (National Vulnerability Database — база уязвимостей NIST), которые ловятся именно тем паттерном, что заложен в нашем скрипте.
CVE-2025-13526 — плагин OneClick Chat to Order для WordPress. Функция wa_order_thank_you_override берёт order_id из URL без проверки прав. Неаутентифицированный атакующий мог менять order_id на странице подтверждения заказа и получать данные чужих покупок: имена, email, телефоны, адреса, содержимое заказов, метаданные платежей. CVSS 7.5 (HIGH), вектор AV:N/AC:L/PR:N/UI:N — атака по сети, низкая сложность, без привилегий, без действий пользователя. CWE-200 (Exposure of Sensitive Information). Порядковые ID заказов делали автоматический перебор тривиальным — базовый скрипт на requests справился бы за минуты. Исправлено в версии 1.0.9 добавлением проверки авторизации. CISA SSVC (система приоритизации уязвимостей от CISA): Track (мониторить). EPSS (оценка вероятности эксплуатации в ближайшие 30 дней): 0.0036, percentile 0.29.
CVE-2023-4836 — WordPress File Sharing Plugin (до версии 2.0.5). Плагин показывает файлы и папки без проверки авторизации; ID объектов подбираются перебором. CVSS 4.3 (MEDIUM), вектор AV:N/AC:L/PR:L/UI:N — нужны минимальные привилегии (залогиненный пользователь). CWE-639 — Authorization Bypass Through User-Controlled Key. CISA SSVC: Track* (следить, готовить патч), exploitation: poc (существует proof-of-concept). EPSS: 0.0049, percentile 0.40.
Обе уязвимости — один паттерн: порядковые числовые ID плюс отсутствие проверки принадлежности объекта пользователю. Именно то, что автоматически ловит написанный нами скрипт для поиска IDOR уязвимостей на Python.
Из этих CVE видна закономерность: IDOR прячется не в ядре CMS или фреймворка, а в плагинах и модулях, которые пишут маленькие команды без security review. OWASP фиксирует Broken Access Control на первой позиции рейтинга рисков уже несколько лет. Автоматизация поиска IDOR при этом — не замена ручному тестированию, а первый фильтр. Скрипт за минуту проверяет сотню ID на одном эндпоинте. За час — десятки эндпоинтов в scope. Ручной тестировщик через Burp Repeater физически не покроет такой объём за рабочий день. Разница не в мастерстве, а в охвате.
Но вот что стабильно упускают начинающие: останавливаются на факте «прочитал чужой профиль» и отправляют репорт с severity P3. А ведь цепочка IDOR read → IDOR write (подмена email через PUT-запрос) → password reset на подменённый адрес → Account Takeover (T1078, Valid Accounts — полный контроль над чужим аккаунтом) превращает P3 в P1, а выплату — из сотни долларов в несколько тысяч. Скрипт находит точку входа. Импакт определяется тем, что вы делаете дальше — а для этого нужно понимать, как устроены сессии, авторизация и логика приложения целиком. Если хотите разобраться в этом системно, а не собирать картину из разрозненных статей — на IB Basics в Codeby Academy такую базу закрывают за пару месяцев, без лишней терминологии и с практикой на каждом шаге.
Эту тему и смежные навыки разбирают на практике в курсе «Python для пентестера» Codeby Academy.