
PoC educativo y análisis de CVE-2025-68621, un ataque de sincronización en el inicio de sesión de sincronización de Trilium Notes. Demuestra la recuperación de hash HMAC mediante un canal lateral de sincronización de red, con causa raíz, corrección y detalles de mitigación.
/api/login/syncGravedad: ALTA (CVSS 7.4) Software afectado: TriliumNext/Trilium < 0.101.0 Tipo de vulnerabilidad: CWE-208 – Discrepancia de temporización observable Corregido en: Trilium 0.101.0 (PR #8129) Publicado: 2026-02-06 | Reservado: 2025-12-19
Trilium Notes es una aplicación de toma de notas jerárquica, de código abierto y multiplataforma, diseñada para construir grandes bases de conocimiento personales. Admite:
La función de sincronización permite que un cliente Trilium se autentique en un servidor Trilium para que las notas permanezcan sincronizadas entre dispositivos. Este endpoint de sincronización es el punto de entrada de CVE-2025-68621.
Un ataque de temporización es un ataque de canal lateral en el que un atacante obtiene información secreta midiendo cuánto tiempo tarda un sistema en procesar diferentes entradas.
El ejemplo clásico es la comparación de cadenas:
"correct_password" !== "aorrect_password" → fails at position 0 → fast
"correct_password" !== "cXrrect_password" → fails at position 1 → slightly slower
"correct_password" !== "correct_password" → matches fully → slowest
La mayoría de los lenguajes de programación comparan cadenas carácter por carácter y se detienen en cuanto encuentran una discrepancia (salida anticipada). Esto significa:
La solución es utilizar una función de comparación en tiempo constante que siempre inspeccione todos los bytes, independientemente de dónde se produzca una discrepancia.
La vulnerabilidad se descubrió mediante revisión manual de código de la lógica de autenticación de Trilium. El investigador examinó el flujo de inicio de sesión de sincronización en apps/server/src/routes/api/login.ts y observó el siguiente patrón en la función loginSync() (alrededor de la línea 111):
const documentSecret = options.getOption("documentSecret");
const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash;
if (expectedHash !== givenHash) { // ← VULNERABLE LINE
return [400, { message: "Sync login credentials are incorrect..." }];
}
La señal de alerta es el uso del operador !== incorporado de JavaScript para comparar hashes HMAC. El operador !== no es de tiempo constante: sale en cuanto encuentra un carácter diferente. Dado que la comparación se realiza sobre cadenas de texto simples (sin utilizar una función de comparación criptográficamente segura), el tiempo de respuesta filtra información sobre cuántos bytes iniciales de la suposición del atacante son correctos.
El investigador se preguntó entonces:
"¿Puede esta pequeña diferencia de temporización amplificarse lo suficiente, a través de una red, como para recuperar el hash HMAC completo de 44 caracteres codificado en Base64?"
La respuesta resultó ser sí — con suficientes mediciones repetidas y algo de análisis estadístico, la señal supera al ruido.
Cuando un cliente Trilium quiere sincronizar, llama a POST /api/login/sync con un cuerpo JSON como:
{
"timestamp": "2025-12-19T10:00:00.000Z",
"syncVersion": 34,
"hash": "<HMAC-SHA256 of documentSecret + timestamp, Base64-encoded>"
}
La recuperación byte a byte funciona de la siguiente manera:
For position = 0 to 43:
For each candidate character c in charset (A-Z, a-z, 0-9, +, /, =):
Send SAMPLES requests with hash = known_prefix + c + padding
Record average response time
Best character = candidate with highest average time
Append best character to known_prefix
Tras 44 iteraciones (una por carácter Base64), se recupera el hash HMAC completo de 44 caracteres.
Requisitos prácticos:
time.perf_counter() en Python ofrece resolución de nanosegundos)Consulte poc.py para ver un PoC de Python totalmente comentado.
Resumen rápido de lo que hace el PoC:
A–Z, a–z, 0–9, +, /, =)./api/login/sync para cada candidato y mide el tiempo de respuesta medio.Aviso legal: Este PoC se proporciona únicamente con fines educativos y de investigación de seguridad responsable. No lo utilice contra sistemas que no le pertenezcan o para los que no tenga permiso explícito por escrito.
Los operadores !== (y ===) de JavaScript realizan una comparación lexicográfica con salida anticipada. La línea vulnerable en apps/server/src/routes/api/login.ts:
if (expectedHash !== givenHash) {
return [400, { message: "Sync login credentials are incorrect..." }];
}
El comportamiento de salida anticipada crea una diferencia de temporización medible por cada byte coincidente:
| Suposición vs. esperado | Bytes comparados | Tiempo |
|---|---|---|
| Byte 0 incorrecto | 1 | ~T |
| Byte 0 correcto, byte 1 incorrecto | 2 | ~T + δ |
| Bytes 0–1 correctos, byte 2 incorrecto | 3 | ~T + 2δ |
| … | … | … |
| Los 44 bytes correctos | 44 | ~T + 43δ |
Cada byte adicional coincidente cuesta una pequeña cantidad extra de tiempo de CPU δ. A lo largo de miles de muestras, el tiempo de respuesta medio para una suposición con el "byte N correcto" es mediblemente mayor que para una con el "byte N incorrecto", filtrando suficiente información para recuperar el hash HMAC completo carácter por carácter.
| Métrica | Valor | Razón |
|---|---|---|
| Puntuación base | 7.4 ALTA | |
| Vector de ataque | Red (N) | Explotable a través de internet |
| Complejidad de ataque | Alta (H) | Requiere muchas solicitudes + temporización estable |
| Privilegios requeridos | Ninguno (N) | No se necesita cuenta |
| Interacción del usuario | Ninguna (N) | La víctima no necesita hacer nada |
| Alcance | Sin cambios (U) | Solo se ve afectado el servidor Trilium |
| Confidencialidad | Alta (H) | Toda la base de notas es legible |
| Integridad | Alta (H) | El atacante puede escribir/modificar notas |
| Disponibilidad | Ninguna (N) | Sin componente de denegación de servicio |