icoa-cli 2.1.2 → 2.1.4
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.
- package/dist/commands/ctf.js +15 -7
- package/dist/index.js +1 -1
- package/dist/lib/ctfd-client.d.ts +10 -2
- package/dist/lib/ctfd-client.js +77 -31
- package/dist/repl.js +5 -6
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
- package/refs/hint.txt +8 -2
- package/refs/icoa.txt +55 -33
package/dist/commands/ctf.js
CHANGED
|
@@ -7,11 +7,13 @@ import { getTranslation } from '../lib/translation.js';
|
|
|
7
7
|
import { printSuccess, printError, printWarning, printInfo, printTable, printMarkdown, printHeader, printKeyValue, createSpinner, formatCountdown, } from '../lib/ui.js';
|
|
8
8
|
function requireConnection() {
|
|
9
9
|
const config = getConfig();
|
|
10
|
-
if (!config.ctfdUrl || !config.token) {
|
|
11
|
-
printError('Not connected to CTFd. Run:
|
|
10
|
+
if (!config.ctfdUrl || (!config.token && !config.sessionCookie)) {
|
|
11
|
+
printError('Not connected to CTFd. Run: join <url>');
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
const session = config.sessionCookie || '';
|
|
15
|
+
const token = config.token && !config.token.includes('session=') ? config.token : '';
|
|
16
|
+
return { config, client: new CTFdClient(config.ctfdUrl, token, session || config.token) };
|
|
15
17
|
}
|
|
16
18
|
export function registerCtfCommands(program) {
|
|
17
19
|
const ctf = program.command('ctf').description('Competition commands');
|
|
@@ -30,7 +32,9 @@ export function registerCtfCommands(program) {
|
|
|
30
32
|
{ name: 'Username & Password', value: 'login' },
|
|
31
33
|
],
|
|
32
34
|
});
|
|
33
|
-
let token;
|
|
35
|
+
let token = '';
|
|
36
|
+
let sessionCookie = '';
|
|
37
|
+
let csrfNonce = '';
|
|
34
38
|
if (authMethod === 'token') {
|
|
35
39
|
token = await input({ message: 'Enter your CTFd Access Token:' });
|
|
36
40
|
}
|
|
@@ -41,7 +45,10 @@ export function registerCtfCommands(program) {
|
|
|
41
45
|
spinner.start();
|
|
42
46
|
try {
|
|
43
47
|
const client = new CTFdClient(url, '');
|
|
44
|
-
|
|
48
|
+
const result = await client.loginWithCredentials(username, password);
|
|
49
|
+
token = result.token;
|
|
50
|
+
sessionCookie = result.session;
|
|
51
|
+
csrfNonce = result.csrf;
|
|
45
52
|
spinner.succeed('Login successful');
|
|
46
53
|
}
|
|
47
54
|
catch (err) {
|
|
@@ -54,7 +61,7 @@ export function registerCtfCommands(program) {
|
|
|
54
61
|
const spinner = createSpinner('Testing connection...');
|
|
55
62
|
spinner.start();
|
|
56
63
|
try {
|
|
57
|
-
const client = new CTFdClient(url, token);
|
|
64
|
+
const client = new CTFdClient(url, token, sessionCookie, csrfNonce);
|
|
58
65
|
const user = await client.testConnection();
|
|
59
66
|
spinner.succeed('Connected successfully');
|
|
60
67
|
console.log();
|
|
@@ -68,10 +75,11 @@ export function registerCtfCommands(program) {
|
|
|
68
75
|
const meta = await client.getCompetitionMeta();
|
|
69
76
|
const configUpdate = {
|
|
70
77
|
ctfdUrl: url,
|
|
71
|
-
token: token,
|
|
78
|
+
token: token || sessionCookie,
|
|
72
79
|
userId: user.id,
|
|
73
80
|
userName: user.name,
|
|
74
81
|
teamId: user.team_id,
|
|
82
|
+
sessionCookie: sessionCookie,
|
|
75
83
|
};
|
|
76
84
|
if (meta.start) {
|
|
77
85
|
configUpdate.competitionStartsAt = new Date(meta.start * 1000).toISOString();
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ ${LINE}
|
|
|
36
36
|
${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
|
|
37
37
|
${chalk.cyan.underline('https://icoa2026.au')}
|
|
38
38
|
|
|
39
|
-
${chalk.gray('CLI-Native Competition Terminal v2.1.
|
|
39
|
+
${chalk.gray('CLI-Native Competition Terminal v2.1.4')}
|
|
40
40
|
|
|
41
41
|
${LINE}
|
|
42
42
|
`;
|
|
@@ -2,7 +2,11 @@ import type { CTFdChallenge, CTFdChallengeListItem, CTFdUser, CTFdTeam, CTFdScor
|
|
|
2
2
|
export declare class CTFdClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private token;
|
|
5
|
-
|
|
5
|
+
private sessionCookie;
|
|
6
|
+
private csrfNonce;
|
|
7
|
+
constructor(baseUrl: string, token: string, sessionCookie?: string, csrfNonce?: string);
|
|
8
|
+
private getAuthHeaders;
|
|
9
|
+
fetchCsrfNonce(): Promise<string>;
|
|
6
10
|
private request;
|
|
7
11
|
testConnection(): Promise<CTFdUser>;
|
|
8
12
|
getChallenges(): Promise<CTFdChallengeListItem[]>;
|
|
@@ -18,5 +22,9 @@ export declare class CTFdClient {
|
|
|
18
22
|
}>;
|
|
19
23
|
getChallengeFiles(id: number): Promise<string[]>;
|
|
20
24
|
downloadFile(filePath: string, destDir: string): Promise<string>;
|
|
21
|
-
loginWithCredentials(username: string, password: string): Promise<
|
|
25
|
+
loginWithCredentials(username: string, password: string): Promise<{
|
|
26
|
+
token: string;
|
|
27
|
+
session: string;
|
|
28
|
+
csrf: string;
|
|
29
|
+
}>;
|
|
22
30
|
}
|
package/dist/lib/ctfd-client.js
CHANGED
|
@@ -5,14 +5,52 @@ import { Readable } from 'node:stream';
|
|
|
5
5
|
export class CTFdClient {
|
|
6
6
|
baseUrl;
|
|
7
7
|
token;
|
|
8
|
-
|
|
8
|
+
sessionCookie;
|
|
9
|
+
csrfNonce;
|
|
10
|
+
constructor(baseUrl, token, sessionCookie, csrfNonce) {
|
|
9
11
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
10
12
|
this.token = token;
|
|
13
|
+
this.sessionCookie = sessionCookie || '';
|
|
14
|
+
this.csrfNonce = csrfNonce || '';
|
|
15
|
+
}
|
|
16
|
+
getAuthHeaders() {
|
|
17
|
+
if (this.token) {
|
|
18
|
+
return { Authorization: `Token ${this.token}` };
|
|
19
|
+
}
|
|
20
|
+
if (this.sessionCookie) {
|
|
21
|
+
const headers = { Cookie: this.sessionCookie };
|
|
22
|
+
if (this.csrfNonce) {
|
|
23
|
+
headers['CSRF-Token'] = this.csrfNonce;
|
|
24
|
+
}
|
|
25
|
+
return headers;
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
async fetchCsrfNonce() {
|
|
30
|
+
if (this.csrfNonce)
|
|
31
|
+
return this.csrfNonce;
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(this.baseUrl, {
|
|
34
|
+
headers: this.sessionCookie ? { Cookie: this.sessionCookie } : {},
|
|
35
|
+
});
|
|
36
|
+
const html = await res.text();
|
|
37
|
+
const match = html.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
38
|
+
if (match) {
|
|
39
|
+
this.csrfNonce = match[1];
|
|
40
|
+
return this.csrfNonce;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { /* ignore */ }
|
|
44
|
+
return '';
|
|
11
45
|
}
|
|
12
46
|
async request(method, path, body) {
|
|
47
|
+
// Auto-fetch CSRF nonce for session-based auth
|
|
48
|
+
if (this.sessionCookie && !this.csrfNonce) {
|
|
49
|
+
await this.fetchCsrfNonce();
|
|
50
|
+
}
|
|
13
51
|
const url = `${this.baseUrl}/api/v1${path}`;
|
|
14
52
|
const headers = {
|
|
15
|
-
|
|
53
|
+
...this.getAuthHeaders(),
|
|
16
54
|
'Content-Type': 'application/json',
|
|
17
55
|
};
|
|
18
56
|
const res = await fetch(url, {
|
|
@@ -43,7 +81,7 @@ export class CTFdClient {
|
|
|
43
81
|
const res = await fetch(`${this.baseUrl}/api/v1/challenges/attempt`, {
|
|
44
82
|
method: 'POST',
|
|
45
83
|
headers: {
|
|
46
|
-
|
|
84
|
+
...this.getAuthHeaders(),
|
|
47
85
|
'Content-Type': 'application/json',
|
|
48
86
|
},
|
|
49
87
|
body: JSON.stringify({ challenge_id: challengeId, submission }),
|
|
@@ -85,13 +123,12 @@ export class CTFdClient {
|
|
|
85
123
|
? filePath
|
|
86
124
|
: `${this.baseUrl}/${filePath.replace(/^\//, '')}`;
|
|
87
125
|
const res = await fetch(url, {
|
|
88
|
-
headers:
|
|
126
|
+
headers: this.getAuthHeaders(),
|
|
89
127
|
redirect: 'follow',
|
|
90
128
|
});
|
|
91
129
|
if (!res.ok || !res.body) {
|
|
92
130
|
throw new Error(`Failed to download: ${url}`);
|
|
93
131
|
}
|
|
94
|
-
// Extract clean filename (strip query params like ?token=xxx)
|
|
95
132
|
const rawName = filePath.split('/').pop() || 'file';
|
|
96
133
|
const fileName = rawName.split('?')[0];
|
|
97
134
|
const destPath = join(destDir, fileName);
|
|
@@ -103,7 +140,6 @@ export class CTFdClient {
|
|
|
103
140
|
// Step 1: GET /login to get nonce
|
|
104
141
|
const loginPageRes = await fetch(`${this.baseUrl}/login`);
|
|
105
142
|
const loginHtml = await loginPageRes.text();
|
|
106
|
-
// Try multiple nonce patterns (CTFd versions differ)
|
|
107
143
|
const nonceMatch = loginHtml.match(/name="nonce"[^>]*value="([^"]+)"/) ||
|
|
108
144
|
loginHtml.match(/value="([^"]+)"[^>]*name="nonce"/) ||
|
|
109
145
|
loginHtml.match(/id="nonce"[^>]*value="([^"]+)"/) ||
|
|
@@ -112,7 +148,6 @@ export class CTFdClient {
|
|
|
112
148
|
if (!nonce) {
|
|
113
149
|
throw new Error('Could not extract CSRF nonce from login page.');
|
|
114
150
|
}
|
|
115
|
-
// Extract cookies
|
|
116
151
|
const cookies = loginPageRes.headers.getSetCookie?.() || [];
|
|
117
152
|
const cookieStr = cookies.map((c) => c.split(';')[0]).join('; ');
|
|
118
153
|
// Step 2: POST /login with credentials
|
|
@@ -127,35 +162,46 @@ export class CTFdClient {
|
|
|
127
162
|
});
|
|
128
163
|
const loginCookies = loginRes.headers.getSetCookie?.() || [];
|
|
129
164
|
const allCookies = [...cookies, ...loginCookies].map((c) => c.split(';')[0]).join('; ');
|
|
130
|
-
// Check if login was successful (redirect to /challenges or /, not back to /login)
|
|
131
165
|
const location = loginRes.headers.get('location') || '';
|
|
132
166
|
if (location.includes('/login')) {
|
|
133
167
|
throw new Error('Invalid username or password.');
|
|
134
168
|
}
|
|
135
|
-
// Step 3:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
// Step 3: Try to generate API token (some CTFd instances allow it)
|
|
170
|
+
try {
|
|
171
|
+
const settingsRes = await fetch(`${this.baseUrl}/settings`, {
|
|
172
|
+
headers: { Cookie: allCookies },
|
|
173
|
+
});
|
|
174
|
+
const settingsHtml = await settingsRes.text();
|
|
175
|
+
const csrfMatch = settingsHtml.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
176
|
+
const csrfNonce = csrfMatch?.[1] || nonce;
|
|
177
|
+
const tokenRes = await fetch(`${this.baseUrl}/api/v1/tokens`, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: {
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
Cookie: allCookies,
|
|
182
|
+
'CSRF-Token': csrfNonce,
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({ expiration: '2026-12-31T23:59:59+00:00' }),
|
|
185
|
+
});
|
|
186
|
+
if (tokenRes.ok) {
|
|
187
|
+
const tokenJson = (await tokenRes.json());
|
|
188
|
+
if (tokenJson.success && tokenJson.data?.value) {
|
|
189
|
+
return { token: tokenJson.data.value, session: '', csrf: '' };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
154
192
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
193
|
+
catch { /* Token generation not available, use session */ }
|
|
194
|
+
// Fallback: use session cookie + CSRF nonce
|
|
195
|
+
// Get CSRF nonce from the main page
|
|
196
|
+
let csrf = '';
|
|
197
|
+
try {
|
|
198
|
+
const mainRes = await fetch(`${this.baseUrl}/challenges`, { headers: { Cookie: allCookies } });
|
|
199
|
+
const mainHtml = await mainRes.text();
|
|
200
|
+
const csrfMatch = mainHtml.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
201
|
+
if (csrfMatch)
|
|
202
|
+
csrf = csrfMatch[1];
|
|
158
203
|
}
|
|
159
|
-
|
|
204
|
+
catch { /* ignore */ }
|
|
205
|
+
return { token: '', session: allCookies, csrf };
|
|
160
206
|
}
|
|
161
207
|
}
|
package/dist/repl.js
CHANGED
|
@@ -6,7 +6,6 @@ import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, r
|
|
|
6
6
|
import { resetTerminalTheme } from './lib/theme.js';
|
|
7
7
|
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
8
8
|
import { logCommand } from './lib/logger.js';
|
|
9
|
-
import { startLogSync, stopLogSync } from './lib/log-sync.js';
|
|
10
9
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
11
|
import { homedir } from 'node:os';
|
|
@@ -27,7 +26,7 @@ const BLOCKED_COMMANDS = new Set([
|
|
|
27
26
|
'iptables', 'ufw', // firewall
|
|
28
27
|
]);
|
|
29
28
|
const INTERCEPT = '__REPL_NO_EXIT__';
|
|
30
|
-
const VERSION = '2.1.
|
|
29
|
+
const VERSION = '2.1.4';
|
|
31
30
|
export async function startRepl(program, resumeMode) {
|
|
32
31
|
const config = getConfig();
|
|
33
32
|
const connected = isConnected();
|
|
@@ -138,8 +137,8 @@ export async function startRepl(program, resumeMode) {
|
|
|
138
137
|
terminal: true,
|
|
139
138
|
});
|
|
140
139
|
let processing = false;
|
|
141
|
-
//
|
|
142
|
-
startLogSync();
|
|
140
|
+
// Log sync disabled until server audit endpoint is configured
|
|
141
|
+
// startLogSync();
|
|
143
142
|
rl.prompt();
|
|
144
143
|
rl.on('line', async (line) => {
|
|
145
144
|
if (processing)
|
|
@@ -153,7 +152,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
153
152
|
logCommand(input);
|
|
154
153
|
// Exit — record, reset terminal colors, and quit
|
|
155
154
|
if (input === 'exit' || input === 'quit' || input === 'q') {
|
|
156
|
-
stopLogSync();
|
|
155
|
+
// stopLogSync();
|
|
157
156
|
recordExit();
|
|
158
157
|
console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
|
|
159
158
|
resetTerminalTheme();
|
|
@@ -296,7 +295,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
296
295
|
rl.prompt();
|
|
297
296
|
});
|
|
298
297
|
rl.on('close', () => {
|
|
299
|
-
stopLogSync();
|
|
298
|
+
// stopLogSync();
|
|
300
299
|
recordExit();
|
|
301
300
|
resetTerminalTheme();
|
|
302
301
|
realExit(0);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -101,6 +101,7 @@ export interface IcoaConfig {
|
|
|
101
101
|
accessToken: string;
|
|
102
102
|
deviceFingerprint: string;
|
|
103
103
|
lastVersion: string;
|
|
104
|
+
sessionCookie: string;
|
|
104
105
|
}
|
|
105
106
|
export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
|
|
106
107
|
export type HintLevel = 'A' | 'B' | 'C';
|
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
package/refs/hint.txt
CHANGED
|
@@ -21,8 +21,14 @@ LEVELS
|
|
|
21
21
|
- No flags
|
|
22
22
|
- Requires confirmation before use
|
|
23
23
|
|
|
24
|
+
COMMANDS
|
|
25
|
+
hint <question> Level A hint
|
|
26
|
+
hint-b <question> Level B hint
|
|
27
|
+
hint-c <question> Level C hint (requires confirmation)
|
|
28
|
+
hint budget Check remaining budget
|
|
29
|
+
|
|
24
30
|
USAGE TIPS
|
|
25
|
-
1. Open a challenge first:
|
|
31
|
+
1. Open a challenge first: open <id>
|
|
26
32
|
This sets context for better hints.
|
|
27
33
|
|
|
28
34
|
2. Be specific in your question:
|
|
@@ -33,7 +39,7 @@ USAGE TIPS
|
|
|
33
39
|
3. Start with Level A — it's cheap (50 uses).
|
|
34
40
|
Only escalate to B/C when A isn't enough.
|
|
35
41
|
|
|
36
|
-
4. Check your budget:
|
|
42
|
+
4. Check your budget: hint budget
|
|
37
43
|
|
|
38
44
|
TOKEN CAP
|
|
39
45
|
All AI usage shares a 50,000 token cap.
|
package/refs/icoa.txt
CHANGED
|
@@ -1,36 +1,58 @@
|
|
|
1
|
-
ICOA CLI — Quick Reference
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
ICOA CLI v2 — Quick Reference
|
|
2
|
+
==============================
|
|
3
|
+
|
|
4
|
+
GETTING STARTED
|
|
5
|
+
activate <token> Unlock with your access token
|
|
6
|
+
join <url> Connect to CTFd competition
|
|
7
|
+
env Check tool environment (109 commands)
|
|
8
|
+
env setup One-click install all tools
|
|
9
|
+
help Show all commands
|
|
10
|
+
|
|
11
|
+
COMPETITION
|
|
12
|
+
challenges (ch) List all challenges
|
|
13
|
+
open <id> View challenge details
|
|
14
|
+
submit <id> <flag> Submit a flag
|
|
15
|
+
scoreboard (sb) View rankings
|
|
16
|
+
status Hint budget, score, rank, time
|
|
17
|
+
time Competition countdown
|
|
18
|
+
|
|
19
|
+
AI HINTS
|
|
20
|
+
hint <question> Level A — General guidance (50 uses)
|
|
21
|
+
hint-b <question> Level B — Deep analysis (10 uses)
|
|
22
|
+
hint-c <question> Level C — Critical assist (2 uses)
|
|
23
|
+
hint budget Show remaining budget
|
|
24
|
+
|
|
25
|
+
TOOLS
|
|
26
|
+
ref <topic> Quick reference (38 topics)
|
|
27
|
+
files <id> Download challenge files
|
|
28
|
+
connect <id> Connect to remote target
|
|
29
|
+
note <text> Personal notepad
|
|
30
|
+
log Session history
|
|
31
|
+
log stats Session statistics
|
|
32
|
+
log export Export audit trail (JSON)
|
|
33
|
+
|
|
34
|
+
SYSTEM COMMANDS
|
|
35
|
+
python solve.py Run Python (forced to 3.12.13)
|
|
36
|
+
nano solve.py Edit files
|
|
37
|
+
gcc -o pwn pwn.c Compile code
|
|
38
|
+
Any Linux command Runs in ~/icoa-workspace/
|
|
39
|
+
|
|
40
|
+
SETTINGS
|
|
41
|
+
setup Configure API keys
|
|
42
|
+
lang <code> Switch language (en/zh/ja/ko/es)
|
|
43
|
+
clear Clear screen
|
|
44
|
+
exit Save session & quit
|
|
45
|
+
|
|
46
|
+
SHORTCUTS
|
|
47
|
+
ch = challenges
|
|
48
|
+
sb = scoreboard
|
|
49
|
+
flag = submit
|
|
30
50
|
|
|
31
51
|
TIPS
|
|
32
|
-
- Use '
|
|
33
|
-
- Level A hints are cheap — use
|
|
52
|
+
- Use 'open <id>' before 'hint' to set challenge context
|
|
53
|
+
- Level A hints are cheap — use freely for direction
|
|
34
54
|
- Level C hints require confirmation — use wisely
|
|
35
|
-
- All
|
|
36
|
-
-
|
|
55
|
+
- All commands are logged for audit
|
|
56
|
+
- python/python3 always uses Python 3.12.13
|
|
57
|
+
- System commands run in ~/icoa-workspace/ (sandboxed)
|
|
58
|
+
- Use 'ref <topic>' for zero-cost tool references
|