ctf-agent 1.2.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.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# CTFd Platform Navigation (No Browser)
|
|
2
|
+
|
|
3
|
+
Programmatic interaction with CTFd-based CTF platforms via REST API. Eliminates browser dependency during competitions.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Detect CTFd](#detect-ctfd)
|
|
8
|
+
- [Authentication](#authentication)
|
|
9
|
+
- [List Challenges](#list-challenges)
|
|
10
|
+
- [Challenge Details](#challenge-details)
|
|
11
|
+
- [Download Challenge Files](#download-challenge-files)
|
|
12
|
+
- [Submit Flags](#submit-flags)
|
|
13
|
+
- [Scoreboard](#scoreboard)
|
|
14
|
+
- [Hints and Unlocks](#hints-and-unlocks)
|
|
15
|
+
- [Notifications](#notifications)
|
|
16
|
+
- [User and Team Info](#user-and-team-info)
|
|
17
|
+
- [Full Competition Workflow](#full-competition-workflow)
|
|
18
|
+
- [Python CTFd Client](#python-ctfd-client)
|
|
19
|
+
- [Troubleshooting](#troubleshooting)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Detect CTFd
|
|
24
|
+
|
|
25
|
+
CTFd fingerprints in HTTP responses:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Check for CTFd signatures in response headers and body
|
|
29
|
+
curl -sI "$CTF_URL" | grep -i 'ctfd\|powered-by'
|
|
30
|
+
|
|
31
|
+
# Check for CTFd API endpoint (returns Swagger UI or JSON)
|
|
32
|
+
curl -s "$CTF_URL/api/v1/" | head -20
|
|
33
|
+
|
|
34
|
+
# Check for CTFd static assets
|
|
35
|
+
curl -s "$CTF_URL" | grep -oE '(ctfd|CTFd|/themes/core)'
|
|
36
|
+
|
|
37
|
+
# Check for CTFd login page structure
|
|
38
|
+
curl -s "$CTF_URL/login" | grep -oE 'name="nonce"'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key indicators:**
|
|
42
|
+
- `/api/v1/` returns Swagger/RESTX documentation
|
|
43
|
+
- HTML contains `/themes/core/` asset paths
|
|
44
|
+
- Login form includes a `nonce` hidden field
|
|
45
|
+
- Response headers may include `CTFd` in `Server` or `X-Powered-By`
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
CTFd supports two auth methods: session cookies (login flow) and API tokens (recommended).
|
|
52
|
+
|
|
53
|
+
**Important:** When CTFd is detected, **ask the user for their API token**. Tokens are not provided by default — the user must generate one from the CTFd web UI (Settings > Access Tokens) before API access works. If the user doesn't have a token yet, guide them: log in to CTFd in a browser, go to Settings > Access Tokens, create a token, and paste it back.
|
|
54
|
+
|
|
55
|
+
### Method 1: API Token (Recommended)
|
|
56
|
+
|
|
57
|
+
Generate a token from the CTFd web UI (Settings > Access Tokens), or if you already have session cookies:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Generate token via API (requires session auth first)
|
|
61
|
+
curl -s -X POST "$CTF_URL/api/v1/tokens" \
|
|
62
|
+
-H "Content-Type: application/json" \
|
|
63
|
+
-b cookies.txt \
|
|
64
|
+
-d '{"expiration": "2026-12-31", "description": "CLI access"}' | jq .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use the token for all subsequent requests:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export CTF_URL="https://ctf.example.com"
|
|
71
|
+
export CTF_TOKEN="ctfd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
72
|
+
|
|
73
|
+
# Test authentication
|
|
74
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me" | jq .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Method 2: Session Login (Cookie-Based)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Step 1: Get CSRF nonce from login page
|
|
81
|
+
NONCE=$(curl -sc cookies.txt "$CTF_URL/login" | grep 'name="nonce"' | grep -oE 'value="[^"]*"' | cut -d'"' -f2)
|
|
82
|
+
|
|
83
|
+
# Step 2: Login with credentials
|
|
84
|
+
curl -sb cookies.txt -c cookies.txt -X POST "$CTF_URL/login" \
|
|
85
|
+
-d "name=username&password=password&nonce=$NONCE" \
|
|
86
|
+
-L -o /dev/null -w '%{http_code}'
|
|
87
|
+
|
|
88
|
+
# Step 3: Use cookies for API calls
|
|
89
|
+
curl -s -b cookies.txt "$CTF_URL/api/v1/users/me" | jq .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Key insight:** The nonce is a CSRF token required for form-based login. API token auth bypasses this entirely — always prefer tokens when available.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## List Challenges
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# All visible challenges
|
|
100
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | jq .
|
|
101
|
+
|
|
102
|
+
# Filter by category
|
|
103
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges?category=web" | jq .
|
|
104
|
+
|
|
105
|
+
# Compact listing: id, name, category, value, solves
|
|
106
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | \
|
|
107
|
+
jq -r '.data[] | "\(.id)\t\(.value)pts\t\(.category)\t\(.name)\t(\(.solves) solves)"' | \
|
|
108
|
+
sort -t$'\t' -k3,3 -k2,2rn | column -t -s$'\t'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Response structure:**
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"success": true,
|
|
115
|
+
"data": [
|
|
116
|
+
{
|
|
117
|
+
"id": 1,
|
|
118
|
+
"type": "standard",
|
|
119
|
+
"name": "Challenge Name",
|
|
120
|
+
"value": 100,
|
|
121
|
+
"solves": 42,
|
|
122
|
+
"solved_by_me": false,
|
|
123
|
+
"category": "web",
|
|
124
|
+
"tags": [],
|
|
125
|
+
"template": "...",
|
|
126
|
+
"script": "..."
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Challenge Details
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Full challenge details (description, files, hints, tags)
|
|
138
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | jq .
|
|
139
|
+
|
|
140
|
+
# Extract just the description (HTML)
|
|
141
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
|
|
142
|
+
jq -r '.data.description'
|
|
143
|
+
|
|
144
|
+
# Strip HTML tags for readable description
|
|
145
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
|
|
146
|
+
jq -r '.data.description' | sed 's/<[^>]*>//g'
|
|
147
|
+
|
|
148
|
+
# List files attached to challenge
|
|
149
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
|
|
150
|
+
jq -r '.data.files[]'
|
|
151
|
+
|
|
152
|
+
# Get connection info (if present in description)
|
|
153
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
|
|
154
|
+
jq -r '.data.description' | grep -oE '(nc |ssh |https?://)[^ <"]+' | head -5
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Download Challenge Files
|
|
160
|
+
|
|
161
|
+
CTFd serves files with token-signed URLs. Extract them from challenge details and download:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Get file URLs from challenge
|
|
165
|
+
FILES=$(curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
|
|
166
|
+
jq -r '.data.files[]')
|
|
167
|
+
|
|
168
|
+
# Download all challenge files
|
|
169
|
+
mkdir -p "chall_$CHALL_ID"
|
|
170
|
+
for f in $FILES; do
|
|
171
|
+
# File paths are relative — prepend base URL
|
|
172
|
+
URL="${CTF_URL}${f}"
|
|
173
|
+
FILENAME=$(basename "$f" | sed 's/?.*//')
|
|
174
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" -o "chall_$CHALL_ID/$FILENAME" "$URL"
|
|
175
|
+
echo "Downloaded: $FILENAME"
|
|
176
|
+
done
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Key insight:** File URLs include a query-string token (`?token=...`) that authenticates the download. The token is time-limited — re-fetch the challenge details if downloads return 403.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Submit Flags
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Submit a flag
|
|
187
|
+
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" \
|
|
188
|
+
-H "Content-Type: application/json" \
|
|
189
|
+
"$CTF_URL/api/v1/challenges/attempt" \
|
|
190
|
+
-d "{\"challenge_id\": $CHALL_ID, \"submission\": \"flag{example}\"}" | jq .
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Response statuses:**
|
|
194
|
+
| Status | Meaning |
|
|
195
|
+
|--------|---------|
|
|
196
|
+
| `correct` | Flag accepted |
|
|
197
|
+
| `incorrect` | Wrong flag |
|
|
198
|
+
| `already_solved` | Previously solved by you/team |
|
|
199
|
+
| `ratelimited` | Too many attempts (default: 10/min) |
|
|
200
|
+
| `paused` | CTF is paused |
|
|
201
|
+
|
|
202
|
+
**Key insight:** Rate limit is 10 incorrect submissions per minute per user. Space out brute-force attempts or you get locked out temporarily.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Scoreboard
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Full scoreboard
|
|
210
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/scoreboard" | jq .
|
|
211
|
+
|
|
212
|
+
# Top 10
|
|
213
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/scoreboard/top/10" | jq .
|
|
214
|
+
|
|
215
|
+
# Compact scoreboard
|
|
216
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/scoreboard/top/20" | \
|
|
217
|
+
jq -r '.data | to_entries[] | "\(.value.pos)\t\(.value.name)\t\(.value.score)pts"' | \
|
|
218
|
+
column -t -s$'\t'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Note:** Scoreboard is cached server-side for 60 seconds.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Hints and Unlocks
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# List hints for a challenge (from challenge details)
|
|
229
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CHALL_ID" | \
|
|
230
|
+
jq '.data.hints'
|
|
231
|
+
|
|
232
|
+
# Get hint content (if free or already unlocked)
|
|
233
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/hints/$HINT_ID" | jq .
|
|
234
|
+
|
|
235
|
+
# Unlock a paid hint (costs points)
|
|
236
|
+
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" \
|
|
237
|
+
-H "Content-Type: application/json" \
|
|
238
|
+
"$CTF_URL/api/v1/unlocks" \
|
|
239
|
+
-d "{\"target\": $HINT_ID, \"type\": \"hints\"}" | jq .
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Notifications
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
# Get all notifications (announcements from organizers)
|
|
248
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/notifications" | jq .
|
|
249
|
+
|
|
250
|
+
# Get notification count (HEAD request)
|
|
251
|
+
curl -sI -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/notifications" | \
|
|
252
|
+
grep -i 'x-total'
|
|
253
|
+
|
|
254
|
+
# Poll for new notifications since last seen
|
|
255
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" \
|
|
256
|
+
"$CTF_URL/api/v1/notifications?since_id=$LAST_ID" | jq .
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## User and Team Info
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Current user profile
|
|
265
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me" | jq .
|
|
266
|
+
|
|
267
|
+
# My solves
|
|
268
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me/solves" | \
|
|
269
|
+
jq -r '.data[] | "\(.challenge.name)\t\(.challenge.value)pts\t\(.date)"'
|
|
270
|
+
|
|
271
|
+
# My failed attempts
|
|
272
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me/fails" | jq .
|
|
273
|
+
|
|
274
|
+
# Current team (teams mode)
|
|
275
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/teams/me" | jq .
|
|
276
|
+
|
|
277
|
+
# Team solves
|
|
278
|
+
TEAM_ID=$(curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/teams/me" | jq '.data.id')
|
|
279
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/teams/$TEAM_ID/solves" | jq .
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Full Competition Workflow
|
|
285
|
+
|
|
286
|
+
End-to-end CTFd interaction from the terminal:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
#!/usr/bin/env bash
|
|
290
|
+
# CTFd CLI workflow — set these two variables and go
|
|
291
|
+
export CTF_URL="https://ctf.example.com"
|
|
292
|
+
export CTF_TOKEN="ctfd_your_token_here"
|
|
293
|
+
AUTH="-H 'Authorization: Token $CTF_TOKEN'"
|
|
294
|
+
|
|
295
|
+
# 1. Verify auth
|
|
296
|
+
echo "=== Logged in as ==="
|
|
297
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/users/me" | jq -r '.data | "\(.name) (id: \(.id))"'
|
|
298
|
+
|
|
299
|
+
# 2. List all challenges grouped by category
|
|
300
|
+
echo -e "\n=== Challenges ==="
|
|
301
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges" | \
|
|
302
|
+
jq -r '.data | sort_by(.category, -.value) | .[] |
|
|
303
|
+
"\(.solved_by_me | if . then "[X]" else " " end) \(.id)\t\(.value)pts\t\(.category)\t\(.name)"' | \
|
|
304
|
+
column -t -s$'\t'
|
|
305
|
+
|
|
306
|
+
# 3. Read a specific challenge
|
|
307
|
+
read -p "Challenge ID: " CID
|
|
308
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CID" | \
|
|
309
|
+
jq -r '.data | "Name: \(.name)\nCategory: \(.category)\nValue: \(.value)\nSolves: \(.solves)\n\nDescription:\n\(.description)"' | \
|
|
310
|
+
sed 's/<[^>]*>//g'
|
|
311
|
+
|
|
312
|
+
# 4. Download files
|
|
313
|
+
mkdir -p "chall_$CID"
|
|
314
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" "$CTF_URL/api/v1/challenges/$CID" | \
|
|
315
|
+
jq -r '.data.files[]' | while read -r f; do
|
|
316
|
+
curl -s -H "Authorization: Token $CTF_TOKEN" -o "chall_$CID/$(basename "$f" | sed 's/?.*//')" "${CTF_URL}${f}"
|
|
317
|
+
done
|
|
318
|
+
echo "Files downloaded to chall_$CID/"
|
|
319
|
+
|
|
320
|
+
# 5. Submit flag
|
|
321
|
+
read -p "Flag: " FLAG
|
|
322
|
+
curl -s -X POST -H "Authorization: Token $CTF_TOKEN" \
|
|
323
|
+
-H "Content-Type: application/json" \
|
|
324
|
+
"$CTF_URL/api/v1/challenges/attempt" \
|
|
325
|
+
-d "{\"challenge_id\": $CID, \"submission\": \"$FLAG\"}" | \
|
|
326
|
+
jq -r '.data | "\(.status): \(.message)"'
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Python CTFd Client
|
|
332
|
+
|
|
333
|
+
Reusable class for scripted interaction:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
import requests
|
|
337
|
+
import os
|
|
338
|
+
import re
|
|
339
|
+
from pathlib import Path
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class CTFdClient:
|
|
343
|
+
"""Minimal CTFd API client for competition use."""
|
|
344
|
+
|
|
345
|
+
def __init__(self, url, token):
|
|
346
|
+
self.url = url.rstrip('/')
|
|
347
|
+
self.s = requests.Session()
|
|
348
|
+
self.s.headers['Authorization'] = f'Token {token}'
|
|
349
|
+
|
|
350
|
+
def _get(self, path, **kwargs):
|
|
351
|
+
r = self.s.get(f'{self.url}/api/v1{path}', **kwargs)
|
|
352
|
+
r.raise_for_status()
|
|
353
|
+
return r.json()
|
|
354
|
+
|
|
355
|
+
def _post(self, path, json=None):
|
|
356
|
+
r = self.s.post(f'{self.url}/api/v1{path}', json=json)
|
|
357
|
+
r.raise_for_status()
|
|
358
|
+
return r.json()
|
|
359
|
+
|
|
360
|
+
# --- Challenges ---
|
|
361
|
+
|
|
362
|
+
def challenges(self, category=None):
|
|
363
|
+
"""List all visible challenges."""
|
|
364
|
+
params = {'category': category} if category else {}
|
|
365
|
+
return self._get('/challenges', params=params)['data']
|
|
366
|
+
|
|
367
|
+
def challenge(self, cid):
|
|
368
|
+
"""Get full challenge details."""
|
|
369
|
+
return self._get(f'/challenges/{cid}')['data']
|
|
370
|
+
|
|
371
|
+
def unsolved(self):
|
|
372
|
+
"""List challenges not yet solved by current user."""
|
|
373
|
+
return [c for c in self.challenges() if not c.get('solved_by_me')]
|
|
374
|
+
|
|
375
|
+
# --- Files ---
|
|
376
|
+
|
|
377
|
+
def download_files(self, cid, dest='.'):
|
|
378
|
+
"""Download all files for a challenge."""
|
|
379
|
+
info = self.challenge(cid)
|
|
380
|
+
dest = Path(dest)
|
|
381
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
paths = []
|
|
383
|
+
for f in info.get('files', []):
|
|
384
|
+
url = f'{self.url}{f}' if f.startswith('/') else f
|
|
385
|
+
fname = re.sub(r'\?.*', '', f.split('/')[-1])
|
|
386
|
+
out = dest / fname
|
|
387
|
+
r = self.s.get(url)
|
|
388
|
+
r.raise_for_status()
|
|
389
|
+
out.write_bytes(r.content)
|
|
390
|
+
paths.append(str(out))
|
|
391
|
+
return paths
|
|
392
|
+
|
|
393
|
+
# --- Flag Submission ---
|
|
394
|
+
|
|
395
|
+
def submit(self, cid, flag):
|
|
396
|
+
"""Submit a flag. Returns (status, message)."""
|
|
397
|
+
resp = self._post('/challenges/attempt',
|
|
398
|
+
json={'challenge_id': cid, 'submission': flag})
|
|
399
|
+
d = resp['data']
|
|
400
|
+
return d['status'], d['message']
|
|
401
|
+
|
|
402
|
+
# --- Scoreboard ---
|
|
403
|
+
|
|
404
|
+
def scoreboard(self, top=10):
|
|
405
|
+
"""Get top N scoreboard entries."""
|
|
406
|
+
return self._get(f'/scoreboard/top/{top}')['data']
|
|
407
|
+
|
|
408
|
+
# --- User/Team ---
|
|
409
|
+
|
|
410
|
+
def me(self):
|
|
411
|
+
"""Current user info."""
|
|
412
|
+
return self._get('/users/me')['data']
|
|
413
|
+
|
|
414
|
+
def my_solves(self):
|
|
415
|
+
"""Challenges solved by current user."""
|
|
416
|
+
return self._get('/users/me/solves')['data']
|
|
417
|
+
|
|
418
|
+
# --- Hints ---
|
|
419
|
+
|
|
420
|
+
def hint(self, hint_id):
|
|
421
|
+
"""Get hint content (if unlocked or free)."""
|
|
422
|
+
return self._get(f'/hints/{hint_id}')['data']
|
|
423
|
+
|
|
424
|
+
def unlock_hint(self, hint_id):
|
|
425
|
+
"""Unlock a hint (costs points)."""
|
|
426
|
+
return self._post('/unlocks', json={'target': hint_id, 'type': 'hints'})
|
|
427
|
+
|
|
428
|
+
# --- Notifications ---
|
|
429
|
+
|
|
430
|
+
def notifications(self, since_id=None):
|
|
431
|
+
"""Get announcements. Optionally filter since a notification ID."""
|
|
432
|
+
params = {'since_id': since_id} if since_id else {}
|
|
433
|
+
return self._get('/notifications', params=params)['data']
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# --- Usage ---
|
|
437
|
+
|
|
438
|
+
if __name__ == '__main__':
|
|
439
|
+
c = CTFdClient(os.environ['CTF_URL'], os.environ['CTF_TOKEN'])
|
|
440
|
+
|
|
441
|
+
# Dashboard
|
|
442
|
+
print(f"Logged in as: {c.me()['name']}")
|
|
443
|
+
print(f"\nUnsolved challenges:")
|
|
444
|
+
for ch in c.unsolved():
|
|
445
|
+
print(f" [{ch['id']}] {ch['category']}/{ch['name']} ({ch['value']}pts, {ch['solves']} solves)")
|
|
446
|
+
|
|
447
|
+
# Download and submit workflow
|
|
448
|
+
# files = c.download_files(1, dest='chall_1')
|
|
449
|
+
# status, msg = c.submit(1, 'flag{...}')
|
|
450
|
+
# print(f"{status}: {msg}")
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Troubleshooting
|
|
456
|
+
|
|
457
|
+
| Symptom | Cause | Fix |
|
|
458
|
+
|---------|-------|-----|
|
|
459
|
+
| 401 Unauthorized | Token expired or invalid | Re-generate token via web UI or session login |
|
|
460
|
+
| 403 on file download | File token expired | Re-fetch challenge details to get fresh file URLs |
|
|
461
|
+
| 403 on challenges | CTF not started or email unverified | Check `/api/v1/users/me` for `verified` field |
|
|
462
|
+
| 429 Rate Limited | Too many wrong flag submissions | Wait 60 seconds; default is 10 incorrect/min |
|
|
463
|
+
| Empty challenge list | CTF hasn't started | Check CTF start time in notifications or config |
|
|
464
|
+
| `nonce` missing | Login page changed or anti-bot | Try API token auth instead of session login |
|
|
465
|
+
| Connection info not in API | Some CTFs use dynamic instances | Check for challenge-specific instance API or Docker endpoints |
|