stihl не предоставил(а) никакой дополнительной информации.
В этой статье я расскажу про кейс с Bug Bounty, в котором мне удалось обойти бизнес‑логику приложения, создавать валидные платежи и списывать деньги клиентов. Я тестировал API одной финансовой организации, у которой есть фонд с личным кабинетом. Основные функции были за авторизацией, за исключением кнопки «Сделать взнос»...
Это исследование получило второе место на Для просмотра ссылки Войдиили Зарегистрируйся в категории «Hack the logic». Соревнование ежегодно проводится компанией Awillix.
Название кейса «Один шаг до крита» — не просто так. Я буквально поменял step=0 на step=1 и, даже не открывая Burp, получил два критичных для вендора бага: списание средств клиента по поддельным данным (оплата на произвольный договор без валидации данных) плюс массовая генерация чеков об оплате. Оценка критичности — critical.
или Зарегистрируйся позволяет обойти серверную валидацию входных параметров (номер договора, паспортные данные), сформировать действующую платежную ссылку и провести оплату деньгами с карты пользователя, даже если указанные договорные данные фиктивны. После завершения транзакции формируется официальная квитанция, подтверждающая оплату, несмотря на отсутствие проверки данных; деньги списываются с карты клиента в «фонд организации» на несуществующий договор — «в никуда».
Пример заполнения
Ожидаемо получил ошибку со статусом 400 (bad request) и увидел, что номер договора и последние шесть цифр паспорта введены неверно.
Пример ошибки
Конечно, сразу хочется проверить реализацию проверки неверных параметров, но я не стал торопиться и открывать Burp. Зачастую достаточно осмотреться, «потыкать кнопки» и ближе познакомиться с логикой работы приложения, а также с его легитимными функциями и бизнес‑логикой. Этим я и занялся, открыв инструменты разработчика вместо Burp, — многие баги действительно можно найти без специализированных инструментов.
В коде кнопки «Продолжить» скрывался POST-запрос со всеми параметрами из GUI, токеном капчи и любопытным параметром step=0.
Вооружившись инструментами разработчика (DevTools), я открыл запрос со статусом 400 и воспользовался функцией Edit and Resend. В теле POST-запроса я изменил значение параметра step с 0 на 1 и отправил запрос повторно. Капча больше не мешала — она уже была успешно пройдена на предыдущем шаге через интерфейс сайта.
Я готовился к очередной ошибке валидации, но на деле далее я убедился в статусе 200 и получил ссылку на оплату на этом всё, баг найден, спасибо за прочтение.
Пример изменения атрибута step с 0 на 1 и повторная отправка запроса с получением ссылки на оплату
После перехода по ссылке на payecom.ru наблюдаем платежку с указанной суммой и надписью «Фонд организации» (на скриншоте, к сожалению, необходимо замазать). Так как система оплаты (payecom.ru) выходит за скоуп, никакие манипуляции с ней я делать не пытался.
Однако убедиться в сформированной работающей платежной форме было необходимо (и крайне интересно в исследовательских целях).
Я использовал свою тестовую карточку и попробовал сделать минимальную транзакцию в «Фонд организации».
Ввод данных карты
Успешная оплата
После успешной транзакции пользователь перенаправляется на такой адрес:
Там он видит кнопку «Распечатать квитанцию», а также подтверждение успешной оплаты.
Успешная оплата и сформированный PDF с фиктивным номером договора
Так как данные договора фиктивные, то в поле ФИО подставилась точка — это не мои закраски белым цветом.
Таким образом, можно отправлять деньги никуда на фиктивные (даже несуществующие) данные.
Успешная попытка оплаты с неполными номерами договора и паспорта
Валидация поля с суммой amount реализована корректно (в рамках моих попыток манипуляций я не заметил никаких странностей). То есть если изменить amount на новое значение (например, с 500 рублей увеличить взнос до 600 или, наоборот, понизить), то ссылка сгенерируется с уже ранее введенной оригинальной суммой в 500 рублей. Скорее всего, это свидетельствует о том, что генерация платежной формы происходит где‑то на моменте первичного ввода неправильных данных или учитывается при GET-запросе к .../api/v3/autopays/default-values с дефолтным значением минимального платежа в 500 рублей.
Вот таким интересным способом и без Burp удалось обнаружить первую уязвимость критического уровня.
Она позволяет обойти необходимую валидацию входных данных и провести финансовую операцию с реальной оплатой, при этом сформировать официальную квитанцию на поддельные параметры. Это сочетание нарушений бизнес‑логики, уязвимостей верификации и отсутствия контроля на стороне сервера приводит к реальному риску финансового мошенничества и потери доверия к платформе.
Это создает дополнительно и репутационные риски для организации: если пользователь обнаружит после фишинга или в процессе самостоятельных манипуляций с запросами (менее вероятно, но смотрим на картину произошедшего в целом), что при неверных (несуществующих) данных договора и паспорта его транзакция все‑таки возможна и деньги утекают куда‑то в фонд, это может понизить доверие клиентов к бренду.
Я решил продолжить исследовать и посмотреть на сгенерированные квитанции об оплате более детально.
При подаче валидного uf_hash (от реально существующей оплаты) можно вписывать произвольный текст в uf_pay_id, и система сгенерирует новый PDF с соответствующим именем, подставив значение из uf_pay_id.
или Зарегистрируйся. Можно ввести легитимные данные, а можно по аналогии с моим репортом выше сформировать оплату по поддельным данным.
После проведения оплаты пользователя перенаправит на конечную точку Для просмотра ссылки Войдиили Зарегистрируйся<hash_отidоплаты>&uf_pay_id=<id_оплатыссайта_payecom.ru>.
Перенаправление на конечную точку
Получив валидное значение uf_hash, обратиться к эндпоинтам:
Либо вызвать API напрямую также GET-запросом:
После обращения к указанным эндпоинтам сформируется либо страница со ссылкой на PDF-квитанцию, либо JSON (если API вызвали напрямую) с той же ссылкой на PDF-квитанцию.
Легитимные ссылки на сформированные PDF с квитанциями
По описанным выше эндпоинтам можно подменять значение uf_pay_id= произвольным. Будет генерироваться новый PDF с такой же квитанцией, более того, значение uf_pay_id подставляется в имя нового PDF-файла. Так можно массово генерировать неограниченное число PDF на сервере.
PDF-документ доступен по прямой ссылке:
Здесь fake_PoC_pdf — вхождение переменной uf_pay_id.
Для просмотра ссылки Войдиили Зарегистрируйся
Далее я проверил, точно ли это новые документы, а не перезапись старого, сравнил хеш‑суммы и обратился к нескольким сгенерированным вручную документам. Все подтвердилось: я действительно могу после одной валидной транзакции (точнее, зная валидный uf_hash от транзакции) генерировать сотни, а может быть, даже тысячи документов. Оставалось продемонстрировать импакт и объяснить коллегам риски.
Каждый мой документ об оплате занимал ~19 Кбайт. Соответственно, сгенерировав 1 000 000 PDF-документов, можно заполнить ~19 Гбайт памяти на веб‑сервере.
Все сгенерированные документы хранятся по такому пути:
В качестве PoC для массовой генерации PDF я решил приложить коллегам скрипт на Bash, который почему‑то я все‑таки написал быстрее, чем открыл бы Burp и проделал все там (скрипт генерирует уникальные PDF в количестве пяти штук для демонстрации DoS без негативных последствий):
Запуск написанного на коленке скрипта и генерация пяти чеков в PDF
Таким образом, сервер не проверяет, что uf_pay_id действительно связан с переданным uf_hash (то есть с транзакцией).
Если атакующий располагает одним валидным uf_hash (например, полученным от собственной тестовой транзакции, в рамках триажа я предложил коллегам взять мой uf_hash 996d63e700aa83e2cc82ee4c51c6798b), он может массово генерировать неограниченное число квитанций PDF с уникальными именами (так как параметр uf_pay_id= подставляется в имя документа) и израсходовать свободное место на веб‑сервере lk.domain.com.
Причина — недостаточная привязка данных: uf_hash никак не защищен от повторного использования (злоупотребление именем конечного PDF), также возможен фаззинг и массовое формирование ошибок из‑за неверной попытки записи в файл при передаче некорректных данных в имя файла. Массовый вызов ошибок записи в файл с кодом 500 может также нагрузить систему или обработчик ошибок.
На мой взгляд, эта история подтверждает, что иногда достаточно посмотреть на бизнес‑логику под другим углом — и удача будет в кармане. Мне удалось продемонстрировать эксплуатацию, не открывая Burp Suite и вооружившись только инструментами разработчика.
Это исследование получило второе место на Для просмотра ссылки Войди
Название кейса «Один шаг до крита» — не просто так. Я буквально поменял step=0 на step=1 и, даже не открывая Burp, получил два критичных для вендора бага: списание средств клиента по поддельным данным (оплата на произвольный договор без валидации данных) плюс массовая генерация чеков об оплате. Оценка критичности — critical.
Баг первый: обход валидации данных
Эндпоинт API Для просмотра ссылки ВойдиКак воспроизвести
Я начал исследование и знакомство с тестируемой организацией на ресурсе lk.domain.com и нажал первую попавшуюся кнопку «Сделать взнос». Далее заполнил форму взноса произвольными данными и нажал «Продолжить».Ожидаемо получил ошибку со статусом 400 (bad request) и увидел, что номер договора и последние шесть цифр паспорта введены неверно.
Конечно, сразу хочется проверить реализацию проверки неверных параметров, но я не стал торопиться и открывать Burp. Зачастую достаточно осмотреться, «потыкать кнопки» и ближе познакомиться с логикой работы приложения, а также с его легитимными функциями и бизнес‑логикой. Этим я и занялся, открыв инструменты разработчика вместо Burp, — многие баги действительно можно найти без специализированных инструментов.
В коде кнопки «Продолжить» скрывался POST-запрос со всеми параметрами из GUI, токеном капчи и любопытным параметром step=0.
Вооружившись инструментами разработчика (DevTools), я открыл запрос со статусом 400 и воспользовался функцией Edit and Resend. В теле POST-запроса я изменил значение параметра step с 0 на 1 и отправил запрос повторно. Капча больше не мешала — она уже была успешно пройдена на предыдущем шаге через интерфейс сайта.
Я готовился к очередной ошибке валидации, но на деле далее я убедился в статусе 200 и получил ссылку на оплату на этом всё, баг найден, спасибо за прочтение.
После перехода по ссылке на payecom.ru наблюдаем платежку с указанной суммой и надписью «Фонд организации» (на скриншоте, к сожалению, необходимо замазать). Так как система оплаты (payecom.ru) выходит за скоуп, никакие манипуляции с ней я делать не пытался.
Однако убедиться в сформированной работающей платежной форме было необходимо (и крайне интересно в исследовательских целях).
Я использовал свою тестовую карточку и попробовал сделать минимальную транзакцию в «Фонд организации».
После успешной транзакции пользователь перенаправляется на такой адрес:
https://lk.domain.com/payment/complete?uf_hash=...&uf_pay_id=...
Там он видит кнопку «Распечатать квитанцию», а также подтверждение успешной оплаты.
Так как данные договора фиктивные, то в поле ФИО подставилась точка — это не мои закраски белым цветом.
info
В качестве дополнительного подтверждения списания я приложил коллегам из триажа скриншоты из моего ЛК банка о проведенной транзакции в «Фонд организации».Таким образом, можно отправлять деньги никуда на фиктивные (даже несуществующие) данные.
Другие поля
Поля с паспортом и номером договора также могут быть заполнены произвольными данными (в частности, неполными цифрами паспорта и неполным номером договора).Валидация поля с суммой amount реализована корректно (в рамках моих попыток манипуляций я не заметил никаких странностей). То есть если изменить amount на новое значение (например, с 500 рублей увеличить взнос до 600 или, наоборот, понизить), то ссылка сгенерируется с уже ранее введенной оригинальной суммой в 500 рублей. Скорее всего, это свидетельствует о том, что генерация платежной формы происходит где‑то на моменте первичного ввода неправильных данных или учитывается при GET-запросе к .../api/v3/autopays/default-values с дефолтным значением минимального платежа в 500 рублей.
Вот таким интересным способом и без Burp удалось обнаружить первую уязвимость критического уровня.
Она позволяет обойти необходимую валидацию входных данных и провести финансовую операцию с реальной оплатой, при этом сформировать официальную квитанцию на поддельные параметры. Это сочетание нарушений бизнес‑логики, уязвимостей верификации и отсутствия контроля на стороне сервера приводит к реальному риску финансового мошенничества и потери доверия к платформе.
Риски
- Нарушение бизнес‑логики: возможность проведения финансовой операции без прохождения проверки данных. Система не подтверждает, что договор существует, но создает реальный платеж.
- Финансовые потери и правовые риски: деньги списываются и попадают в «Фонд организации», но могут быть не связаны с клиентом, что создает ситуацию «висячих» средств. Пользователь может предъявить квитанцию, требуя зачисления, даже если договор поддельный.
- Мошенничество и фишинг: злоумышленник может генерировать квитанции на произвольные суммы и договоры, рассылать фишинговые ссылки с валидной формой оплаты, вводить в заблуждение сотрудников и клиентов.
Это создает дополнительно и репутационные риски для организации: если пользователь обнаружит после фишинга или в процессе самостоятельных манипуляций с запросами (менее вероятно, но смотрим на картину произошедшего в целом), что при неверных (несуществующих) данных договора и паспорта его транзакция все‑таки возможна и деньги утекают куда‑то в фонд, это может понизить доверие клиентов к бренду.
Причина уязвимости
- Отсутствие строгой проверки параметров contractNumber, passport на step=1.
- step передается от клиента и не контролируется сервером (нет session binding, флагов прохождения).
- Возможность генерации валидной платежной формы без авторизации и без предварительной проверки данных.
Рекомендации
- Привязать step=1 к успешному step=0 через backend-сессию, временный токен или nonce.
- Запретить генерацию платежного URL без успешной серверной валидации договора.
- Дополнительно: включить audit и alerting (в общем — мониторинг) при множественных попытках создания платежей с одного IP или для одного договора (тут уже говорю о снижении поверхности атаки, чтобы массово нельзя было генерировать ссылки. Текущий механизм защиты в виде капчи легко обходится, так как ее можно запросить по прямому GET-запросу и самостоятельно вставить токен и валидное решение на основе сгенерированной картинки).
- Ввести технические ограничения на доступ к API — валидация структуры договора, паспорта и их соответствия. Возможно, отказаться от генерации взносов в «Фонд организации» вне границ личного кабинета (сейчас я это сделал без какой‑либо авторизации по «Госуслугам» или входу в ЛК, так как кнопка «взноса» доступна без авторизации) и отключить эту опцию генерации взносов без предварительной верификации, чтобы не формировать массово ложные ссылки на оплаты и снизить риск социальной инженерии.
Я решил продолжить исследовать и посмотреть на сгенерированные квитанции об оплате более детально.
Баг второй: массовая генерация чеков об оплате
После оплаты взноса в «Фонд организации» можно массово генерировать квитанции с произвольными именами и исчерпать свободное место на веб‑сервере lk.domain.com. Сервер не проверяет на конечной точкеhttps://lk.domain.com/api/v3/pay?uf_hash=996d63e700aa83e2cc82ee4c51c6798b&uf_pay_id=<random_pay_id> соответствие между параметрами uf_hash и uf_pay_id.
При подаче валидного uf_hash (от реально существующей оплаты) можно вписывать произвольный текст в uf_pay_id, и система сгенерирует новый PDF с соответствующим именем, подставив значение из uf_pay_id.
Как воспроизвести
Сделать оплату в «Фонд организации» на конечной точке Для просмотра ссылки ВойдиПосле проведения оплаты пользователя перенаправит на конечную точку Для просмотра ссылки Войди
Получив валидное значение uf_hash, обратиться к эндпоинтам:
https://lk.domain.com/payment/complete?uf_hash=996d63e700aa83e2cc82ee4c51c6798b&uf_pay_id=965a3a6f-8ec8-55dc-16fb-674ff59c5647
Либо вызвать API напрямую также GET-запросом:
https://lk.domain.com/api/v3/pay?uf_hash=996d63e700aa83e2cc82ee4c51c6798b&uf_pay_id=965a3a6f-8ec8-55dc-16fb-674ff59c5647
После обращения к указанным эндпоинтам сформируется либо страница со ссылкой на PDF-квитанцию, либо JSON (если API вызвали напрямую) с той же ссылкой на PDF-квитанцию.
По описанным выше эндпоинтам можно подменять значение uf_pay_id= произвольным. Будет генерироваться новый PDF с такой же квитанцией, более того, значение uf_pay_id подставляется в имя нового PDF-файла. Так можно массово генерировать неограниченное число PDF на сервере.
PDF-документ доступен по прямой ссылке:
https://lk.domain.com/upload/invoices/Kvitanciya_po_operacii_fake_PoC_pdf_996d63e700aa83e2cc82ee4c51c6798b.pdf
Здесь fake_PoC_pdf — вхождение переменной uf_pay_id.
Для просмотра ссылки Войди
Далее я проверил, точно ли это новые документы, а не перезапись старого, сравнил хеш‑суммы и обратился к нескольким сгенерированным вручную документам. Все подтвердилось: я действительно могу после одной валидной транзакции (точнее, зная валидный uf_hash от транзакции) генерировать сотни, а может быть, даже тысячи документов. Оставалось продемонстрировать импакт и объяснить коллегам риски.
Каждый мой документ об оплате занимал ~19 Кбайт. Соответственно, сгенерировав 1 000 000 PDF-документов, можно заполнить ~19 Гбайт памяти на веб‑сервере.
Все сгенерированные документы хранятся по такому пути:
https://lk.domain.com/upload/invoices/Kvitanciya_po_operacii_...
В качестве PoC для массовой генерации PDF я решил приложить коллегам скрипт на Bash, который почему‑то я все‑таки написал быстрее, чем открыл бы Burp и проделал все там (скрипт генерирует уникальные PDF в количестве пяти штук для демонстрации DoS без негативных последствий):
Код:
HASH="996d63e700aa83e2cc82ee4c51c6798b" for i in $(seq 1 5); do PAY_ID="AbuseOkiDoki_$i" echo "[*] Запрос $i → PAY_ID=$PAY_ID" curl -s "https://lk.domain.com/api/v3/pay?uf_hash=$HASH&uf_pay_id=$PAY_ID" \ --compressed \ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ -H "Accept: application/json, text/plain, */*" \ -H "Referer: https://lk.domain.com/payment/complete?uf_hash=$HASH&uf_pay_id=test" \ -H "Origin: https://lk.domain.com" \ -H "Sec-Fetch-Site: same-origin" \ -H "Sec-Fetch-Mode: cors" \ -H "Sec-Fetch-Dest: empty" \ -H "Accept-Language: ru-RU,ru;q=0.9" \ -H "Connection: keep-alive" \ -H "X-Bug-Bounty:OkiDoki" echo -e "\n" sleep 1 done
Таким образом, сервер не проверяет, что uf_pay_id действительно связан с переданным uf_hash (то есть с транзакцией).
Если атакующий располагает одним валидным uf_hash (например, полученным от собственной тестовой транзакции, в рамках триажа я предложил коллегам взять мой uf_hash 996d63e700aa83e2cc82ee4c51c6798b), он может массово генерировать неограниченное число квитанций PDF с уникальными именами (так как параметр uf_pay_id= подставляется в имя документа) и израсходовать свободное место на веб‑сервере lk.domain.com.
Риски и причины
DoS по дисковому пространству: генерация сотен, а может быть, даже тысяч PDF будет значить исчерпание свободного места на веб‑сервере. Злоумышленнику не нужно быть аутентифицированным на ресурсе, чтобы исчерпать свободное место на диске.Причина — недостаточная привязка данных: uf_hash никак не защищен от повторного использования (злоупотребление именем конечного PDF), также возможен фаззинг и массовое формирование ошибок из‑за неверной попытки записи в файл при передаче некорректных данных в имя файла. Массовый вызов ошибок записи в файл с кодом 500 может также нагрузить систему или обработчик ошибок.
Рекомендации
- Валидировать соответствие uf_hash и uf_pay_id на сервере, а также исключить возможность записи PDF на веб‑сервер после изменения uf_pay_id (одной транзакции достаточно иметь одну PDF об оплате без возможности перезаписи или создания копии).
- Исключить произвольный uf_pay_id из имени файла или заменять его UUID. Рассмотреть обращение к этим эндпоинтам за аутентификацией (возможно, вовсе отказаться от механизма взносов в фонд без авторизации по «Госуслугам» и прочим провайдерам аутентификации) или ввести одноразовые токены на генерацию PDF.
- Скорректировать работу эндпоинта Для просмотра ссылки Войди
или Зарегистрируйся, чтобы при изменении uf_pay_id не генерировался новый PDF, а использовался ранее созданный. - Очищать неиспользуемые файлы по TTL или по истечении времени хранения.
- Смягчающая мера: ограничить число обращений к одному uf_hash (rate limit + TTL).
Выводы
Таким образом, мне удалось отыскать две уязвимости, которые коллеги высоко оценили с точки зрения критичности, несмотря на то что я зашел в программу багбаунти далеко не первым и там ранее было сдано несколько приличных багов.На мой взгляд, эта история подтверждает, что иногда достаточно посмотреть на бизнес‑логику под другим углом — и удача будет в кармане. Мне удалось продемонстрировать эксплуатацию, не открывая Burp Suite и вооружившись только инструментами разработчика.