vpixray 0.1.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.
- core/__init__.py +0 -0
- core/cmdi.py +40 -0
- core/cors.py +38 -0
- core/crlf.py +40 -0
- core/csrf.py +35 -0
- core/dirlisting.py +31 -0
- core/fingerprint.py +38 -0
- core/headers_check.py +38 -0
- core/hostheader.py +29 -0
- core/idor.py +32 -0
- core/jwt.py +77 -0
- core/lfi.py +49 -0
- core/nosqli.py +51 -0
- core/redirect.py +39 -0
- core/requester.py +44 -0
- core/sensitive.py +41 -0
- core/sqli.py +102 -0
- core/sqli_blind.py +48 -0
- core/ssrf.py +36 -0
- core/ssti.py +39 -0
- core/xss.py +42 -0
- core/xxe.py +26 -0
- recon/__init__.py +0 -0
- recon/dns_lookup.py +50 -0
- recon/http_methods.py +66 -0
- recon/ipinfo.py +76 -0
- recon/port_scan.py +63 -0
- recon/server_info.py +59 -0
- recon/ssl_info.py +72 -0
- recon/subdomain_enum.py +54 -0
- recon/whois_lookup.py +61 -0
- utils/__init__.py +0 -0
- utils/banner.py +23 -0
- utils/colors.py +14 -0
- utils/reporter.py +56 -0
- vpixray-0.1.0.dist-info/METADATA +370 -0
- vpixray-0.1.0.dist-info/RECORD +42 -0
- vpixray-0.1.0.dist-info/WHEEL +5 -0
- vpixray-0.1.0.dist-info/entry_points.txt +2 -0
- vpixray-0.1.0.dist-info/licenses/LICENSE +21 -0
- vpixray-0.1.0.dist-info/top_level.txt +4 -0
- vpixray.py +198 -0
core/__init__.py
ADDED
|
File without changes
|
core/cmdi.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
3
|
+
from utils.colors import console
|
|
4
|
+
|
|
5
|
+
CMDI_PAYLOADS = [
|
|
6
|
+
(";id", r"uid=\d+\("),
|
|
7
|
+
("|id", r"uid=\d+\("),
|
|
8
|
+
("`id`", r"uid=\d+\("),
|
|
9
|
+
("$(id)", r"uid=\d+\("),
|
|
10
|
+
(";cat /etc/passwd", r"root:.*:0:0:"),
|
|
11
|
+
("|whoami", r"(root|www-data|apache|nginx)"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
def check_cmdi(requester, url):
|
|
15
|
+
result = {"name": "Command Injection", "findings": [], "vulnerable": []}
|
|
16
|
+
parsed = urlparse(url)
|
|
17
|
+
params = parse_qs(parsed.query)
|
|
18
|
+
if not params:
|
|
19
|
+
console.print("[dim] i No parameters found, skipping[/dim]")
|
|
20
|
+
return result
|
|
21
|
+
console.print(f"[info] -> Testing {len(params)} param(s) with {len(CMDI_PAYLOADS)} payloads...[/info]")
|
|
22
|
+
for param in params:
|
|
23
|
+
console.print(f"[dim] -> Param: {param}[/dim]")
|
|
24
|
+
for i, (payload, pattern) in enumerate(CMDI_PAYLOADS, 1):
|
|
25
|
+
q = params.copy()
|
|
26
|
+
q[param] = [params[param][0] + payload]
|
|
27
|
+
test_url = urlunparse(parsed._replace(query=urlencode(q, doseq=True)))
|
|
28
|
+
console.print(f"[dim] [{i}/{len(CMDI_PAYLOADS)}] {payload!r}[/dim]")
|
|
29
|
+
resp = requester.get(test_url)
|
|
30
|
+
if resp and re.search(pattern, resp.text):
|
|
31
|
+
console.print(f"[vuln] !! Command Injection via {param}![/vuln]")
|
|
32
|
+
result["vulnerable"].append({"param": param, "payload": payload})
|
|
33
|
+
result["findings"].append({
|
|
34
|
+
"severity": "critical",
|
|
35
|
+
"msg": f"Command Injection via param: {param} (payload: {payload})",
|
|
36
|
+
})
|
|
37
|
+
break
|
|
38
|
+
if not result["vulnerable"]:
|
|
39
|
+
console.print("[success] + No command injection found[/success]")
|
|
40
|
+
return result
|
core/cors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from utils.colors import console
|
|
2
|
+
|
|
3
|
+
def check_cors(requester, url):
|
|
4
|
+
result = {"name": "CORS Misconfig", "findings": [], "vulnerable": []}
|
|
5
|
+
console.print("[info] -> Sending requests with crafted Origin headers...[/info]")
|
|
6
|
+
|
|
7
|
+
origins = [
|
|
8
|
+
"https://evil.example.com",
|
|
9
|
+
"null",
|
|
10
|
+
"http://localhost",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
for origin in origins:
|
|
14
|
+
console.print(f"[dim] -> Testing Origin: {origin}[/dim]")
|
|
15
|
+
resp = requester.get(url, headers={"Origin": origin})
|
|
16
|
+
if not resp:
|
|
17
|
+
continue
|
|
18
|
+
acao = resp.headers.get("Access-Control-Allow-Origin", "")
|
|
19
|
+
acac = resp.headers.get("Access-Control-Allow-Credentials", "")
|
|
20
|
+
|
|
21
|
+
if acao == "*":
|
|
22
|
+
console.print(f"[warn] ! CORS wildcard * (no credentials)[/warn]")
|
|
23
|
+
result["findings"].append({
|
|
24
|
+
"severity": "low",
|
|
25
|
+
"msg": "CORS allows wildcard origin (*) — low risk without credentials",
|
|
26
|
+
})
|
|
27
|
+
elif acao == origin:
|
|
28
|
+
sev = "high" if acac.lower() == "true" else "medium"
|
|
29
|
+
console.print(f"[vuln] !! CORS reflects Origin: {origin} (credentials: {acac})[/vuln]")
|
|
30
|
+
result["vulnerable"].append({"origin": origin, "credentials": acac})
|
|
31
|
+
result["findings"].append({
|
|
32
|
+
"severity": sev,
|
|
33
|
+
"msg": f"CORS reflects origin '{origin}' (credentials: {acac or 'none'})",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if not result["findings"]:
|
|
37
|
+
console.print("[success] + CORS properly configured[/success]")
|
|
38
|
+
return result
|
core/crlf.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
CRLF_PAYLOADS = [
|
|
5
|
+
"%0d%0aInjected-Header: vpixray-test",
|
|
6
|
+
"%0aInjected-Header: vpixray-test",
|
|
7
|
+
"%0d%0a%0d%0a<html>injected</html>",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
def check_crlf(requester, url):
|
|
11
|
+
result = {"name": "CRLF Injection", "findings": [], "vulnerable": []}
|
|
12
|
+
parsed = urlparse(url)
|
|
13
|
+
params = parse_qs(parsed.query)
|
|
14
|
+
if not params:
|
|
15
|
+
console.print("[dim] i No parameters found, skipping[/dim]")
|
|
16
|
+
return result
|
|
17
|
+
console.print(f"[info] -> Testing {len(params)} param(s) with {len(CRLF_PAYLOADS)} payloads...[/info]")
|
|
18
|
+
for param in params:
|
|
19
|
+
console.print(f"[dim] -> Param: {param}[/dim]")
|
|
20
|
+
for i, payload in enumerate(CRLF_PAYLOADS, 1):
|
|
21
|
+
console.print(f"[dim] [{i}/{len(CRLF_PAYLOADS)}] Testing...[/dim]")
|
|
22
|
+
q = params.copy()
|
|
23
|
+
q[param] = [payload]
|
|
24
|
+
# نبني URL يدويًا لأن urlencode يشوّه %0d%0a
|
|
25
|
+
query = "&".join(f"{k}={v[0]}" for k, v in q.items())
|
|
26
|
+
test_url = urlunparse(parsed._replace(query=query))
|
|
27
|
+
resp = requester.get(test_url)
|
|
28
|
+
if not resp:
|
|
29
|
+
continue
|
|
30
|
+
if "vpixray-test" in resp.headers or "Injected-Header" in str(resp.headers):
|
|
31
|
+
console.print(f"[vuln] !! CRLF Injection via {param}![/vuln]")
|
|
32
|
+
result["vulnerable"].append({"param": param, "payload": payload})
|
|
33
|
+
result["findings"].append({
|
|
34
|
+
"severity": "high",
|
|
35
|
+
"msg": f"CRLF Injection via param: {param}",
|
|
36
|
+
})
|
|
37
|
+
break
|
|
38
|
+
if not result["vulnerable"]:
|
|
39
|
+
console.print("[success] + No CRLF injection found[/success]")
|
|
40
|
+
return result
|
core/csrf.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from bs4 import BeautifulSoup
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
def check_csrf(requester, url):
|
|
5
|
+
result = {"name": "CSRF", "findings": [], "vulnerable": []}
|
|
6
|
+
console.print("[info] -> Scanning for forms...[/info]")
|
|
7
|
+
resp = requester.get(url)
|
|
8
|
+
if not resp:
|
|
9
|
+
console.print("[error] x Connection failed[/error]")
|
|
10
|
+
return result
|
|
11
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
12
|
+
forms = soup.find_all("form")
|
|
13
|
+
console.print(f"[dim] Found {len(forms)} form(s)[/dim]")
|
|
14
|
+
for i, form in enumerate(forms, 1):
|
|
15
|
+
action = form.get("action", "")
|
|
16
|
+
method = form.get("method", "GET").upper()
|
|
17
|
+
inputs = form.find_all("input")
|
|
18
|
+
has_token = False
|
|
19
|
+
for inp in inputs:
|
|
20
|
+
name = (inp.get("name") or "").lower()
|
|
21
|
+
if "token" in name or "csrf" in name or "_token" in name:
|
|
22
|
+
has_token = True
|
|
23
|
+
break
|
|
24
|
+
if method == "POST" and not has_token:
|
|
25
|
+
console.print(f"[vuln] !! Form #{i} POST without CSRF token[/vuln]")
|
|
26
|
+
result["vulnerable"].append({"form": i, "action": action, "method": method})
|
|
27
|
+
result["findings"].append({
|
|
28
|
+
"severity": "medium",
|
|
29
|
+
"msg": f"POST form without CSRF token (action: {action or 'same page'})",
|
|
30
|
+
})
|
|
31
|
+
else:
|
|
32
|
+
console.print(f"[success] + Form #{i} OK ({method}, token: {has_token})[/success]")
|
|
33
|
+
if not result["vulnerable"]:
|
|
34
|
+
console.print("[success] + No CSRF issues found[/success]")
|
|
35
|
+
return result
|
core/dirlisting.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from utils.colors import console
|
|
2
|
+
|
|
3
|
+
COMMON_DIRS = [
|
|
4
|
+
"/", "/images/", "/uploads/", "/files/", "/backup/",
|
|
5
|
+
"/admin/", "/assets/", "/static/", "/css/", "/js/",
|
|
6
|
+
"/logs/", "/tmp/", "/data/",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
def check_dirlisting(requester, base_url):
|
|
10
|
+
result = {"name": "Directory Listing", "findings": [], "vulnerable": []}
|
|
11
|
+
base = base_url.rstrip("/")
|
|
12
|
+
console.print(f"[info] -> Testing {len(COMMON_DIRS)} directories...[/info]")
|
|
13
|
+
|
|
14
|
+
for i, path in enumerate(COMMON_DIRS, 1):
|
|
15
|
+
url = f"{base}{path}"
|
|
16
|
+
console.print(f"[dim] [{i}/{len(COMMON_DIRS)}] {path}[/dim]")
|
|
17
|
+
resp = requester.get(url)
|
|
18
|
+
if not resp or resp.status_code != 200:
|
|
19
|
+
continue
|
|
20
|
+
body = resp.text.lower()
|
|
21
|
+
if "<title>index of" in body or "<h1>index of" in body:
|
|
22
|
+
console.print(f"[vuln] !! Directory listing enabled: {path}[/vuln]")
|
|
23
|
+
result["vulnerable"].append({"path": path})
|
|
24
|
+
result["findings"].append({
|
|
25
|
+
"severity": "medium",
|
|
26
|
+
"msg": f"Directory listing enabled: {path}",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if not result["vulnerable"]:
|
|
30
|
+
console.print("[success] + No directory listing found[/success]")
|
|
31
|
+
return result
|
core/fingerprint.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
TECH_SIGNATURES = {
|
|
5
|
+
"WordPress": [r"wp-content", r"wp-includes"],
|
|
6
|
+
"Joomla": [r"/components/com_", r"joomla"],
|
|
7
|
+
"Drupal": [r"sites/default/files", r"Drupal"],
|
|
8
|
+
"React": [r"__REACT_DEVTOOLS", r"react-dom"],
|
|
9
|
+
"Vue.js": [r"vue\.js", r"__vue__"],
|
|
10
|
+
"Angular": [r"ng-version", r"angular"],
|
|
11
|
+
"jQuery": [r"jquery(\.min)?\.js"],
|
|
12
|
+
"Bootstrap": [r"bootstrap(\.min)?\.(css|js)"],
|
|
13
|
+
"Nginx": [r"nginx"],
|
|
14
|
+
"Apache": [r"apache"],
|
|
15
|
+
"Cloudflare": [r"cloudflare"],
|
|
16
|
+
"PHP": [r"X-Powered-By:\s*PHP", r"PHPSESSID"],
|
|
17
|
+
"ASP.NET": [r"ASP\.NET", r"X-AspNet-Version"],
|
|
18
|
+
"Django": [r"csrftoken", r"django"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def check_fingerprint(requester, url):
|
|
22
|
+
console.print("[info] -> Analyzing server response...[/info]")
|
|
23
|
+
result = {"name": "Tech Fingerprint", "findings": [], "technologies": []}
|
|
24
|
+
resp = requester.get(url)
|
|
25
|
+
if not resp:
|
|
26
|
+
console.print("[error] x Connection failed[/error]")
|
|
27
|
+
return result
|
|
28
|
+
haystack = resp.text + "\n" + "\n".join(f"{k}: {v}" for k, v in resp.headers.items())
|
|
29
|
+
for tech, patterns in TECH_SIGNATURES.items():
|
|
30
|
+
for pat in patterns:
|
|
31
|
+
if re.search(pat, haystack, re.IGNORECASE):
|
|
32
|
+
console.print(f"[success] + Detected: {tech}[/success]")
|
|
33
|
+
result["technologies"].append(tech)
|
|
34
|
+
result["findings"].append({"severity": "info", "msg": f"Detected: {tech}"})
|
|
35
|
+
break
|
|
36
|
+
if not result["technologies"]:
|
|
37
|
+
console.print("[dim] i No known technologies detected[/dim]")
|
|
38
|
+
return result
|
core/headers_check.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from utils.colors import console
|
|
2
|
+
|
|
3
|
+
SECURITY_HEADERS = {
|
|
4
|
+
"Strict-Transport-Security": "Enforces HTTPS",
|
|
5
|
+
"Content-Security-Policy": "Mitigates XSS",
|
|
6
|
+
"X-Frame-Options": "Prevents Clickjacking",
|
|
7
|
+
"X-Content-Type-Options": "Prevents MIME sniffing",
|
|
8
|
+
"Referrer-Policy": "Controls referrer leakage",
|
|
9
|
+
"Permissions-Policy": "Restricts browser features",
|
|
10
|
+
"Cross-Origin-Opener-Policy":"Isolates windows",
|
|
11
|
+
"Cross-Origin-Resource-Policy":"Blocks cross-origin loads",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def check_headers(requester, url):
|
|
15
|
+
console.print("[info] -> Fetching HTTP headers...[/info]")
|
|
16
|
+
result = {"name": "Security Headers", "findings": [], "missing": [], "score": 0}
|
|
17
|
+
resp = requester.get(url)
|
|
18
|
+
if not resp:
|
|
19
|
+
console.print("[error] x Connection failed[/error]")
|
|
20
|
+
result["findings"].append({"severity": "info", "msg": "Could not connect"})
|
|
21
|
+
return result
|
|
22
|
+
console.print("[dim] OK Response received, analyzing...[/dim]")
|
|
23
|
+
present = 0
|
|
24
|
+
lower_keys = {k.lower() for k in resp.headers.keys()}
|
|
25
|
+
for header, desc in SECURITY_HEADERS.items():
|
|
26
|
+
if header.lower() in lower_keys:
|
|
27
|
+
console.print(f"[success] + {header}[/success]")
|
|
28
|
+
present += 1
|
|
29
|
+
else:
|
|
30
|
+
console.print(f"[warn] - {header} MISSING[/warn]")
|
|
31
|
+
result["missing"].append({"header": header, "desc": desc})
|
|
32
|
+
result["score"] = int((present / len(SECURITY_HEADERS)) * 100)
|
|
33
|
+
if result["missing"]:
|
|
34
|
+
result["findings"].append({
|
|
35
|
+
"severity": "low" if result["score"] > 50 else "medium",
|
|
36
|
+
"msg": f"{len(result['missing'])} missing security headers (score: {result['score']}%)",
|
|
37
|
+
})
|
|
38
|
+
return result
|
core/hostheader.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from utils.colors import console
|
|
2
|
+
|
|
3
|
+
def check_host_header(requester, url):
|
|
4
|
+
result = {"name": "Host Header Injection", "findings": [], "vulnerable": []}
|
|
5
|
+
console.print("[info] -> Testing Host header manipulation...[/info]")
|
|
6
|
+
|
|
7
|
+
evil_hosts = [
|
|
8
|
+
"evil.example.com",
|
|
9
|
+
"localhost",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
for host in evil_hosts:
|
|
13
|
+
console.print(f"[dim] -> Testing Host: {host}[/dim]")
|
|
14
|
+
resp = requester.get(url, headers={"Host": host})
|
|
15
|
+
if not resp:
|
|
16
|
+
continue
|
|
17
|
+
body = resp.text.lower()
|
|
18
|
+
if "evil.example.com" in body or host in body:
|
|
19
|
+
console.print(f"[vuln] !! Host header reflected in response: {host}[/vuln]")
|
|
20
|
+
result["vulnerable"].append({"host": host})
|
|
21
|
+
result["findings"].append({
|
|
22
|
+
"severity": "high",
|
|
23
|
+
"msg": f"Host header '{host}' reflected in response (password reset poisoning risk)",
|
|
24
|
+
})
|
|
25
|
+
break
|
|
26
|
+
|
|
27
|
+
if not result["vulnerable"]:
|
|
28
|
+
console.print("[success] + Host header not reflected[/success]")
|
|
29
|
+
return result
|
core/idor.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
def check_idor(requester, url):
|
|
5
|
+
result = {"name": "IDOR", "findings": [], "vulnerable": []}
|
|
6
|
+
parsed = urlparse(url)
|
|
7
|
+
params = parse_qs(parsed.query)
|
|
8
|
+
numeric_params = {k: v for k, v in params.items() if v and v[0].isdigit()}
|
|
9
|
+
if not numeric_params:
|
|
10
|
+
console.print("[dim] i No numeric parameters found, skipping[/dim]")
|
|
11
|
+
return result
|
|
12
|
+
console.print(f"[info] -> Testing {len(numeric_params)} numeric param(s) for IDOR...[/info]")
|
|
13
|
+
for param, values in numeric_params.items():
|
|
14
|
+
original_id = int(values[0])
|
|
15
|
+
console.print(f"[dim] -> Param: {param} (id={original_id})[/dim]")
|
|
16
|
+
for test_id in [original_id - 1, original_id + 1, original_id + 100, 1, 0]:
|
|
17
|
+
if test_id < 0 or test_id == original_id:
|
|
18
|
+
continue
|
|
19
|
+
q = params.copy()
|
|
20
|
+
q[param] = [str(test_id)]
|
|
21
|
+
test_url = urlunparse(parsed._replace(query=urlencode(q, doseq=True)))
|
|
22
|
+
resp = requester.get(test_url)
|
|
23
|
+
if resp and resp.status_code == 200 and len(resp.text) > 100:
|
|
24
|
+
console.print(f"[warn] ? Different response for {param}={test_id} (potential IDOR)[/warn]")
|
|
25
|
+
result["findings"].append({
|
|
26
|
+
"severity": "medium",
|
|
27
|
+
"msg": f"Possible IDOR via {param}={test_id} (manual verification needed)",
|
|
28
|
+
})
|
|
29
|
+
break
|
|
30
|
+
if not result["findings"]:
|
|
31
|
+
console.print("[success] + No obvious IDOR found[/success]")
|
|
32
|
+
return result
|
core/jwt.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from utils.colors import console
|
|
5
|
+
|
|
6
|
+
JWT_REGEX = re.compile(r"eyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]*")
|
|
7
|
+
|
|
8
|
+
def _b64decode_pad(s):
|
|
9
|
+
s += "=" * (-len(s) % 4)
|
|
10
|
+
try:
|
|
11
|
+
return base64.urlsafe_b64decode(s).decode("utf-8", errors="ignore")
|
|
12
|
+
except Exception:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
def check_jwt(requester, url):
|
|
16
|
+
result = {"name": "JWT Analyzer", "findings": [], "vulnerable": []}
|
|
17
|
+
console.print("[info] -> Scanning for JWT tokens in response...[/info]")
|
|
18
|
+
resp = requester.get(url)
|
|
19
|
+
if not resp:
|
|
20
|
+
console.print("[error] x Connection failed[/error]")
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
# ندور في الرد والـ headers
|
|
24
|
+
haystack = resp.text + "\n" + "\n".join(f"{k}: {v}" for k, v in resp.headers.items())
|
|
25
|
+
tokens = set(JWT_REGEX.findall(haystack))
|
|
26
|
+
|
|
27
|
+
if not tokens:
|
|
28
|
+
console.print("[dim] i No JWT tokens found[/dim]")
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
console.print(f"[dim] Found {len(tokens)} JWT token(s)[/dim]")
|
|
32
|
+
|
|
33
|
+
for token in tokens:
|
|
34
|
+
parts = token.split(".")
|
|
35
|
+
if len(parts) != 3:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
header = _b64decode_pad(parts[0])
|
|
39
|
+
payload = _b64decode_pad(parts[1])
|
|
40
|
+
console.print(f"[dim] Header: {header[:100]}[/dim]")
|
|
41
|
+
console.print(f"[dim] Payload: {payload[:100]}[/dim]")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
header_json = json.loads(header)
|
|
45
|
+
alg = header_json.get("alg", "").lower()
|
|
46
|
+
except Exception:
|
|
47
|
+
alg = ""
|
|
48
|
+
|
|
49
|
+
if alg == "none":
|
|
50
|
+
console.print("[vuln] !! JWT with 'alg: none'![/vuln]")
|
|
51
|
+
result["findings"].append({
|
|
52
|
+
"severity": "critical",
|
|
53
|
+
"msg": "JWT uses 'alg: none' (signature bypass)",
|
|
54
|
+
})
|
|
55
|
+
result["vulnerable"].append({"issue": "alg_none"})
|
|
56
|
+
|
|
57
|
+
if alg in ("hs256", "hs384", "hs512"):
|
|
58
|
+
console.print(f"[warn] ! JWT uses {alg.upper()} (test weak secret)[/warn]")
|
|
59
|
+
result["findings"].append({
|
|
60
|
+
"severity": "low",
|
|
61
|
+
"msg": f"JWT uses HMAC ({alg}) — test for weak secret",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
payload_json = json.loads(payload)
|
|
66
|
+
if "exp" not in payload_json:
|
|
67
|
+
console.print("[warn] ! JWT has no expiration (exp)[/warn]")
|
|
68
|
+
result["findings"].append({
|
|
69
|
+
"severity": "medium",
|
|
70
|
+
"msg": "JWT has no 'exp' claim",
|
|
71
|
+
})
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
if not result["findings"]:
|
|
76
|
+
console.print("[success] + No JWT issues found[/success]")
|
|
77
|
+
return result
|
core/lfi.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
3
|
+
from utils.colors import console
|
|
4
|
+
|
|
5
|
+
LFI_PAYLOADS = [
|
|
6
|
+
"../../../../etc/passwd",
|
|
7
|
+
"../../../../../../etc/passwd",
|
|
8
|
+
"....//....//....//etc/passwd",
|
|
9
|
+
"..%2F..%2F..%2F..%2Fetc%2Fpasswd",
|
|
10
|
+
"/etc/passwd",
|
|
11
|
+
"....//....//....//....//etc/passwd",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
LFI_PATTERNS = [
|
|
15
|
+
r"root:.*:0:0:",
|
|
16
|
+
r"daemon:.*:1:1:",
|
|
17
|
+
r"\[boot loader\]",
|
|
18
|
+
r"\[extensions\]",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def check_lfi(requester, url):
|
|
22
|
+
result = {"name": "LFI", "findings": [], "vulnerable": []}
|
|
23
|
+
parsed = urlparse(url)
|
|
24
|
+
params = parse_qs(parsed.query)
|
|
25
|
+
if not params:
|
|
26
|
+
console.print("[dim] i No parameters found, skipping[/dim]")
|
|
27
|
+
return result
|
|
28
|
+
console.print(f"[info] -> Testing {len(params)} param(s) with {len(LFI_PAYLOADS)} payloads...[/info]")
|
|
29
|
+
for param in params:
|
|
30
|
+
console.print(f"[dim] -> Param: {param}[/dim]")
|
|
31
|
+
for i, payload in enumerate(LFI_PAYLOADS, 1):
|
|
32
|
+
q = params.copy()
|
|
33
|
+
q[param] = [payload]
|
|
34
|
+
test_url = urlunparse(parsed._replace(query=urlencode(q, doseq=True)))
|
|
35
|
+
console.print(f"[dim] [{i}/{len(LFI_PAYLOADS)}] {payload}[/dim]")
|
|
36
|
+
resp = requester.get(test_url)
|
|
37
|
+
if resp:
|
|
38
|
+
for pat in LFI_PATTERNS:
|
|
39
|
+
if re.search(pat, resp.text):
|
|
40
|
+
console.print(f"[vuln] !! LFI via {param}![/vuln]")
|
|
41
|
+
result["vulnerable"].append({"param": param, "payload": payload})
|
|
42
|
+
result["findings"].append({
|
|
43
|
+
"severity": "high",
|
|
44
|
+
"msg": f"LFI via param: {param} (payload: {payload})",
|
|
45
|
+
})
|
|
46
|
+
return result
|
|
47
|
+
if not result["vulnerable"]:
|
|
48
|
+
console.print("[success] + No LFI found[/success]")
|
|
49
|
+
return result
|
core/nosqli.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
NOSQL_ERRORS = [
|
|
5
|
+
"mongoerror",
|
|
6
|
+
"mongo",
|
|
7
|
+
"$where",
|
|
8
|
+
"unexpected token",
|
|
9
|
+
"json parse error",
|
|
10
|
+
"cast to objectid failed",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
NOSQL_PAYLOADS = [
|
|
14
|
+
'{"$ne": null}',
|
|
15
|
+
'{"$gt": ""}',
|
|
16
|
+
"' || '1'=='1",
|
|
17
|
+
'"; return true; var x="',
|
|
18
|
+
"[$ne]=1",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def check_nosqli(requester, url):
|
|
22
|
+
result = {"name": "NoSQL Injection", "findings": [], "vulnerable": []}
|
|
23
|
+
parsed = urlparse(url)
|
|
24
|
+
params = parse_qs(parsed.query)
|
|
25
|
+
if not params:
|
|
26
|
+
console.print("[dim] i No parameters found, skipping[/dim]")
|
|
27
|
+
return result
|
|
28
|
+
console.print(f"[info] -> Testing {len(params)} param(s) with {len(NOSQL_PAYLOADS)} payloads...[/info]")
|
|
29
|
+
for param in params:
|
|
30
|
+
console.print(f"[dim] -> Param: {param}[/dim]")
|
|
31
|
+
for i, payload in enumerate(NOSQL_PAYLOADS, 1):
|
|
32
|
+
console.print(f"[dim] [{i}/{len(NOSQL_PAYLOADS)}] {payload[:40]}[/dim]")
|
|
33
|
+
q = params.copy()
|
|
34
|
+
q[param] = [payload]
|
|
35
|
+
test_url = urlunparse(parsed._replace(query=urlencode(q, doseq=True)))
|
|
36
|
+
resp = requester.get(test_url)
|
|
37
|
+
if not resp:
|
|
38
|
+
continue
|
|
39
|
+
body = resp.text.lower()
|
|
40
|
+
for err in NOSQL_ERRORS:
|
|
41
|
+
if err in body:
|
|
42
|
+
console.print(f"[vuln] !! NoSQL injection possible via {param}! Pattern: {err}[/vuln]")
|
|
43
|
+
result["vulnerable"].append({"param": param, "payload": payload})
|
|
44
|
+
result["findings"].append({
|
|
45
|
+
"severity": "critical",
|
|
46
|
+
"msg": f"NoSQL injection possible via param: {param}",
|
|
47
|
+
})
|
|
48
|
+
break
|
|
49
|
+
if not result["vulnerable"]:
|
|
50
|
+
console.print("[success] + No NoSQL injection found[/success]")
|
|
51
|
+
return result
|
core/redirect.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
PAYLOADS = [
|
|
5
|
+
"https://evil.example.com",
|
|
6
|
+
"//evil.example.com",
|
|
7
|
+
"https://evil.example.com/%2f..",
|
|
8
|
+
"/\\evil.example.com",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
def _build_url(parsed, param, value, original_query):
|
|
12
|
+
q = original_query.copy()
|
|
13
|
+
q[param] = [value]
|
|
14
|
+
return urlunparse(parsed._replace(query=urlencode(q, doseq=True)))
|
|
15
|
+
|
|
16
|
+
def check_redirect(requester, url):
|
|
17
|
+
result = {"name": "Open Redirect", "findings": [], "vulnerable": []}
|
|
18
|
+
parsed = urlparse(url)
|
|
19
|
+
params = parse_qs(parsed.query)
|
|
20
|
+
if not params:
|
|
21
|
+
console.print("[dim] i No parameters found, skipping[/dim]")
|
|
22
|
+
return result
|
|
23
|
+
console.print(f"[info] -> Testing {len(params)} param(s) with {len(PAYLOADS)} payloads...[/info]")
|
|
24
|
+
for param in params:
|
|
25
|
+
console.print(f"[dim] -> Param: {param}[/dim]")
|
|
26
|
+
for payload in PAYLOADS:
|
|
27
|
+
test_url = _build_url(parsed, param, payload, params)
|
|
28
|
+
resp = requester.get(test_url)
|
|
29
|
+
if not resp:
|
|
30
|
+
continue
|
|
31
|
+
loc = resp.headers.get("Location", "")
|
|
32
|
+
if resp.status_code in (301, 302, 303, 307, 308) and "evil.example.com" in loc:
|
|
33
|
+
console.print(f"[vuln] !! VULNERABLE via {param}![/vuln]")
|
|
34
|
+
result["vulnerable"].append({"param": param, "payload": payload, "location": loc})
|
|
35
|
+
result["findings"].append({"severity": "high", "msg": f"Open Redirect via param: {param}"})
|
|
36
|
+
break
|
|
37
|
+
if not result["vulnerable"]:
|
|
38
|
+
console.print("[success] + No open redirect found[/success]")
|
|
39
|
+
return result
|
core/requester.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from requests.adapters import HTTPAdapter
|
|
3
|
+
from urllib3.util.retry import Retry
|
|
4
|
+
|
|
5
|
+
DEFAULT_TIMEOUT = 10
|
|
6
|
+
DEFAULT_UA = "Mozilla/5.0 (X11; Linux x86_64) VpiXray/0.1"
|
|
7
|
+
|
|
8
|
+
class Requester:
|
|
9
|
+
def __init__(self, timeout=DEFAULT_TIMEOUT, headers=None, proxy=None, verify=False, cookie=None):
|
|
10
|
+
self.timeout = timeout
|
|
11
|
+
self.session = requests.Session()
|
|
12
|
+
self.session.headers.update({"User-Agent": DEFAULT_UA})
|
|
13
|
+
if headers:
|
|
14
|
+
self.session.headers.update(headers)
|
|
15
|
+
if proxy:
|
|
16
|
+
self.session.proxies = {"http": proxy, "https": proxy}
|
|
17
|
+
if cookie:
|
|
18
|
+
for item in cookie.split(";"):
|
|
19
|
+
item = item.strip()
|
|
20
|
+
if "=" in item:
|
|
21
|
+
k, v = item.split("=", 1)
|
|
22
|
+
self.session.cookies.set(k.strip(), v.strip())
|
|
23
|
+
retries = Retry(total=2, backoff_factor=0.3, status_forcelist=[500, 502, 503, 504])
|
|
24
|
+
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
25
|
+
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
26
|
+
self.verify = verify
|
|
27
|
+
|
|
28
|
+
def get(self, url, **kwargs):
|
|
29
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
30
|
+
kwargs.setdefault("verify", self.verify)
|
|
31
|
+
kwargs.setdefault("allow_redirects", False)
|
|
32
|
+
try:
|
|
33
|
+
return self.session.get(url, **kwargs)
|
|
34
|
+
except requests.RequestException:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def post(self, url, **kwargs):
|
|
38
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
39
|
+
kwargs.setdefault("verify", self.verify)
|
|
40
|
+
kwargs.setdefault("allow_redirects", False)
|
|
41
|
+
try:
|
|
42
|
+
return self.session.post(url, **kwargs)
|
|
43
|
+
except requests.RequestException:
|
|
44
|
+
return None
|
core/sensitive.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from utils.colors import console
|
|
3
|
+
|
|
4
|
+
DEFAULT_FILES = [
|
|
5
|
+
".env", ".git/config", ".git/HEAD", "backup.zip", "backup.tar.gz",
|
|
6
|
+
"config.php.bak", "wp-config.php.bak", ".DS_Store", "phpinfo.php",
|
|
7
|
+
"admin/", "robots.txt", "sitemap.xml", ".htaccess", "web.config",
|
|
8
|
+
"composer.json", "package.json", "Dockerfile", ".dockerignore",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
def load_wordlist(path=None):
|
|
12
|
+
if path and os.path.isfile(path):
|
|
13
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
14
|
+
return [l.strip() for l in f if l.strip() and not l.startswith("#")]
|
|
15
|
+
return DEFAULT_FILES
|
|
16
|
+
|
|
17
|
+
def check_sensitive(requester, base_url, wordlist_path=None):
|
|
18
|
+
files = load_wordlist(wordlist_path)
|
|
19
|
+
console.print(f"[info] -> Testing {len(files)} sensitive paths...[/info]")
|
|
20
|
+
result = {"name": "Sensitive Files", "findings": [], "hits": []}
|
|
21
|
+
base = base_url.rstrip("/")
|
|
22
|
+
hits = 0
|
|
23
|
+
for i, path in enumerate(files, 1):
|
|
24
|
+
url = f"{base}/{path}"
|
|
25
|
+
console.print(f"[dim] [{i}/{len(files)}] -> /{path}[/dim]")
|
|
26
|
+
resp = requester.get(url)
|
|
27
|
+
if resp and resp.status_code in (200, 301, 302, 403):
|
|
28
|
+
size = len(resp.content)
|
|
29
|
+
hits += 1
|
|
30
|
+
result["hits"].append({"url": url, "status": resp.status_code, "size": size})
|
|
31
|
+
sev = "high" if resp.status_code == 200 else "low"
|
|
32
|
+
console.print(f"[vuln] !! HIT [{resp.status_code}] {url} ({size} bytes)[/vuln]")
|
|
33
|
+
result["findings"].append({
|
|
34
|
+
"severity": sev,
|
|
35
|
+
"msg": f"[{resp.status_code}] {url} ({size} bytes)",
|
|
36
|
+
})
|
|
37
|
+
if hits == 0:
|
|
38
|
+
console.print("[success] + No sensitive files exposed[/success]")
|
|
39
|
+
else:
|
|
40
|
+
console.print(f"[error] !! {hits} sensitive file(s) found[/error]")
|
|
41
|
+
return result
|