
إثبات مفهوم تعليمي وتحليل لـ CVE-2025-68621، هجوم توقيت على تسجيل دخول المزامنة في Trilium Notes. يوضح استرداد تجزئة HMAC عبر القناة الجانبية لتوقيت الشبكة، مع السبب الجذري والإصلاح وتفاصيل التخفيف.
/api/login/syncالخطورة: عالية (CVSS 7.4) البرنامج المتأثر: TriliumNext/Trilium < 0.101.0 نوع الثغرة: CWE-208 – تباين توقيت ملحوظ تم الإصلاح في: Trilium 0.101.0 (PR #8129) تاريخ النشر: 2026-02-06 | تاريخ الحجز: 2025-12-19
Trilium Notes هو تطبيق مفتوح المصدر متعدد المنصات هرمي لتدوين الملاحظات، مصمم لبناء قواعد معرفية شخصية كبيرة. يدعم:
تتيح ميزة المزامنة لمستخدم Trilium المصادقة على خادم Trilium بحيث تظل الملاحظات متزامنة عبر الأجهزة. نقطة نهاية المزامنة هذه هي نقطة الدخول لـ CVE-2025-68621.
هجوم التوقيت هو هجوم جانبي يتعلم فيه المهاجم معلومات سرية عن طريق قياس المدة التي يستغرقها النظام لمعالجة مدخلات مختلفة.
المثال الكلاسيكي هو مقارنة السلاسل النصية:
"correct_password" !== "aorrect_password" → يفشل في الموضع 0 → سريع
"correct_password" !== "cXrrect_password" → يفشل في الموضع 1 → أبطأ قليلاً
"correct_password" !== "correct_password" → تطابق كامل → أبطأ
تقارن معظم لغات البرمجة السلاسل النصية حرفًا بحرف وتتوقف فور اكتشاف عدم تطابق (خروج مبكر). يعني هذا:
الحل هو استخدام دالة مقارنة زمنية ثابتة تفحص دائمًا كل بايت بغض النظر عن مكان حدوث عدم التطابق.
تم اكتشاف الثغرة من خلال مراجعة يدوية للكود لمنطق المصادقة في Trilium. فحص الباحث تدفق تسجيل الدخول للمزامنة في apps/server/src/routes/api/login.ts ولاحظ النمط التالي في الدالة loginSync() (حوالي السطر 111):
const documentSecret = options.getOption("documentSecret");
const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash;
if (expectedHash !== givenHash) { // ← سطر ضعيف
return [400, { message: "Sync login credentials are incorrect..." }];
}
العلم الأحمر هو استخدام عامل المقارنة المدمج !== في JavaScript لمقارنة تجزئات HMAC. عامل !== ليس زمنيًا ثابتًا — فهو يخرج فور العثور على حرف مختلف. نظرًا لأن المقارنة تتم على سلاسل نصية عادية (بدون استخدام دالة مقارنة آمنة تشفيريًا)، فإن وقت الاستجابة يتسرب معلومات حول عدد البايتات السابقة الصحيحة من تخمين المهاجم.
ثم طرح الباحث السؤال:
"هل يمكن تضخيم هذا الفرق الزمني الصغير بما يكفي، عبر الشبكة، لاسترداد تجزئة HMAC كاملة مكونة من 44 حرفًا مشفرًا بـ Base64؟"
اتضح أن الإجابة هي نعم — مع تكرار قياسات كافية وبعض التحليل الإحصائي، يرتفع الإشارة فوق الضوضاء.
عندما يريد عميل Trilium المزامنة، يستدعي POST /api/login/sync مع جسم JSON كالتالي:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 لـ documentSecret + timestamp، مشفر بـ Base64>"
}
يعمل الاسترداد بايتًا بايت كما يلي:
للموضع = 0 إلى 43:
لكل حرف مرشح ج في مجموعة الأحرف (A-Z, a-z, 0-9, +, /, =):
أرسل SAMPLES طلبًا مع hash = known_prefix + ج + padding
سجل متوسط وقت الاستجابة
أفضل حرف = المرشح الذي لديه أعلى متوسط وقت
أضف أفضل حرف إلى known_prefix
بعد 44 تكرارًا (واحد لكل حرف Base64)، يتم استرداد تجزئة HMAC كاملة المكونة من 44 حرفًا.
المتطلبات العملية:
time.perf_counter() في بايثون يوفر دقة نانوثانية)انظر poc.py للحصول على إثبات مفهوم موثق بالكامل بلغة بايثون.
ملخص سريع لما يفعله إثبات المفهوم:
A–Z, a–z, 0–9, +, /, =)./api/login/sync لكل مرشح ويقيس متوسط وقت الاستجابة الوسيط.إخلاء مسؤولية: تم توفير إثبات المفهوم هذا للأغراض التعليمية وبحوث الأمان المسؤولة فقط. لا تستخدمه ضد أنظمة لا تملكها أو لديك إذن كتابي صريح لاختبارها.
تقوم عوامل المقارنة !== (و ===) في JavaScript بإجراء مقارنة معجمية مع خروج مبكر. السطر الضعيف في apps/server/src/routes/api/login.ts:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
يخلق سلوك الخروج المبكر فرقًا زمنيًا قابلاً للقياس لكل بايت مطابق:
| التخمين مقابل المتوقع | البايتات المقارنة | الوقت |
|---|---|---|
| بايت 0 خاطئ | 1 | ~T |
| بايت 0 صحيح، بايت 1 خاطئ | 2 | ~T + δ |
| بايت 0-1 صحيح، بايت 2 خاطئ | 3 | ~T + 2δ |
| … | … | … |
| جميع الـ 44 بايت صحيحة | 44 | ~T + 43δ |
كل بايت إضافي مطابق يكلف مقدارًا ضئيلًا إضافيًا من وقت وحدة المعالجة المركزية δ. عبر آلاف العينات، يكون متوسط وقت الاستجابة لتخمين "البايت N الصحيح" أطول بشكل قابل للقياس من تخمين "البايت N الخاطئ"، مما يتسرب معلومات كافية لاسترداد تجزئة HMAC كاملة حرفًا حرفًا.
| المقياس | القيمة | السبب |
|---|---|---|
| النقاط الأساسية | 7.4 عالية | |
| ناقل الهجوم | شبكة (N) | قابل للاستغلال عبر الإنترنت |
| تعقيد الهجوم | عالٍ (H) | يتطلب طلبات كثيرة + توقيت مستقر |
| الامتيازات المطلوبة | لا شيء (N) | لا حاجة لحساب |
| تفاعل المستخدم | لا شيء (N) | لا يحتاج الضحية لفعل أي شيء |
| النطاق | غير متغير (U) | يتأثر فقط خادم Trilium |
| السرية | عالية (H) | قاعدة الملاحظات بأكملها قابلة للقراءة |
| النزاهة | عالية (H) | يمكن للمهاجم كتابة/تعديل الملاحظات |
| التوفر | لا شيء (N) | لا يوجد مكون رفض الخدمة |
سلسلة المتجهات: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
الاستغلال الناجح يمنح المهاجم:
هذا شديد الخطورة بشكل خاص للمستخدمين الذين يخزنون بيانات شخصية حساسة (كلمات مرور، مستندات خاصة، إدخالات يوميات) في قاعدة معرفة Trilium الخاصة بهم.
يستبدل الإصلاح عملية المقارنة غير الثابتة زمنيًا !== بوظيفة Node.js المدمجة crypto.timingSafeEqual():
قبل (ضعيف):
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
بعد (آمن):
import * as crypto from "crypto";
const expectedBuffer = Buffer.from(expectedHash);
const givenBuffer = Buffer.from(givenHash ?? "");
if (expectedBuffer.length !== givenBuffer.length ||
!crypto.timingSafeEqual(expectedBuffer, givenBuffer)) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
تضمن crypto.timingSafeEqual() مقارنة كل بايت دائمًا، لذا لا يعتمد وقت التنفيذ على عدد البايتات المطابقة. يختفي إشارة التوقيت.
انظر vulnerable.ts و fix.ts للحصول على أمثلة كود جنبًا إلى جنب.