enumendpoint 7.0__py3-none-any.whl
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.
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
enumerateendpoint.py,sha256=nC-ttsmtVskSPRV-kWSgyNc30hPoxLXuSxZgns22WCA,26564
|
|
2
|
+
enumendpoint-7.0.dist-info/licenses/LICENSE,sha256=7rttKh3T46zBKmFfudbz3gF0SL4JkvR0wz81Z3ixUz8,1077
|
|
3
|
+
enumendpoint-7.0.dist-info/METADATA,sha256=VTIFIUVN1LK5XyaeYU_Dm1d5IMK4t9gt8VtSc0UeD_8,213
|
|
4
|
+
enumendpoint-7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
enumendpoint-7.0.dist-info/entry_points.txt,sha256=90VwG7YhGtwI-x2FIB0e7wkQNFhelXDDgEBuvcMNC0Q,56
|
|
6
|
+
enumendpoint-7.0.dist-info/top_level.txt,sha256=W6rq8WOng3JdtjeyETLwwRWFFrcFYGeAvDCw6xywTMQ,18
|
|
7
|
+
enumendpoint-7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SphericalFlower52811
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
enumerateendpoint
|
enumerateendpoint.py
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from curl_cffi import requests
|
|
3
|
+
import re
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from urllib.parse import urljoin, urlparse
|
|
6
|
+
import argparse
|
|
7
|
+
import time
|
|
8
|
+
from playwright.sync_api import sync_playwright #headless browser to solve captcha
|
|
9
|
+
from playwright_stealth import Stealth #Ensure strict firewalls do not block the playwright browser
|
|
10
|
+
|
|
11
|
+
#Will be fixed in version 8
|
|
12
|
+
'''
|
|
13
|
+
def isthere_captcha(response):
|
|
14
|
+
#me when captcha
|
|
15
|
+
html_content = response.text
|
|
16
|
+
html_lower = html_content.lower()
|
|
17
|
+
|
|
18
|
+
# cloudflare turnstile
|
|
19
|
+
if "challenge-platform" in html_content or "cf-challenge" in html_content:
|
|
20
|
+
return True, "Cloudflare Turnstile (Managed Challenge)"
|
|
21
|
+
if "window._cf_chl_opt" in html_content or "cf-ray:" in html_lower:
|
|
22
|
+
return True, "Cloudflare WAF Block Page"
|
|
23
|
+
if "cf-mitigated" in response.headers.get("Server", "").lower():
|
|
24
|
+
return True, "Cloudflare Edge Mitigation"
|
|
25
|
+
|
|
26
|
+
# perimeterx
|
|
27
|
+
if "window._pxappid" in html_lower or "px-captcha" in html_lower:
|
|
28
|
+
return True, "Human Security (PerimeterX) CAPTCHA"
|
|
29
|
+
if "captcha.px-cdn.net" in html_content or "client.perimeterx.net" in html_content:
|
|
30
|
+
return True, "Human Security (PerimeterX) Shield Active"
|
|
31
|
+
|
|
32
|
+
#recaptcha
|
|
33
|
+
if "google.com" in html_lower or "g-recaptcha" in html_lower:
|
|
34
|
+
return True, "Google reCAPTCHA Challenge"
|
|
35
|
+
if "recaptcha.js" in html_lower or "__recaptcha_api" in html_lower:
|
|
36
|
+
return True, "Google reCAPTCHA Script Loaded"
|
|
37
|
+
|
|
38
|
+
#h captcha
|
|
39
|
+
if "hcaptcha.com" in html_content or "h-captcha" in html_lower:
|
|
40
|
+
return True, "hCaptcha Verification Screen"
|
|
41
|
+
|
|
42
|
+
#akamai
|
|
43
|
+
if "akam_bm" in response.cookies or "bm_sz" in response.cookies:
|
|
44
|
+
return True, "Akamai Bot Manager Cookie Block"
|
|
45
|
+
if "_sec_challenge" in html_lower or "akamai-extension" in html_lower:
|
|
46
|
+
return True, "Akamai WAF Challenge Injection"
|
|
47
|
+
|
|
48
|
+
# aws/amazon captcha
|
|
49
|
+
if "aws-waf-token" in html_lower or "awswaf" in html_lower:
|
|
50
|
+
return True, "AWS WAF Token Challenge"
|
|
51
|
+
if "amazon captcha" in html_lower or "amzn-captcha" in html_lower:
|
|
52
|
+
return True, "Amazon Custom CAPTCHA Screen"
|
|
53
|
+
|
|
54
|
+
# incapsula or soemthing
|
|
55
|
+
if "incapsula" in html_lower or "_incap_" in html_lower:
|
|
56
|
+
return True, "Imperva Incapsula Bot Shield"
|
|
57
|
+
if "visid_incap" in response.cookies:
|
|
58
|
+
return True, "Imperva Session Interception"
|
|
59
|
+
|
|
60
|
+
# kasada
|
|
61
|
+
if "kpsdk" in html_lower or "ips.js" in html_lower:
|
|
62
|
+
return True, "Kasada Anti-Bot Handshake"
|
|
63
|
+
|
|
64
|
+
# generic captchas
|
|
65
|
+
if response.status_code in [403, 429]:
|
|
66
|
+
generic_signals = ["captcha", "robot", "automated access", "verify you are human", "checking your browser"]
|
|
67
|
+
for signal in generic_signals:
|
|
68
|
+
if signal in html_lower:
|
|
69
|
+
return True, f"Generic Firewall Block ({signal.title()})"
|
|
70
|
+
return True, f"Unidentified Security Drop (HTTP {response.status_code})"
|
|
71
|
+
|
|
72
|
+
return False, ""
|
|
73
|
+
'''
|
|
74
|
+
HEADER = {
|
|
75
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
76
|
+
"Accept-Language": "en-US, en;q=0.9"
|
|
77
|
+
} #browser header
|
|
78
|
+
|
|
79
|
+
def gethtmlafterload(url):
|
|
80
|
+
with sync_playwright() as p:
|
|
81
|
+
browser = p.chromium.launch(
|
|
82
|
+
headless=True,
|
|
83
|
+
args=[
|
|
84
|
+
"--disable-blink-features=AutomationControlled",
|
|
85
|
+
"--no-sandbox",
|
|
86
|
+
"--disable-infobars"
|
|
87
|
+
])
|
|
88
|
+
context = browser.new_context(
|
|
89
|
+
user_agent=HEADER['User-Agent'],
|
|
90
|
+
viewport={'width': 1920, 'height': 1080},
|
|
91
|
+
has_touch=True
|
|
92
|
+
)
|
|
93
|
+
page = context.new_page()
|
|
94
|
+
Stealth().apply_stealth_sync(page) #stealth
|
|
95
|
+
|
|
96
|
+
#go to page and get code, using stealth to bypass captchas
|
|
97
|
+
try:
|
|
98
|
+
page.goto(url, wait_until="domcontentloaded", timeout=60000) #wait for page content to download
|
|
99
|
+
page.mouse.move(100, 100)
|
|
100
|
+
page.mouse.move(200, 300)
|
|
101
|
+
page.evaluate("window.scrollTo(0, 500)")
|
|
102
|
+
|
|
103
|
+
page.wait_for_timeout(5000) #wait for page to load downloaded content, and cookie
|
|
104
|
+
mainhtml = page.content()
|
|
105
|
+
cookies = {c['name']: c['value'] for c in context.cookies()}
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print("Unexpected Error:", e)
|
|
109
|
+
mainhtml, cookies = "", {}
|
|
110
|
+
|
|
111
|
+
browser.close()
|
|
112
|
+
return mainhtml, cookies
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def identify_javascript_type(html, headers=None):
|
|
116
|
+
stack = []
|
|
117
|
+
#print(f"\n[DEBUG] HTML Snippet: {html[:1000]}\n")
|
|
118
|
+
# Next.js
|
|
119
|
+
if any(term in html for term in ['data-next-head', 'script id="__NEXT_DATA__"', 'next-head-count', '_next/', '_next/data']):
|
|
120
|
+
stack.append("Next.js")
|
|
121
|
+
# React
|
|
122
|
+
if 'data-reactroot' in html or 'id="root"' in html or 'react-dom' in html.lower():
|
|
123
|
+
stack.append("React")
|
|
124
|
+
# Vue / Angular
|
|
125
|
+
if 'id="app"' in html or 'v-bind' in html: stack.append("Vue.js")
|
|
126
|
+
if 'id="__nuxt"' in html or 'window.__NUXT__' in html: stack.append("Nuxt.js (Vue)")
|
|
127
|
+
if '<app-root' in html or 'ng-version' in html or '_nghost-' in html: stack.append("Angular")
|
|
128
|
+
# Node.js
|
|
129
|
+
if headers:
|
|
130
|
+
powered_by = headers.get('X-Powered-By', '').lower()
|
|
131
|
+
if 'express' in powered_by or 'node' in powered_by:
|
|
132
|
+
stack.append(f"Node.js ({powered_by.capitalize()})")
|
|
133
|
+
# Build Tools
|
|
134
|
+
if any(term in html.lower() for term in ['@vite/client', 'vite-plugin', 'src="/@vite']):
|
|
135
|
+
stack.append("Vite")
|
|
136
|
+
if 'webpack' in html.lower(): stack.append("Webpack")
|
|
137
|
+
|
|
138
|
+
return " + ".join(stack) if stack else "Unknown JS Stack"
|
|
139
|
+
|
|
140
|
+
async def async_rate_test(url, num_reqs=100):
|
|
141
|
+
print(f"\nStarting Rate Limit Test: {num_reqs} requests to {url}")
|
|
142
|
+
|
|
143
|
+
async with requests.AsyncSession(impersonate="chrome120") as session:
|
|
144
|
+
tasks = [session.get(url, headers=HEADER, timeout=10) for _ in range(num_reqs)]
|
|
145
|
+
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
|
146
|
+
|
|
147
|
+
status_counts = {}
|
|
148
|
+
first_limit_at = None
|
|
149
|
+
|
|
150
|
+
for i, res in enumerate(responses):
|
|
151
|
+
request_number = i + 1
|
|
152
|
+
if isinstance(res, Exception):
|
|
153
|
+
status_counts['Error'] = status_counts.get('Error', 0) + 1
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
code = res.status_code
|
|
157
|
+
status_counts[code] = status_counts.get(code, 0) + 1
|
|
158
|
+
|
|
159
|
+
if code != 200 and first_limit_at is None:
|
|
160
|
+
first_limit_at = (request_number, code)
|
|
161
|
+
|
|
162
|
+
print("\n--- Rate Limit Results ---")
|
|
163
|
+
for code, count in status_counts.items():
|
|
164
|
+
if code == 'Error': continue
|
|
165
|
+
label = "OK" if code == 200 else "LIMITED" if code == 429 else "WAF/FORBIDDEN" if code == 403 else "CRASHED" if code == 500 else "Other"
|
|
166
|
+
print(f" Status {code} ({label}): {count}")
|
|
167
|
+
|
|
168
|
+
if status_counts.get(200, 0) == num_reqs:
|
|
169
|
+
print(f"\nWebsite is potentially vulnerable to DoS or brute-forcing (No rate limit detected after {num_reqs} requests).")
|
|
170
|
+
elif first_limit_at:
|
|
171
|
+
req_num, code = first_limit_at
|
|
172
|
+
if code == 403:
|
|
173
|
+
print(f"\nA WAF (Firewall) likely intercepted the requests (403 Forbidden after {req_num} requests).")
|
|
174
|
+
elif code == 429:
|
|
175
|
+
print(f"\nServer-side rate limiting is active (429 Too Many Requests detected after {req_num} requests).")
|
|
176
|
+
else:
|
|
177
|
+
print(f"\nServer began responding with {code} after {req_num} requests.")
|
|
178
|
+
|
|
179
|
+
def main():
|
|
180
|
+
parser = argparse.ArgumentParser()
|
|
181
|
+
parser.add_argument("target", nargs='?', help="URL")
|
|
182
|
+
parser.add_argument("--ratelimit", nargs='?', const=100, type=int, default=None, help="Number of requests")
|
|
183
|
+
parser.add_argument("--testpath", nargs='?', const='/', type=str, help="Endpoint to test")
|
|
184
|
+
parser.add_argument("--show-404s", action="store_true", help="Show endpoints tested that returned a 404")
|
|
185
|
+
parser.add_argument("--disable-extra-files", action="store_true", help="Disable scanning of extra structural mapping files (robots, sitemaps, manifests, etc.)")
|
|
186
|
+
parser.add_argument("--show-assets", action="store_true", help="Include assets like images and fonts in scan results")
|
|
187
|
+
|
|
188
|
+
args = parser.parse_args()
|
|
189
|
+
if args.testpath and args.ratelimit is None:
|
|
190
|
+
args.ratelimit = 100
|
|
191
|
+
|
|
192
|
+
ignored_extensions = ()
|
|
193
|
+
|
|
194
|
+
# error if the user somehow didn't read the instructions (i've idiotproofed the code ENOUGH)
|
|
195
|
+
if args.testpath and args.ratelimit is None:
|
|
196
|
+
parser.error("--testpath requires the --ratelimit flag.\nIf you want to do a rate limit test, use --ratelimit (number of requests) --testpath (path to test).\nIf not, don't use --ratelimit nor --testpath.")
|
|
197
|
+
|
|
198
|
+
show_dead = args.show_404s
|
|
199
|
+
target = args.target if args.target else input("Enter website (e.g. https://example.com): ").strip()
|
|
200
|
+
if not target.startswith(("http://", "https://")):
|
|
201
|
+
target = "https://" + target
|
|
202
|
+
try:
|
|
203
|
+
response = requests.get(target, headers=HEADER, timeout=5, impersonate="chrome120")
|
|
204
|
+
except requests.exceptions.SSLError:
|
|
205
|
+
if target.startswith("https://"):
|
|
206
|
+
print('[!] HTTPS SSL Error. Trying HTTP...') #becuase later sum http noob
|
|
207
|
+
target = target.replace("https://", "http://")
|
|
208
|
+
try:
|
|
209
|
+
requests.get(target, headers=HEADER, timeout=5, impersonate="chrome120")
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(f'[!] Target unreachable on HTTP: {e}')
|
|
212
|
+
exit()
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f'Target unreachable: {e}')
|
|
215
|
+
exit()
|
|
216
|
+
try:
|
|
217
|
+
st = time.perf_counter()
|
|
218
|
+
uptimeres = requests.get(target, headers=HEADER, timeout=10, impersonate="chrome120")
|
|
219
|
+
et = time.perf_counter()
|
|
220
|
+
restime = et - st
|
|
221
|
+
print(f"Site responded in {round(restime, 2)} seconds.")
|
|
222
|
+
|
|
223
|
+
if restime < 0.5:
|
|
224
|
+
print("Server is very fast.")
|
|
225
|
+
elif restime < 1:
|
|
226
|
+
print("Server is fast.")
|
|
227
|
+
elif restime < 2.5:
|
|
228
|
+
print("Server is average speed.")
|
|
229
|
+
elif restime < 4:
|
|
230
|
+
print("Server is slow.")
|
|
231
|
+
else:
|
|
232
|
+
print("Server is very slow.")
|
|
233
|
+
except requests.exceptions.Timeout:
|
|
234
|
+
print("Server did not respond after 10 seconds.")
|
|
235
|
+
|
|
236
|
+
SENSITIVE_ENDPOINT = {
|
|
237
|
+
"/.env", "/.env.local", "/.env.production", "/.env.development",
|
|
238
|
+
"/.git/config", "/.git/HEAD", "/robots.txt", "/sitemap.xml",
|
|
239
|
+
"/package.json", "/package-lock.json", "/.npmrc", "/.dockerenv",
|
|
240
|
+
"/.gitignore", "/api/health", "/admin", "/login", "/config"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
results_fromotherfiles = []
|
|
244
|
+
found_paths = set(SENSITIVE_ENDPOINT)
|
|
245
|
+
discovered_in_js = set()
|
|
246
|
+
|
|
247
|
+
if not args.disable_extra_files:
|
|
248
|
+
print("\nFinding paths from map files. (If they exist)")
|
|
249
|
+
|
|
250
|
+
# bobot.txt
|
|
251
|
+
try:
|
|
252
|
+
r_res = requests.get(urljoin(target, "/robots.txt"), headers=HEADER, impersonate="chrome120", timeout=4)
|
|
253
|
+
if r_res.status_code == 200 and "disallow" in r_res.text.lower():
|
|
254
|
+
rules = re.findall(r'(?:Disallow|Allow):\s*(/[a-zA-Z0-9_\-\./{}:|]*)', r_res.text, re.IGNORECASE)
|
|
255
|
+
for rule in rules:
|
|
256
|
+
clean_rule = rule.strip()
|
|
257
|
+
if clean_rule and clean_rule not in ["/", "/*"] and clean_rule not in found_paths:
|
|
258
|
+
found_paths.add(clean_rule)
|
|
259
|
+
results_fromotherfiles.append(f"{clean_rule} [Source: robots.txt]")
|
|
260
|
+
except: pass
|
|
261
|
+
|
|
262
|
+
# sitemap
|
|
263
|
+
try:
|
|
264
|
+
s_res = requests.get(urljoin(target, "/sitemap.xml"), headers=HEADER, impersonate="chrome120", timeout=4)
|
|
265
|
+
if s_res.status_code == 200 and "<loc" in s_res.text.lower():
|
|
266
|
+
locs = re.findall(r'<loc>https?://[^/]+(/[^<]+)</loc>', s_res.text, re.IGNORECASE)
|
|
267
|
+
for loc in locs:
|
|
268
|
+
clean_path = loc.strip()
|
|
269
|
+
if clean_path and clean_path != "/" and clean_path not in found_paths:
|
|
270
|
+
found_paths.add(clean_path)
|
|
271
|
+
results_fromotherfiles.append(f"{clean_path} [Source: sitemap.xml]")
|
|
272
|
+
except: pass
|
|
273
|
+
|
|
274
|
+
# me when manifesto without the o
|
|
275
|
+
try:
|
|
276
|
+
m_res = requests.get(urljoin(target, "/asset-manifest.json"), headers=HEADER, impersonate="chrome120", timeout=4)
|
|
277
|
+
if m_res.status_code == 200 and "{" in m_res.text:
|
|
278
|
+
paths = re.findall(r'["\'](/[a-zA-Z0-9_\-\./]+)["\']', m_res.text)
|
|
279
|
+
for path in paths:
|
|
280
|
+
if path not in found_paths:
|
|
281
|
+
found_paths.add(path)
|
|
282
|
+
results_fromotherfiles.append(f"{path} [Source: asset-manifest.json]")
|
|
283
|
+
except: pass
|
|
284
|
+
for manifest_path in ["/web-manifest.json", "/manifest.json"]:
|
|
285
|
+
try:
|
|
286
|
+
m_url = urljoin(target, manifest_path)
|
|
287
|
+
m_res = requests.get(m_url, headers=HEADER, impersonate="chrome120", timeout=4)
|
|
288
|
+
if m_res.status_code == 200 and "{" in m_res.text:
|
|
289
|
+
paths = re.findall(r'["\'](/[a-zA-Z0-9_\-\./]+)["\']', m_res.text)
|
|
290
|
+
for path in paths:
|
|
291
|
+
if path not in found_paths:
|
|
292
|
+
found_paths.add(path)
|
|
293
|
+
results_fromotherfiles.append(f"{path} [Source: {manifest_path.lstrip('/')}]")
|
|
294
|
+
except: pass
|
|
295
|
+
# service worker lol
|
|
296
|
+
for sw_path in ["/service-worker.js", "/sw.js"]:
|
|
297
|
+
try:
|
|
298
|
+
sw_res = requests.get(urljoin(target, sw_path), headers=HEADER, impersonate="chrome120", timeout=4)
|
|
299
|
+
if sw_res.status_code == 200:
|
|
300
|
+
paths = re.findall(r'["\'`](/[a-zA-Z0-9_\-\./{}:]+)["\'`]', sw_res.text)
|
|
301
|
+
for path in paths:
|
|
302
|
+
if path not in found_paths and not any(path.endswith(ext) for ext in ['.js', '.css']):
|
|
303
|
+
found_paths.add(path)
|
|
304
|
+
results_fromotherfiles.append(f"{path} [Source: {sw_path}]")
|
|
305
|
+
except: pass
|
|
306
|
+
|
|
307
|
+
#openid
|
|
308
|
+
try:
|
|
309
|
+
oidc_res = requests.get(urljoin(target, "/.well-known/openid-configuration"), headers=HEADER, impersonate="chrome120", timeout=4)
|
|
310
|
+
if oidc_res.status_code == 200 and "{" in oidc_res.text:
|
|
311
|
+
paths = re.findall(r'https?://[^/]+(/[^"\']*)', oidc_res.text)
|
|
312
|
+
for path in paths:
|
|
313
|
+
if path not in found_paths:
|
|
314
|
+
found_paths.add(path)
|
|
315
|
+
results_fromotherfiles.append(f"{path} [Source: openid-configuration]")
|
|
316
|
+
except: pass
|
|
317
|
+
|
|
318
|
+
print("\nStarting headless browser to bypass captchas and detect shells with a fake path.")
|
|
319
|
+
main_html, session_cookies = gethtmlafterload(target)
|
|
320
|
+
|
|
321
|
+
fake_path = "/very-fake-page-123456123456abcdefg"
|
|
322
|
+
fake_url = urljoin(target, fake_path)
|
|
323
|
+
try:
|
|
324
|
+
fake_res = requests.get(fake_url, cookies=session_cookies, headers=HEADER, impersonate="chrome120", timeout=10)
|
|
325
|
+
shell_content = fake_res.text
|
|
326
|
+
except:
|
|
327
|
+
shell_content = ""
|
|
328
|
+
try:
|
|
329
|
+
print(f"Detected JS Type: {identify_javascript_type(main_html)}")
|
|
330
|
+
soup = BeautifulSoup(main_html, 'html.parser')
|
|
331
|
+
|
|
332
|
+
# next js code files
|
|
333
|
+
js_files = [urljoin(target, s.get('src')) for s in soup.find_all('script') if s.get('src')]
|
|
334
|
+
js_files.append(urljoin(target, "/_next/static/development/_buildManifest.js"))
|
|
335
|
+
js_files.append(urljoin(target, "/_next/static/runtime/_buildManifest.js"))
|
|
336
|
+
|
|
337
|
+
for script_tag in soup.find_all('script'):
|
|
338
|
+
src = script_tag.get('src')
|
|
339
|
+
if src and not src.startswith(('http://', 'https://')):
|
|
340
|
+
# Use urlparse to strip away parameters and isolate a clean local path
|
|
341
|
+
local_path = urlparse(src).path
|
|
342
|
+
if local_path:
|
|
343
|
+
# Guarantee exactly ONE starting slash, nothing more, nothing less
|
|
344
|
+
clean_path = '/' + local_path.lstrip('/')
|
|
345
|
+
found_paths.add(clean_path)
|
|
346
|
+
|
|
347
|
+
# Gather stylesheets safely without corrupting string slashes
|
|
348
|
+
for link_tag in soup.find_all('link', rel='stylesheet'):
|
|
349
|
+
href = link_tag.get('href')
|
|
350
|
+
if href and not href.startswith(('http://', 'https://')):
|
|
351
|
+
local_path = urlparse(href).path
|
|
352
|
+
if local_path:
|
|
353
|
+
clean_path = '/' + local_path.lstrip('/')
|
|
354
|
+
found_paths.add(clean_path)
|
|
355
|
+
patterns = [
|
|
356
|
+
r'["\'`](/[a-zA-Z0-9_\-\./{}:]*)["\'`]',
|
|
357
|
+
r'(?:path|href|to|post|get|patch|put|delete|head|options)[\s]*[:=\(\|]+[\s]*["\'`](/?[a-zA-Z0-9_\-\./{}:\$]*[\./][a-zA-Z0-9_\-\./{}:\$]*)["\'`]',
|
|
358
|
+
r'[`](https?://[a-zA-Z0-9_\-\./{}:\$]+)[`]'
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
# check <script> for endpoint too
|
|
362
|
+
inline_scripts = soup.find_all('script')
|
|
363
|
+
for script in inline_scripts:
|
|
364
|
+
if script.string:
|
|
365
|
+
chunks = re.findall(r'["\'](/[a-zA-Z0-9_\-\./]*\.js)["\']', script.string)
|
|
366
|
+
for c in chunks:
|
|
367
|
+
js_files.append(urljoin(target, c))
|
|
368
|
+
|
|
369
|
+
for p in patterns:
|
|
370
|
+
matches = re.findall(p, script.string)
|
|
371
|
+
for m in matches:
|
|
372
|
+
m_clean = re.sub(r'(\$\{.*\}|:[a-zA-Z0-9]+)', '1', m)
|
|
373
|
+
if not m_clean.startswith('/'): m_clean = '/' + m_clean
|
|
374
|
+
|
|
375
|
+
if not m_clean.lower().endswith(ignored_extensions):
|
|
376
|
+
if m_clean in ["/", "//", "///", "/.", "/..", "/..."]:
|
|
377
|
+
continue
|
|
378
|
+
found_paths.add(m_clean)
|
|
379
|
+
discovered_in_js.add(m_clean)
|
|
380
|
+
|
|
381
|
+
# scan all js files
|
|
382
|
+
for js_url in js_files:
|
|
383
|
+
try:
|
|
384
|
+
js_res = requests.get(js_url, headers=HEADER, cookies=session_cookies, timeout=5, impersonate="chrome120")
|
|
385
|
+
if js_res.status_code == 200:
|
|
386
|
+
for p in patterns:
|
|
387
|
+
matches = re.findall(p, js_res.text)
|
|
388
|
+
for m in matches:
|
|
389
|
+
m_clean = re.sub(r'(\$\{.*?\}|:[a-zA-Z0-9]+)', '1', m)
|
|
390
|
+
if not m_clean.startswith('/'): m_clean = '/' + m_clean
|
|
391
|
+
|
|
392
|
+
if not m_clean.lower().endswith(ignored_extensions):
|
|
393
|
+
if m_clean in ["/", "//", "///", "/.", "/..", "/..."]:
|
|
394
|
+
continue
|
|
395
|
+
found_paths.add(m_clean)
|
|
396
|
+
discovered_in_js.add(m_clean)
|
|
397
|
+
except: continue
|
|
398
|
+
|
|
399
|
+
print(f"Total paths to test: {len(found_paths)} ({len(discovered_in_js)} scraped from JS).")
|
|
400
|
+
print("Testing endpoints...")
|
|
401
|
+
results_200, results_dead, results_30x = [], [], []
|
|
402
|
+
results_services, results_ext = [], []
|
|
403
|
+
results_frameworks, results_assets = [], []
|
|
404
|
+
|
|
405
|
+
for path in sorted(found_paths):
|
|
406
|
+
# all external url cuz https: //blahblah
|
|
407
|
+
if "://" in path:
|
|
408
|
+
results_ext.append(path.lstrip('/'))
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
parsed_path = urlparse(path)
|
|
413
|
+
target_domain = urlparse(target).netloc
|
|
414
|
+
|
|
415
|
+
# get the base domain (efg.hijk from abcd.efg.hijk)
|
|
416
|
+
def get_base(domain):
|
|
417
|
+
parts = domain.split('.')
|
|
418
|
+
return ".".join(parts[-2:]) if len(parts) > 1 else domain
|
|
419
|
+
|
|
420
|
+
is_external = parsed_path.netloc and get_base(parsed_path.netloc) != get_base(target_domain)
|
|
421
|
+
|
|
422
|
+
if is_external:
|
|
423
|
+
results_ext.append(f"{path.lstrip('/')} [External Reference]")
|
|
424
|
+
continue
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
r = requests.get(
|
|
428
|
+
urljoin(target, path),
|
|
429
|
+
headers=HEADER,
|
|
430
|
+
cookies=session_cookies,
|
|
431
|
+
timeout=5,
|
|
432
|
+
allow_redirects=False,
|
|
433
|
+
impersonate="chrome120"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
content_type = r.headers.get("Content-Type", "").lower()
|
|
437
|
+
is_shell = (r.text == shell_content)
|
|
438
|
+
is_home_redirect = (r.text == main_html)
|
|
439
|
+
|
|
440
|
+
# common service and api i think
|
|
441
|
+
service_markers = ["/api", "/v1", "/v2", "socket.io", "engine.io", "/graphql", "/webhook", "/rpc"]
|
|
442
|
+
is_machine_path = any(marker in path.lower() for marker in service_markers)
|
|
443
|
+
|
|
444
|
+
media_extensions = ('.png', '.jpg', '.jpeg', '.svg', '.webp', '.gif', '.ico', '.woff', '.woff2', '.ttf')
|
|
445
|
+
framework_extensions = ('.js', '.css', '.json', '.txt', '.xml', '.map')
|
|
446
|
+
|
|
447
|
+
is_media_asset = path.lower().endswith(media_extensions)
|
|
448
|
+
is_framework_asset = any(path.lower().endswith(ext) for ext in framework_extensions)
|
|
449
|
+
|
|
450
|
+
if r.status_code == 200:
|
|
451
|
+
# if its a service/api path then like service and stuff ykyk
|
|
452
|
+
if is_machine_path:
|
|
453
|
+
results_services.append(f"{path} [Service/API]")
|
|
454
|
+
elif is_shell or is_home_redirect:
|
|
455
|
+
if path in discovered_in_js:
|
|
456
|
+
results_200.append(f"{path} [Client-Side Route, Requires Login]")
|
|
457
|
+
else:
|
|
458
|
+
results_dead.append(f"404 Not Found (React Shell): {path}")
|
|
459
|
+
else:
|
|
460
|
+
if "text/html" in content_type:
|
|
461
|
+
results_200.append(f"{path} [Access no matter what]")
|
|
462
|
+
elif is_media_asset:
|
|
463
|
+
results_assets.append(f"{path}")
|
|
464
|
+
elif is_framework_asset:
|
|
465
|
+
results_frameworks.append(f"{path}")
|
|
466
|
+
else:
|
|
467
|
+
#standard is like js and css
|
|
468
|
+
results_frameworks.append(f"{path} [Non-Standard File]")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
elif r.status_code == 400:
|
|
472
|
+
if is_framework_asset:
|
|
473
|
+
results_frameworks.append(f"{path} [Asset Error - 400]")
|
|
474
|
+
# get machine services like socket.io that reject simple GET request. cuz socket stinky.
|
|
475
|
+
if "." in path or "/" in path:
|
|
476
|
+
results_services.append(f"{path} [Potential Service - 400]")
|
|
477
|
+
elif is_machine_path:
|
|
478
|
+
results_services.append(f"{path} [Service/API]") #cuz socket
|
|
479
|
+
else:
|
|
480
|
+
results_dead.append(f"400 Bad Request: {path}")
|
|
481
|
+
|
|
482
|
+
elif r.status_code in [403, 404]:
|
|
483
|
+
results_dead.append(f"{r.status_code} Error: {path}")
|
|
484
|
+
elif str(r.status_code).startswith('3'):
|
|
485
|
+
results_30x.append(f"{path} -> {r.headers.get('Location')}")
|
|
486
|
+
except: continue
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
if len(results_200) != 0:
|
|
491
|
+
print("\n---- ENDPOINTS FOUND ----")
|
|
492
|
+
for p in results_200: print(f" {p}")
|
|
493
|
+
else:
|
|
494
|
+
print("\n----NO ENDPOITNS FOUND----")
|
|
495
|
+
if len(results_services) != 0:
|
|
496
|
+
print("\n----SERVICES/APIS USED----")
|
|
497
|
+
for p in results_services: print(f" {p}")
|
|
498
|
+
else:
|
|
499
|
+
print("\n----NO SERVICES/APIS FOUND----")
|
|
500
|
+
if len(results_ext) != 0:
|
|
501
|
+
print("\n----EXTERNAL LINKS----")
|
|
502
|
+
for p in results_ext: print(f" {p}")
|
|
503
|
+
else:
|
|
504
|
+
print("\n----NO EXTERNAL LINKS FOUND----")
|
|
505
|
+
if len(results_frameworks) != 0:
|
|
506
|
+
print("\n----WEBSITE FRAMEWORKS----")
|
|
507
|
+
for p in results_frameworks: print(f" {p}")
|
|
508
|
+
else:
|
|
509
|
+
print("\n----NO WEBSITE FRAMEWORKS FOUND----")
|
|
510
|
+
if len(results_30x) != 0:
|
|
511
|
+
print("\n---- REDIRECTS (301/302/307) ----")
|
|
512
|
+
for p in results_30x: print(f" {p}")
|
|
513
|
+
else:
|
|
514
|
+
print("\n----NO REDIRECTS FOUND----")
|
|
515
|
+
# if result contains stuff
|
|
516
|
+
if not args.disable_extra_files:
|
|
517
|
+
if results_fromotherfiles:
|
|
518
|
+
print("\n---- PATHS FROM OTHER FILES ----")
|
|
519
|
+
for entry in results_fromotherfiles:
|
|
520
|
+
print(f" {entry}")
|
|
521
|
+
else:
|
|
522
|
+
print("\n----NO EXTRA PATHS FOUND FROM OTHER FILES----")
|
|
523
|
+
else:
|
|
524
|
+
pass
|
|
525
|
+
if args.show_assets:
|
|
526
|
+
if len(results_assets) != 0:
|
|
527
|
+
print("\n----WEBSITE ASSETS----")
|
|
528
|
+
for p in results_assets: print(f" {p}")
|
|
529
|
+
else:
|
|
530
|
+
print("\n----NO WEBSITE ASSETS FOUND----")
|
|
531
|
+
else:
|
|
532
|
+
pass
|
|
533
|
+
if show_dead:
|
|
534
|
+
if len(results_dead) != 0:
|
|
535
|
+
print("\n---- INACCESSIBLE (Confirmed 404/403) ----")
|
|
536
|
+
for p in results_dead: print(f" {p}")
|
|
537
|
+
else:
|
|
538
|
+
print("\n----NONE INACCESSIBLE 404/403----")
|
|
539
|
+
|
|
540
|
+
print(f"\n--- Scan Summary ---")
|
|
541
|
+
if args.show_assets:
|
|
542
|
+
print(f"Total Accessible Pages: {len(results_200)}\nTotal Assets: {len(results_assets)}\nTotal Inaccessible: {len(results_dead)}\nTotal Redirects: {len(results_30x)}\nTotal Frameworks: {len(results_frameworks)}")
|
|
543
|
+
else:
|
|
544
|
+
print(f"Total Accessible Pages: {len(results_200)}\nTotal Assets: {len(results_assets)} (Assets are hidden, use --show-assets to show them.)\nTotal Inaccessible: {len(results_dead)}\nTotal Redirects: {len(results_30x)}\nTotal Frameworks: {len(results_frameworks)}")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
if args.ratelimit is not None:
|
|
548
|
+
num = args.ratelimit
|
|
549
|
+
test_path = "/" #root dir
|
|
550
|
+
if args.testpath: #idiotproof
|
|
551
|
+
test_path = args.testpath if args.testpath.startswith('/') else '/' + args.testpath
|
|
552
|
+
try:
|
|
553
|
+
check_res = requests.get(urljoin(target, test_path), headers=HEADER, timeout=5, impersonate="chrome120")
|
|
554
|
+
if check_res.status_code in [301, 302, 307, 308, 403, 404]:
|
|
555
|
+
print(f"{test_path} is {check_res.status_code}. Testing on root domain.")
|
|
556
|
+
test_path = "/"
|
|
557
|
+
except:
|
|
558
|
+
test_path = "/"
|
|
559
|
+
|
|
560
|
+
asyncio.run(async_rate_test(urljoin(target, test_path), num))
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
print(f"Main Error: {e}")
|
|
564
|
+
|
|
565
|
+
if __name__ == "__main__":
|
|
566
|
+
main()
|