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

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

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

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 или значение cookie session.

[Применимо: внешний пентест, внутренний пентест, 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)}")

Что происходит на каждом шаге:

  1. import requests — подключает HTTP-библиотеку.
  2. base_url — шаблон URL с {} на месте ID. Подставьте реальный эндпоинт целевого приложения.
  3. headers — токен авторизации Attacker. Именно от его имени идут все запросы. Скопируйте из DevTools браузера: вкладка Network → заголовок Authorization.
  4. baseline_len — длина ответа на запрос с вашим собственным ID. Точка отсчёта: если ответ с чужим ID существенно отличается по длине — сигнал.
  5. Цикл range(2, 100) — перебирает ID от 2 до 99. Диапазон подбирайте под конкретное приложение.
  6. requests.get(...) — отправляет GET-запрос с подставленным ID.
  7. Условие: статус 200 (данные отданы) И длина ответа отличается от baseline более чем на 50 символов. Порог 50 — защита от ложных срабатываний из-за CSRF-токенов и временных меток, которые меняются между запросами.
  8. 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.