«Чаще всего дверь в систему открывают не блестящие атаки на нулевые дни, а банальная человеческая забывчивость — пароли в репозитории, демо1-демо1 в продакшене. Но иногда встречаются такие уязвимости, что после их обнаружения хочется спросить: ‘а вы точно проверяли, как это работает?'».
1. Уязвимость, которая была в самом названии
Одна из первых уязвимостей, которую я находил, заставила задуматься о том, как много информации разработчики оставляют на виду просто потому, что считают её неважной. Речь шла о внутреннем сервисе компании, название которого было сформировано по шаблону: prod-[название_сервиса]-[версия]-[дата_сборки]. Например, prod-invoice-processor-v2.1-20240115.
Проблема была не в самом названии, а в том, что доступ к панели администратора этого сервиса осуществлялся через поддомен, который формировался автоматически из этого же имени. Система оркестрации просто брала имя контейнера и создавала для него DNS -запись вида [имя_контейнера].internal.company.ru. В результате любой, кто имел доступ к внутренней сети, мог просто перебрать логичные названия сервисов и попасть на интерфейсы управления, которые часто были защищены стандартными или слабыми учётными данными.
Это пример классической уязвимости через избыточное раскрытие информации. Система администрирования, которая должна быть изолирована, оказалась на виду только из-за автоматизированного, но необдуманного процесса развёртывания. Устранили проблему пересмотром политики именования и введением отдельного пространства имён для служебных интерфейсов.
2. Когда ‘readonly’ на самом деле ‘read-write’
В процессе пентеста веб-приложения столкнулся с интересной ситуацией. Приложение работало с файловой системой, и для определённых операций использовался PHP-функционал, который должен был ограничиваться режимом ‘только чтение’. В коде было написано:
$handle = fopen('/path/to/data.json', 'r');
// Далее операции чтения
Проверка казалась простой: режим ‘r’ действительно предназначен только для чтения. Однако в одном из скриптов, который обрабатывал загрузку пользовательских аватаров, разработчики по невнимательности использовали эту же переменную $handle для записи лога ошибок. Код выглядел примерно так:
if ($upload_error) {
fwrite($handle, "Error: " . $upload_error . "n");
}
Функция fwrite спокойно писала в дескриптор, открытый в режиме ‘r’. В некоторых версиях и конфигурациях PHP это не вызывало ошибки, а тихо выполнялось. В результате, манипулируя ошибкой загрузки, можно было инжектировать произвольные данные в критичный системный файл data.json, что в конечном итоге привело бы к его повреждению или выполнению кода.
Уязвимость заключалась не в функции, а в нарушении предполагаемого контракта. Дескриптор, открытый для одной цели, использовался для другой. Это подчёркивает важность проверки не только прав доступа при открытии ресурса, но и контроля за тем, как и где этот ресурс используется далее по коду.
3. API, которое отвечало на любой запрос ‘OK’
Проверяя интеграцию с внешним платежным сервисом, обнаружил эндпоинт /api/v1/confirm-payment. Согласно документации, он должен был принимать сложный JSON -объект с подписью транзакции, проверять её и возвращать статус. В ходе тестирования выяснилось, что внутренняя логика проверки подписи была сломана ещё несколько версий назад из-за обновления библиотеки шифрования.
Вместо корректной проверки код возвращал HTTP 500 при любых ошибках валидации. Чтобы ‘заглушить’ ошибки и не ломать процесс для клиентов, разработчики добавили глобальный обработчик исключений в этом методе, который просто возвращал {"status": "OK"} на любой входящий запрос, если первоначальный парсинг JSON прошёл успешно.
отправка пустого JSON-объекта {} или объекта с фиктивными полями приводила к ‘подтверждению’ платежа. Система далее отмечала заказ как оплаченный. Эта уязвимость жила в продакшене несколько месяцев, потому что автоматические тесты проверяли только положительный сценарий с корректными данными, а мониторинг ошибок 500 был отключён для этого эндпоинта, чтобы ‘не зашумлять’ логи.
История показывает, как попытка быстро решить проблему пользовательского опыта (убирая ошибки) создала критическую брешь в бизнес-логике. Безопасность была принесена в жертву бесперебойности работы, причём в самой чувствительной части — финансовых операциях.
4. Переменная окружения, которая переопределяла всё
Этот случай связан с конфигурацией приложения, развёрнутого в контейнерах. Приложение использовало иерархию конфигурационных файлов: базовый config.yaml, затем переопределения для среды (дев, стейдж, прод) и, наконец, переменные окружения для тонкой настройки.
Была переменная окружения с именем CONFIG_PATH. По задумке, она должна была указывать на кастомный путь к конфигурации для тестовых прогонов. Однако в коде загрузки конфигурации была ошибка: эта переменная проверялась и применялась первой, до загрузки каких-либо файлов. Если CONFIG_PATH был пустым или указывал на несуществующий файл, система не падала, а просто продолжала работу с пустой конфигурацией, используя значения по умолчанию, вшитые в код.
Злоумышленник, получивший возможность устанавливать переменные окружения (например, через уязвимость в соседнем сервисе или через панель управления оркестратором), мог просто установить CONFIG_PATH=/dev/null. Это приводило к сбросу всех критичных настроек: отключению аутентификации, сбросу паролей администратора на значения по умолчанию, открытию debug-Mode и портов.
Уязвимость была в неправильном порядке загрузки и отсутствии валидации. Критичный путь конфигурации не должен полностью зависеть от пользовательского ввода, даже если это переменная окружения. Нужна была проверка на существование файла и fallback на встроенную безопасную конфигурацию, а не на дефолты из кода.
5. База данных, которая слушала всех
Не все уязвимости находятся в коде приложения. Часто они возникают на уровне инфраструктуры. В одном из проектов столкнулся с базой данных PostgreSQL, развёрнутой в облачной среде. Сетевые политики были настроены, как казалось, правильно: доступ на порт 5432 был разрешён только для определённой группы внутренних сервисов.
Проблема обнаружилась при анализе логов самой СУБД. В конфигурационном файле postgresql.conf параметр listen_addresses был установлен в значение '*' (слушать все интерфейсы). Это стандартная настройка для простоты развёртывания. Однако в сочетании с правилом сетевого файрвола, которое по ошибке разрешало входящий трафик на этот порт не из конкретной подсети, а с любых адресов внутри всего облачного проекта VPC, это привело к неожиданным последствиям.
Любой сервис, контейнер или виртуальная машина, развёрнутая в том же облачном проекте (а таких могло быть сотни, включая временные среды для тестирования), мог подключиться к этой продовой базе данных. Достаточно было знать её внутренний IP -адрес. Аутентификация по паролю была включена, и хотя пароли были сложными, их можно было найти в открытых конфигурационных файлах других, менее защищённых сервисов того же проекта.
Это пример цепочки уязвимостей: небезопасная настройка по умолчанию на уровне СУБД, слишком широкое правило на уровне сетевой безопасности и утечка учётных данных через третьи сервисы. Каждый слой в отдельности казался не критичным, но вместе они создавали широкий вектор для атаки.
Чему учат эти ‘глупые’ уязвимости
На первый взгляд, каждый из этих случаев можно списать на невнимательность или ошибку в давно написанном коде. Но их объединяют более глубокие проблемы, характерные для многих проектов:
- Автоматизация без понимания. Скрипты развёртывания, оркестраторы и CI/CD -пайплайны часто копируют шаблоны, не учитывая контекст безопасности. Автоматическое создание DNS -имён из метаданных — яркий пример.
- Приоритет ‘работы’ над корректностью. Обработчик исключений, который всегда возвращает ‘OK’, был реализован, чтобы система не падала перед клиентами. Это временное решение стало постоянным и крайне опасным.
- Слепая вера в изоляцию сред. Убеждение, что ‘это же внутренняя сеть’ или ‘это же один проект в облаке’, приводит к ослаблению контроля. Границы периметра размываются, и внутренняя сеть становится новым фронтом атаки.
- Неявные контракты. Когда ресурс открывается с одними намерениями (чтение), но используется для других (запись), возникает рассогласование. Это признак плохого проектирования потока данных.
Поиск и устранение таких уязвимостей редко требует сложных инструментов статического анализа или дорогих сканеров. Часто достаточно методичного ручного тестирования, внимательного чтения конфигурационных файлов и простого вопроса на каждом этапе: ‘а что будет, если здесь что-то пойдёт не так, как мы предполагаем?’. Самые большие риски часто прячутся не в сложных алгоритмах, а в простых, но ошибочных допущениях о том, как система должна работать.