Поиск и доказательство багов
Уязвимость, которую ты не можешь доказать другому, ещё не существует — для триажёра это шум. Этот урок — ремесло превращения догадки в отчёт, переживающий чужую машину — воспроизводимый PoC, честное утверждение об ущербе и раскрытие, остающееся в скоупе.
На авторизованном задании тестировщик находит настоящий баг нарушенного контроля доступа: эндпоинт настроек возвращает конфиг чужого тенанта. На его ноутбуке это воспроизводится безупречно каждый раз. Он пишет три строки — «залогинься, сделай GET к эндпоинту, наблюдай данные другого тенанта» — и отправляет. Через три недели отчёт закрыт: невоспроизводимо. А баг был абсолютно реален. Тестировщик забыл записать, что на его аккаунте был включён preview-флаг фичи, а сессионный токен выпущен до деплоя — невидимое состояние, жившее только на его машине. Триажёр выполнил три строки на свежем аккаунте и получил чистый 403. Это урок, которому не учат рядом с эксплуатацией: уязвимость, которую ты нашёл, и уязвимость, которую ты можешь доказать незнакомцу — две разные вещи, и чинят только вторую. Вся вторая половина наступательной работы — после остроумного бага, до признания заслуг — это неброское ремесло построения доказательства, переживающего единственную важную границу: чужую машину. Найти — половина работы. Доказать — та половина, что решает, засчитана ли находка.
К концу урока ты сможешь превратить подтверждённый баг в отчёт, переживающий триаж: самодостаточный воспроизводимый PoC, честное утверждение об ущербе и серьёзности и раскрытие, строго остающееся в скоупе.
Сначала об авторизации: где живёт доказательство
Всё в этом уроке предполагает подписанное задание с явным скоупом — программу bug bounty с целью в списке активов, пентест с документом правил вовлечения, намеренно уязвимую лабораторию или твоё собственное приложение. Сам акт доказательства бага — ровно то место, где этика обостряется сильнее всего, потому что доказательство — там, где живёт соблазн «просто показать, насколько всё плохо на самом деле». Дисциплина, которой учит урок — минимально необходимое доказательство, уважающий скоуп ущерб, координированное раскрытие — это не бюрократическая вежливость; это грань между исследователем и взломщиком. Одно межаккаунтное чтение записи, которую ты подсадил, доказывает IDOR; выгрузка десяти тысяч инвойсов реальных клиентов, чтобы «продемонстрировать серьёзность», доказывает тот же баг, попутно совершая свежую утечку данных с твоим именем на ней. Здесь нет рабочих эксплойтов или собранных данных — только рассуждение, позволяющее доказать находку, не став самим инцидентом.
Найденный баг — это ещё не доказанный баг
Ментальный сдвиг, который делают зрелые исследователи, — перестать считать отчёт формальностью и начать считать его настоящим продуктом. Уязвимость в коде — не то, что ты поставляешь; доказательство — вот продукт. А у доказательства есть жестокий приёмочный тест, не имеющий отношения к тому, реален ли баг: сможет ли незнакомец на своей машине выполнить твои шаги и увидеть то же самое? Если ответ «нет», баг — каким бы реальным он ни был — неотличим от шума, и занятый триажёр, разбирающий очередь, закроет его.
Эта переформулировка превращает остаток работы в три конкретных артефакта, которые отчёт обязан нести, у каждого свой режим отказа:
- Воспроизводимый PoC — детерминированные, минимальные, самодостаточные шаги. Падает, когда тайно зависит от состояния, которое есть только у тебя.
- Честное утверждение об ущербе — что баг реально даёт атакующему, на бизнес-языке, с точной серьёзностью. Падает, когда оно завышено или оставлено пустым.
- Раскрытие, уважающее скоуп — минимально необходимое доказательство, никаких данных реальных жертв, приватное сообщение первым. Падает, когда «показ ущерба» переходит в его причинение.
Воспроизводимость: свойство, решающее всё
PoC воспроизводим, когда он детерминирован, минимален и самодостаточен — три свойства, отказывающие по-разному, и эти отказы — причина, по которой реальные баги закрывают.
Детерминирован значит даёт тот же результат каждый запуск, а не «иногда». Слово иногда в отчёте смертельно: оно гарантирует, что триажёр запустит твои шаги один раз, попадёт на путь, где ничего не происходит, и закроет тикет. Гонка или зависимый от состояния баг всё ещё доказуем — но ты обязан зафиксировать точные условия, при которых он срабатывает, чтобы «иногда» стало «каждый раз, когда X».
Минимален значит ты срезал повтор до единственной значимой переменной. Зрелое написание PoC — это редукция: меняй одно и то же и перезапускай, пока шаги не станут кратчайшей последовательностью, всё ещё демонстрирующей баг. Повтор, который триажёр повторит меньше чем за минуту, берут в работу; двенадцатишаговый ритуал с тремя расширениями браузера и кастомной цепочкой прокси откладывают «когда будет время», а это никогда.
Самодостаточен — то самое свойство, что закрывает реальные баги как невоспроизводимые, ловушка из вступления. Твой повтор обязан объявить каждое предусловие явно, потому что шаги выполняются в мире триажёра, а не твоём. Скрытое предполагаемое состояние — флаг фичи, включённый на твоём аккаунте, токен, выпущенный до деплоя, запись, существующая лишь потому, что ты создал её на прошлой неделе, конкретный тариф аккаунта — невидимо для тебя именно потому, что всегда истинно на твоей машине. Лекарство — записать каждое предусловие («требуется аккаунт с флагом F; запись-жертва N должна существовать») или, лучше, редуцировать, пока повтор вовсе не перестанет зависеть от этого состояния.
| Свойство | Что его ломает | Что фиксирует триажёр | Дисциплина |
|---|---|---|---|
| Детерминирован | «Чужие данные видны иногда» | Запустил раз, ничего → невоспроизводимо | Зафиксируй точные условия: «каждый раз, когда X» |
| Минимален | 12 шагов, кастомный прокси, три расширения | Отложено «когда будет время» | Сведи к одной переменной; повтор <1 мин |
| Самодостаточен | Скрытое состояние: флаг, токен до деплоя | Чистый 403 на свежем аккаунте → невоспроизводимо | Заяви каждое предусловие; редуцируй скрытое |
| Наблюдаемое vs ожидаемое | Одинокий скриншот 200 без базовой линии | «Может, твои же данные» → не подтверждено | Заяви фактическое (200, данные B) vs ожидаемое (403) |
Ещё один элемент отделяет доказательство от скриншота: наблюдаемое против ожидаемого. Голый ответ 200 сам по себе ничего не доказывает — это может быть твоя собственная запись, артефакт кэша или причуда сессии. Доказательство — это разрыв: «с сессией аккаунта A я запросил запись аккаунта B и получил 200, возвращающий данные B, тогда как корректная система возвращает 403». Два аккаунта под твоим контролем, одна изменённая переменная, фактический результат, названный против ожидаемого. Эта структура и делает межаккаунтное чтение неоспоримым, а не догадкой с картинкой.
▸Почему это работает
Почему «самодостаточность» — то свойство, что закрывает реальные баги, тогда как детерминизм и минимальность закрывают в основном слабые? Потому что скрытое состояние невидимо по построению. Недетерминированный или раздутый повтор хотя бы выглядит подозрительно для собственного автора — ты чувствуешь это «иногда». А предполагаемое состояние окружения ощущается как пол реальности: флаг был на твоём аккаунте два дня, так что эндпоинт, конечно, ведёт себя так; запись всегда была, ведь ты её создал. Ты не опускаешь предусловие из лени — ты искренне не воспринимаешь его как предусловие, потому что оно было постоянным в каждом твоём наблюдении. Вот почему дисциплина не может быть «не забудь упомянуть предусловия»; она обязана быть структурной привычкой воспроизводить собственную находку с чистого листа — свежий аккаунт, новый токен, ничего заранее не подготовлено — и смотреть, что ломается. То, что ломается, и есть предусловие, которое ты никогда бы не записал. Самодостаточность — не шаг документации; это эксперимент, который ты ставишь против себя.
Ущерб: половина отчёта, задающая приоритет
Воспроизводимый PoC доказывает, что баг существует; утверждение об ущербе решает, есть ли кому до него дело — и тут живут два противоположных отказа.
Завышение — заявлять больше, чем доказал: помечать read-only SSRF к одной безобидной внутренней status-странице как «Критический RCE, 10.0». Механизм может быть реальным, но если ничто в отчёте не показывает выполнение кода или доступ к metadata, серьёзность сфабрикована. Цена не только в том, что триажёр понизит этот один отчёт — в том, что он перестанет доверять каждому числу, что ты отправишь после. Репутация — валюта раскрытия, и ты тратишь её навсегда в первый же раз, когда переоцениваешь. Честный ход — назвать доказанный потолок и пометить реалистичную цепочку отдельно: «подтверждён read-only SSRF к внутренним хостам; я не достиг облачного metadata-эндпоинта, но тот же примитив правдоподобно мог бы, что дало бы учётки — помечаю как реалистичный цепочечный риск, не продемонстрированный».
Пустота — противоположность: «может быть плохо». Триажёр не может приоритизировать пожатие плечами. Сильное утверждение об ущербе переводит технический примитив на бизнес-язык — кто может сделать что с чьими данными, каким действием, и реалистичный потолок. Для IDOR на последовательных id инвойсов без rate-limit: «любой аутентифицированный пользователь может прочитать инвойс любого другого клиента — имя, платёжный адрес, последние четыре цифры карты — запросив id, которым не владеет; id последовательны и без троттлинга, поэтому весь корпус перечислим, что я описываю, но не выполняю». Это задаёт защитимую серьёзность (высокое влияние на конфиденциальность, нарушенный контроль доступа) и указывает на фикс.
Язык серьёзности — общий контракт: CVSS существует, чтобы «high» значил примерно одно и то же для тебя и триажёра. Тебе не нужно вычислять идеальный балл, но привязка к вектору (сложность атаки, требуемые привилегии, влияние на конфиденциальность/целостность/доступность) держит тебя честным: read-only межтенантная утечка и захват сервера — не одно число, и притворяться иначе — это завышение, надевшее формулу.
На авторизованной цели bug-bounty, где разрешены только тестовые аккаунты и нет доступа к реальным данным клиентов, ты подтверждаешь IDOR: с сессией аккаунта A запрос GET /api/invoices/{id} возвращает инвойс аккаунта B, когда запрашивается id B. Правила вовлечения запрещают массовые автоматические запросы. Тебе нужно доказательство, чинящее это быстрее всего, не выходя за скоуп. Что ты отправишь?
Раскрытие: доказать, не став инцидентом
Последний артефакт — это поведение, и у него два правила, против которых будет толкать соблазн впечатлить.
Минимально необходимое доказательство. Докажи класс наименьшей возможной демонстрацией и остановись. Одна запись-канарейка, не вся таблица. Один поддельный токен на тестовом аккаунте, не каждый аккаунт. Инстинкт «показать, насколько всё плохо на самом деле», идя дальше — ровно тот инстинкт, что превращает чистую находку в преступление, потому что лишняя демонстрация обращается к реальным данным, трогать которые ты не уполномочен. Класс был доказан при первом чтении; всё после — вред без доказательной выгоды.
Координированное, приватное-первым раскрытие. Ответственное раскрытие означает сообщить владельцу приватно первым и дать разумное окно на починку — обычно 90 дней, фактический отраслевой стандарт — до любой публичной детали. Публикация рабочего эксплойта, а хуже — zip с реальными данными клиентов, до того как у владельца был шанс исправить, вооружает баг: подвергает тех самых пользователей, которых ты должен был защитить, и переводит из исследователя в атакующего. Опубликованная политика раскрытия уязвимостей или канал bug-bounty формализует этот контракт с другой стороны, давая тебе определённый приём и SLA, а владельцу — определённое окно ответа.
Асимметрия и есть весь аргумент: несколько минут явных предусловий и честного утверждения об ущербе на приёме против реального бага, что гниёт, пере-обнаруживается и приземляется публичным 0-day с приложенным инцидентом. Ремесло доказательства — то, что держит находку — и твою репутацию — на правильной стороне этой грани.
Ты сообщаешь о настоящем баге нарушенного контроля доступа тремя краткими шагами, и он закрыт как «невоспроизводимо». Твой повтор идеально работает на ноутбуке каждый раз. Какова единственная наиболее вероятная причина и верный фикс?
На авторизованном задании ты подтвердил IDOR на последовательных id инвойсов без rate-limit. Как доказать масштаб ущерба, не выходя за скоуп?
Упорядочи, как зрелый исследователь превращает подтверждённый IDOR в скоупе в отчёт, который чинят:
- 1 Сведи к минимальному детерминированному повтору на двух аккаунтах под твоим контролем, читая одну подложенную запись-жертву (наблюдаемое 200 с данными B вместо ожидаемого 403)
- 2 Перезапусти с чистого листа — свежий аккаунт, новый токен — чтобы вскрыть любые скрытые предусловия и сделать PoC самодостаточным
- 3 Напиши честное утверждение об ущербе и серьёзности: кто читает чьи данные, описанный потолок перебора, точный класс CVSS, направление исправления
- 4 Раскрой приватно владельцу с минимально необходимым доказательством и придержи публичную деталь до закрытия окна починки
- 01Какие три свойства делают PoC воспроизводимым, как отказывает каждое и почему самодостаточность — то, что закрывает по-настоящему реальные баги?
- 02Каковы два режима отказа утверждения об ущербе и два правила раскрытия, удерживающие доказательство бага от его совершения?
Найти уязвимость — точка входа, а не продукт; продукт — отчёт. Баг, который ты можешь продемонстрировать на своей машине, и баг, который ты можешь доказать незнакомцу — две разные вещи, и занятый триажёр чинит только второй, так что вторая половина наступательной работы — ремесло построения доказательства, переживающего чужую машину. Это доказательство несёт три артефакта. Воспроизводимый PoC обязан быть детерминированным (никаких «иногда» — зафиксируй условия срабатывания), минимальным (сведён к одной изменённой переменной, повторяемый меньше чем за минуту) и самодостаточным (каждое предусловие заявлено, ведь скрытое состояние — флаг фичи, токен до деплоя, запись, которую создал лишь ты — невидимо тебе именно потому, что всегда истинно у тебя, и это оно закрывает по-настоящему реальные баги как «невоспроизводимо»); структурное лекарство — перезапуск с чистого листа и показ наблюдаемое-против-ожидаемого на двух аккаунтах под твоим контролем. Честное утверждение об ущербе переводит примитив в бизнес-риск с точной серьёзностью CVSS, избегая и завышения (которое навсегда тратит репутацию), и пустоты (которая не приоритизируется), и сообщает масштаб описанием потолка перебора, а не его достижением. Уважающее скоуп раскрытие доказывает класс минимально необходимым доказательством — одна подложенная канарейка, никогда данные реальных жертв — и сообщает владельцу приватно первым с разумным окном на починку до любой публичной детали. Поэтому в следующий раз, подтвердив баг, вопрос, решающий, засчитан ли он — не «реален ли он?» (ты уже знаешь, что да), а «увидит ли незнакомец на своей машине ровно то, что увидел я, и доказал ли я ущерб, не став им?»
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.