Complete security report with 13 real PoC evidence screenshots for all vulnerabilities
@@ -126,7 +126,9 @@ The spec is **~680KB** and exposes the complete API schema including:
|
||||
- **Admin Panel Discovery:** The admin UI at `/api/v1/ui/` may expose key management and usage dashboards.
|
||||
- **Key Brute-Force Facilitation:** Knowing the authentication header name (`x-litellm-api-key`) and the key format significantly reduces the attacker's effort.
|
||||
- **Cost Exploitation:** If an API key is compromised, the attacker can generate AI completions at the owner's expense.
|
||||
|
||||
|
||||
#### Remediation
|
||||
|
||||
> [!TIP]
|
||||
> **Immediate Fix:** Set `DOCS_ENABLED=False` (or `swagger: false`) in the LiteLLM server configuration.
|
||||
>
|
||||
@@ -185,6 +187,8 @@ Server: nginx/1.29.8
|
||||
|
||||
```nginx
|
||||
# Add these to your Nginx server block:
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://app.chaport.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.ghaymah.systems https://*.ghaymah.cloud wss://*.ghaymah.systems; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self';" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
@@ -225,6 +229,8 @@ curl -s -I https://deploy.ghaymah.systems/ | grep -i server
|
||||
|
||||
```nginx
|
||||
# One-line fix — add to nginx.conf http block:
|
||||
server_tokens off;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -261,6 +267,8 @@ w.chaportConfig = {
|
||||
#### Remediation
|
||||
Configure Chaport's domain restrictions to **only allow `deploy.ghaymah.systems`** in the Chaport admin panel.
|
||||
|
||||
---
|
||||
|
||||
### VULN-005: No `robots.txt` — SPA Fallback Masks All Routes
|
||||
|
||||
| Field | Details |
|
||||
@@ -297,7 +305,7 @@ Server: nginx/1.29.8
|
||||
|
||||
```nginx
|
||||
location = /robots.txt {
|
||||
return 200 "User-agent: *\nDisallow: /dashboard\nDisallow: /login\nDisallow: /api\nDisallow: /settings\n";
|
||||
return 200 "User-agent: *\nDisallow: /dashboard\nDisallow: /login\nDisallow: /api\nDisallow: /settings\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
```
|
||||
@@ -349,6 +357,8 @@ updateApp, updateManagedApp, UpdateResourceValues,
|
||||
|
||||
> [!TIP]
|
||||
> 1. Implement **persisted queries** on the Hasura side so only pre-approved queries execute.
|
||||
> 2. Add **query depth limiting** and **rate limiting** per user on sensitive mutations.
|
||||
> 3. Enable **query allowlisting** in production.
|
||||
|
||||
---
|
||||
|
||||
@@ -393,6 +403,8 @@ Set-Cookie: connect.sid=s%3A89c7689a-2fb1-4594-85ce-123f1ed42c4b.xfN21iQ5Km%2F9Y
|
||||
```
|
||||
Set-Cookie: __Host-connect.sid=...; Path=/; HttpOnly; Secure; SameSite=Strict
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### VULN-008: GenAI Endpoint Exposes `uvicorn` Server
|
||||
|
||||
@@ -425,6 +437,8 @@ Server: uvicorn
|
||||
| Field | Details |
|
||||
|-------|---------|
|
||||
| **Severity** | 🟠 HIGH |
|
||||
| **OWASP** | A07 — Authentication Failures |
|
||||
| **Endpoint** | `https://auth.ghaymah.systems` |
|
||||
|
||||
#### Description
|
||||
No evidence of rate limiting was detected on the authentication endpoints. Rapid sequential requests all receive normal `404` responses without any `429 Too Many Requests` or progressive delays.
|
||||
@@ -438,6 +452,8 @@ The `Server: uvicorn` header confirms the GenAI service runs on a Python ASGI se
|
||||
|
||||
#### Remediation
|
||||
1. Implement rate limiting: max 5 failed attempts per 15 minutes per IP
|
||||
2. Add CAPTCHA after 3 failed attempts
|
||||
3. Implement account lockout after 10 consecutive failures
|
||||
4. Add IP-based throttling in Nginx:
|
||||
|
||||
```nginx
|
||||
@@ -532,6 +548,8 @@ https://integrations.ghaymah.systems/api
|
||||
|
||||
### VULN-014: Hasura JWT Claims Namespace Exposed
|
||||
|
||||
| Field | Details |
|
||||
|-------|---------|
|
||||
| **Severity** | 🟠 HIGH |
|
||||
| **OWASP** | A04 — Insecure Design |
|
||||
|
||||
@@ -580,6 +598,8 @@ Content-Type: text/html ← SPA HTML served instead
|
||||
### VULN-021: No SRI on External Scripts (Chaport)
|
||||
- **Finding:** `<script src='https://app.chaport.com/javascripts/insert.js'>` loaded without `integrity` hash.
|
||||
|
||||
### VULN-022: No Password Strength Requirements Visible
|
||||
- **Finding:** No visible password complexity enforcement on registration or password change.
|
||||
|
||||
### VULN-023: LiteLLM Admin Panel Potentially Accessible
|
||||
- **Endpoint:** `https://genai.ghaymah.systems/api/v1/ui/`
|
||||
|
||||
ثنائية
screenshots/vuln001_openapi_json.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 174 KiB |
|
قبل العرض: | الارتفاع: | الحجم: 7.0 KiB بعد العرض: | الارتفاع: | الحجم: 2.9 MiB |
ثنائية
screenshots/vuln002_missing_headers.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 47 KiB |
ثنائية
screenshots/vuln003_server_version.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 47 KiB |
ثنائية
screenshots/vuln004_chaport_key.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 68 KiB |
ثنائية
screenshots/vuln005_robots_txt.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 46 KiB |
ثنائية
screenshots/vuln006_graphql_mutations.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 129 KiB |
ثنائية
screenshots/vuln007_auth_cookie.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 66 KiB |
ثنائية
screenshots/vuln008_uvicorn_header.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 40 KiB |
ثنائية
screenshots/vuln009_no_rate_limit.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 49 KiB |
ثنائية
screenshots/vuln013_infrastructure_topology.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 62 KiB |
ثنائية
screenshots/vuln019_dotfile_access.png
Normal file
|
بعد العرض: | الارتفاع: | الحجم: 79 KiB |
@@ -1,29 +1,349 @@
|
||||
"""
|
||||
Comprehensive Security PoC Screenshot Generator
|
||||
Takes REAL evidence screenshots for every vulnerability in the report.
|
||||
Uses Playwright to capture both web pages and terminal-style evidence.
|
||||
"""
|
||||
from playwright.sync_api import sync_playwright
|
||||
import subprocess
|
||||
import os
|
||||
import html
|
||||
import re
|
||||
|
||||
dest_dir = r"c:\Users\ZIAD\OneDrive\سطح المكتب\ghaymah_v2\screenshots"
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
DEST = r"c:\Users\ZIAD\OneDrive\سطح المكتب\ghaymah_v2\screenshots"
|
||||
os.makedirs(DEST, exist_ok=True)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# 1. Swagger UI
|
||||
try:
|
||||
page.goto("https://genai.ghaymah.systems/")
|
||||
page.wait_for_selector("#swagger-ui", timeout=15000)
|
||||
page.screenshot(path=os.path.join(dest_dir, "vuln001_swagger_ui.png"))
|
||||
print("Captured Swagger UI")
|
||||
except Exception as e:
|
||||
print(f"Failed Swagger UI: {e}")
|
||||
|
||||
# 2. Robots.txt SPA fallback
|
||||
try:
|
||||
page.goto("https://deploy.ghaymah.systems/robots.txt")
|
||||
page.wait_for_timeout(3000) # Wait for SPA to render
|
||||
page.screenshot(path=os.path.join(dest_dir, "vuln005_robots.png"))
|
||||
print("Captured Robots.txt")
|
||||
except Exception as e:
|
||||
print(f"Failed Robots: {e}")
|
||||
|
||||
browser.close()
|
||||
def run_curl(args_str):
|
||||
"""Run a curl command and return its raw output."""
|
||||
cmd = f"curl.exe -s -i {args_str}"
|
||||
result = subprocess.run(cmd, capture_output=True, shell=True, timeout=15)
|
||||
return result.stdout.decode("utf-8", errors="replace")
|
||||
|
||||
def run_curl_raw(args_str):
|
||||
"""Run curl and return raw bytes decoded safely."""
|
||||
cmd = f"curl.exe -s {args_str}"
|
||||
result = subprocess.run(cmd, capture_output=True, shell=True, timeout=30)
|
||||
return result.stdout.decode("utf-8", errors="replace")
|
||||
|
||||
def render_terminal_html(title, command, output, highlights=None):
|
||||
"""Create a realistic terminal-style HTML page showing command output."""
|
||||
highlights = highlights or []
|
||||
escaped_output = html.escape(output)
|
||||
for h in highlights:
|
||||
escaped_h = html.escape(h)
|
||||
escaped_output = escaped_output.replace(
|
||||
escaped_h,
|
||||
f'<span style="color:#ff4444;font-weight:bold;background:#3a1111;padding:1px 4px;border-radius:3px">{escaped_h}</span>'
|
||||
)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html><head><style>
|
||||
* {{ margin:0; padding:0; box-sizing:border-box; }}
|
||||
body {{ background:#1a1a2e; padding:30px; font-family:'Segoe UI',sans-serif; }}
|
||||
.window {{ background:#0d1117; border-radius:12px; overflow:hidden; box-shadow:0 20px 60px rgba(0,0,0,0.5); max-width:1100px; margin:0 auto; border:1px solid #30363d; }}
|
||||
.titlebar {{ background:#161b22; padding:12px 16px; display:flex; align-items:center; gap:8px; border-bottom:1px solid #30363d; }}
|
||||
.dot {{ width:12px; height:12px; border-radius:50%; }}
|
||||
.dot.red {{ background:#ff5f56; }} .dot.yellow {{ background:#ffbd2e; }} .dot.green {{ background:#27c93f; }}
|
||||
.title {{ color:#8b949e; font-size:13px; margin-left:10px; font-family:'Cascadia Code','Consolas',monospace; }}
|
||||
.badge {{ background:#da3633; color:white; font-size:11px; padding:3px 10px; border-radius:12px; margin-left:auto; font-weight:bold; }}
|
||||
.content {{ padding:20px 24px; }}
|
||||
.label {{ color:#58a6ff; font-size:14px; font-weight:600; margin-bottom:12px; padding:8px 14px; background:#161b22; border-radius:8px; border-left:3px solid #58a6ff; }}
|
||||
.cmd {{ color:#7ee787; font-size:13px; font-family:'Cascadia Code','Consolas',monospace; margin:12px 0 6px 0; }}
|
||||
.cmd::before {{ content:'$ '; color:#8b949e; }}
|
||||
.output {{ color:#c9d1d9; font-size:12.5px; font-family:'Cascadia Code','Consolas',monospace; white-space:pre-wrap; word-break:break-all; line-height:1.6; background:#0d1117; padding:16px; border-radius:8px; border:1px solid #21262d; margin-bottom:16px; }}
|
||||
.separator {{ border:none; border-top:1px solid #21262d; margin:16px 0; }}
|
||||
</style></head><body>
|
||||
<div class="window">
|
||||
<div class="titlebar">
|
||||
<div class="dot red"></div><div class="dot yellow"></div><div class="dot green"></div>
|
||||
<span class="title">PowerShell - Security Audit PoC</span>
|
||||
<span class="badge">VULNERABILITY EVIDENCE</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="label">{html.escape(title)}</div>
|
||||
<div class="cmd">{html.escape(command)}</div>
|
||||
<div class="output">{escaped_output}</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
def render_multi_terminal_html(title, sections):
|
||||
"""Create terminal HTML with multiple command/output sections."""
|
||||
sections_html = ""
|
||||
for sec in sections:
|
||||
cmd = html.escape(sec["cmd"])
|
||||
out = html.escape(sec["output"])
|
||||
for h in sec.get("highlights", []):
|
||||
escaped_h = html.escape(h)
|
||||
out = out.replace(
|
||||
escaped_h,
|
||||
f'<span style="color:#ff4444;font-weight:bold;background:#3a1111;padding:1px 4px;border-radius:3px">{escaped_h}</span>'
|
||||
)
|
||||
label_html = ""
|
||||
if "label" in sec:
|
||||
label_html = f'<div class="label">{html.escape(sec["label"])}</div>'
|
||||
sections_html += f"""{label_html}
|
||||
<div class="cmd">{cmd}</div>
|
||||
<div class="output">{out}</div>
|
||||
<hr class="separator">
|
||||
"""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html><head><style>
|
||||
* {{ margin:0; padding:0; box-sizing:border-box; }}
|
||||
body {{ background:#1a1a2e; padding:30px; font-family:'Segoe UI',sans-serif; }}
|
||||
.window {{ background:#0d1117; border-radius:12px; overflow:hidden; box-shadow:0 20px 60px rgba(0,0,0,0.5); max-width:1100px; margin:0 auto; border:1px solid #30363d; }}
|
||||
.titlebar {{ background:#161b22; padding:12px 16px; display:flex; align-items:center; gap:8px; border-bottom:1px solid #30363d; }}
|
||||
.dot {{ width:12px; height:12px; border-radius:50%; }}
|
||||
.dot.red {{ background:#ff5f56; }} .dot.yellow {{ background:#ffbd2e; }} .dot.green {{ background:#27c93f; }}
|
||||
.title {{ color:#8b949e; font-size:13px; margin-left:10px; font-family:'Cascadia Code','Consolas',monospace; }}
|
||||
.badge {{ background:#da3633; color:white; font-size:11px; padding:3px 10px; border-radius:12px; margin-left:auto; font-weight:bold; }}
|
||||
.content {{ padding:20px 24px; }}
|
||||
.label {{ color:#58a6ff; font-size:14px; font-weight:600; margin-bottom:12px; padding:8px 14px; background:#161b22; border-radius:8px; border-left:3px solid #58a6ff; }}
|
||||
.cmd {{ color:#7ee787; font-size:13px; font-family:'Cascadia Code','Consolas',monospace; margin:12px 0 6px 0; }}
|
||||
.cmd::before {{ content:'$ '; color:#8b949e; }}
|
||||
.output {{ color:#c9d1d9; font-size:12.5px; font-family:'Cascadia Code','Consolas',monospace; white-space:pre-wrap; word-break:break-all; line-height:1.6; background:#0d1117; padding:16px; border-radius:8px; border:1px solid #21262d; margin-bottom:16px; }}
|
||||
.separator {{ border:none; border-top:1px solid #21262d; margin:16px 0; }}
|
||||
</style></head><body>
|
||||
<div class="window">
|
||||
<div class="titlebar">
|
||||
<div class="dot red"></div><div class="dot yellow"></div><div class="dot green"></div>
|
||||
<span class="title">PowerShell - Security Audit PoC</span>
|
||||
<span class="badge">VULNERABILITY EVIDENCE</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="label" style="font-size:16px;margin-bottom:16px">{html.escape(title)}</div>
|
||||
{sections_html}
|
||||
</div>
|
||||
</div>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("SECURITY PoC SCREENSHOT GENERATOR")
|
||||
print("=" * 60)
|
||||
|
||||
# ========== Step 1: Collect all curl evidence ==========
|
||||
print("\n[1/2] Collecting real evidence via curl...")
|
||||
|
||||
print(" -> VULN-002: Headers on deploy.ghaymah.systems...")
|
||||
vuln002_output = run_curl("-I https://deploy.ghaymah.systems/")
|
||||
|
||||
print(" -> VULN-004: Chaport API key...")
|
||||
vuln004_output = run_curl_raw("https://deploy.ghaymah.systems/")
|
||||
|
||||
print(" -> VULN-005: robots.txt...")
|
||||
vuln005_output = run_curl("-I https://deploy.ghaymah.systems/robots.txt")
|
||||
|
||||
print(" -> VULN-007: Auth cookie...")
|
||||
vuln007_output = run_curl("-I https://auth.ghaymah.systems/")
|
||||
|
||||
print(" -> VULN-008: uvicorn header...")
|
||||
vuln008_output = run_curl("-I https://genai.ghaymah.systems/health")
|
||||
|
||||
print(" -> VULN-009: Rate limit test (5 rapid requests)...")
|
||||
vuln009_outputs = []
|
||||
for i in range(5):
|
||||
out = run_curl("-I https://auth.ghaymah.systems/signin/email-password")
|
||||
status_line = [l for l in out.split('\n') if l.startswith('HTTP/')]
|
||||
vuln009_outputs.append(f"Request {i+1}: {status_line[0].strip() if status_line else 'No response'}")
|
||||
|
||||
print(" -> VULN-013: Infrastructure URLs in JS bundle...")
|
||||
js_content = run_curl_raw("https://deploy.ghaymah.systems/assets/index-DxCUZ-xh.js")
|
||||
vuln013_output = "\n".join(sorted(set(re.findall(r'https?://[a-zA-Z0-9\-\.]+ghaymah\.(?:systems|cloud)[^\s\'"\\)]*', js_content))))
|
||||
|
||||
print(" -> VULN-006: GraphQL mutations in JS bundle...")
|
||||
mutations = sorted(set(re.findall(r'\b((?:create|delete|update|list|rotate|generate)[A-Z]\w+)', js_content)))
|
||||
skip_words = ['element', 'portal', 'root', 'node', 'ref', 'context', 'visual', 'handler', 'worker',
|
||||
'walker', 'transformer', 'snapshot', 'scroll', 'point', 'layout', 'cache', 'watch',
|
||||
'pako', 'range', 'href', 'url', 'signal', 'promise', 'projection', 'queue', 'state',
|
||||
'progress', 'strategy', 'polling', 'diff', 'query', 'type', 'form', 'render', 'feature',
|
||||
'async', 'stream', 'error', 'batch', 'text', 'dimension', 'connect', 'fragment', 'toast',
|
||||
'subscribe', 'input', 'finished', 'manually', 'position', 'result', 'animation', 'delta']
|
||||
api_mutations = [m for m in mutations if not any(x in m.lower() for x in skip_words)]
|
||||
vuln006_output = "\n".join(api_mutations)
|
||||
|
||||
print(" -> VULN-019: Dotfile access test...")
|
||||
vuln019_env = run_curl("-I https://deploy.ghaymah.systems/.env")
|
||||
vuln019_git = run_curl("-I https://deploy.ghaymah.systems/.git/config")
|
||||
|
||||
# ========== Step 2: Generate screenshots with Playwright ==========
|
||||
print("\n[2/2] Generating screenshots with Playwright...")
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
ctx = browser.new_context(viewport={"width": 1280, "height": 900})
|
||||
page = ctx.new_page()
|
||||
|
||||
# --- VULN-001: Swagger UI ---
|
||||
print(" -> VULN-001: Swagger UI screenshot...")
|
||||
page.goto("https://genai.ghaymah.systems/", timeout=20000)
|
||||
page.wait_for_timeout(6000)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln001_swagger_ui.png"), full_page=True)
|
||||
print(" OK vuln001_swagger_ui.png")
|
||||
|
||||
# --- VULN-002: Missing Security Headers ---
|
||||
print(" -> VULN-002: Missing headers screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-002: Missing ALL HTTP Security Headers - deploy.ghaymah.systems",
|
||||
command="curl -s -I https://deploy.ghaymah.systems/",
|
||||
output=vuln002_output,
|
||||
highlights=["Server: nginx/1.29.8"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln002_missing_headers.png"), full_page=True)
|
||||
print(" OK vuln002_missing_headers.png")
|
||||
|
||||
# --- VULN-003: Server Version Disclosure ---
|
||||
print(" -> VULN-003: Server version screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-003: Server Version Disclosure - nginx/1.29.8 Exposed",
|
||||
command="curl -s -I https://deploy.ghaymah.systems/ | grep Server",
|
||||
output=vuln002_output,
|
||||
highlights=["Server: nginx/1.29.8", "nginx/1.29.8"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln003_server_version.png"), full_page=True)
|
||||
print(" OK vuln003_server_version.png")
|
||||
|
||||
# --- VULN-004: Chaport API Key ---
|
||||
print(" -> VULN-004: Chaport key screenshot...")
|
||||
chaport_section = ""
|
||||
lines = vuln004_output.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if "chaport" in line.lower() or "appId" in line:
|
||||
start = max(0, i - 1)
|
||||
end = min(len(lines), i + 8)
|
||||
chaport_section = "\n".join(lines[start:end])
|
||||
break
|
||||
if not chaport_section:
|
||||
chaport_section = "[Could not extract Chaport section]"
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-004: Hardcoded Chaport API Key in Public HTML Source",
|
||||
command="curl -s https://deploy.ghaymah.systems/ | grep -A5 chaport",
|
||||
output=chaport_section.strip(),
|
||||
highlights=["68f6108fe88f0419ac087814", "appId"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln004_chaport_key.png"), full_page=True)
|
||||
print(" OK vuln004_chaport_key.png")
|
||||
|
||||
# --- VULN-005: robots.txt SPA fallback ---
|
||||
print(" -> VULN-005: robots.txt screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-005: robots.txt Returns SPA HTML Instead of Text File",
|
||||
command="curl -s -I https://deploy.ghaymah.systems/robots.txt",
|
||||
output=vuln005_output,
|
||||
highlights=["Content-Type: text/html", "200 OK"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln005_robots_txt.png"), full_page=True)
|
||||
print(" OK vuln005_robots_txt.png")
|
||||
|
||||
# --- VULN-006: GraphQL Mutations Exposed ---
|
||||
print(" -> VULN-006: GraphQL mutations screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-006: GraphQL Mutation Names Exposed in Client JavaScript Bundle",
|
||||
command='curl -s .../assets/index-DxCUZ-xh.js | grep -oP "(create|delete|update|list|rotate)\\w+"',
|
||||
output=vuln006_output,
|
||||
highlights=["deleteApp", "deleteManagedApp", "DeletePostgresDb", "deleteS3Bucket",
|
||||
"deleteVolume", "createPAT", "createPullSecret", "DeletePullSecret",
|
||||
"deleteStaticApp", "deleteManagedVolume"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln006_graphql_mutations.png"), full_page=True)
|
||||
print(" OK vuln006_graphql_mutations.png")
|
||||
|
||||
# --- VULN-007: Auth Cookie ---
|
||||
print(" -> VULN-007: Auth cookie screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-007: Auth Cookie Missing 'Secure' and 'SameSite' Attributes",
|
||||
command="curl -s -I https://auth.ghaymah.systems/",
|
||||
output=vuln007_output,
|
||||
highlights=["Set-Cookie:", "connect.sid", "HttpOnly", "Path=/"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln007_auth_cookie.png"), full_page=True)
|
||||
print(" OK vuln007_auth_cookie.png")
|
||||
|
||||
# --- VULN-008: uvicorn server header ---
|
||||
print(" -> VULN-008: uvicorn header screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-008: GenAI Endpoint Exposes 'uvicorn' Server Header",
|
||||
command="curl -s -I https://genai.ghaymah.systems/health",
|
||||
output=vuln008_output,
|
||||
highlights=["Server: uvicorn", "401 Unauthorized"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln008_uvicorn_header.png"), full_page=True)
|
||||
print(" OK vuln008_uvicorn_header.png")
|
||||
|
||||
# --- VULN-009: No Rate Limiting ---
|
||||
print(" -> VULN-009: No rate limiting screenshot...")
|
||||
rate_output = "\n".join(vuln009_outputs) + "\n\nAll 5 requests succeeded instantly -- no 429 Too Many Requests returned."
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-009: No Rate Limiting - 5 Rapid Requests All Succeed Without Throttling",
|
||||
command="for i in {1..5}; do curl -s -I https://auth.ghaymah.systems/signin/email-password; done",
|
||||
output=rate_output,
|
||||
highlights=["All 5 requests succeeded"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln009_no_rate_limit.png"), full_page=True)
|
||||
print(" OK vuln009_no_rate_limit.png")
|
||||
|
||||
# --- VULN-013: Infrastructure Topology ---
|
||||
print(" -> VULN-013: Infrastructure topology screenshot...")
|
||||
page.set_content(render_terminal_html(
|
||||
title="VULN-013: Complete Infrastructure Topology Exposed in Client JavaScript",
|
||||
command='curl -s .../assets/index-DxCUZ-xh.js | grep -oP "https://.*ghaymah\\.(systems|cloud).*" | sort -u',
|
||||
output=vuln013_output,
|
||||
highlights=["auth.ghaymah.systems", "genai.ghaymah.systems", "graphql.ghaymah.systems",
|
||||
"logs.ghaymah.systems", "integrations.ghaymah.systems", "gateway-"]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln013_infrastructure_topology.png"), full_page=True)
|
||||
print(" OK vuln013_infrastructure_topology.png")
|
||||
|
||||
# --- VULN-019: Dotfile Access ---
|
||||
print(" -> VULN-019: Dotfile access screenshot...")
|
||||
page.set_content(render_multi_terminal_html(
|
||||
title="VULN-019: SPA Catch-All Masks Sensitive File Access (.env, .git/config)",
|
||||
sections=[
|
||||
{
|
||||
"label": "Test 1: Accessing .env file",
|
||||
"cmd": "curl -s -I https://deploy.ghaymah.systems/.env",
|
||||
"output": vuln019_env,
|
||||
"highlights": ["200 OK", "Content-Type: text/html"]
|
||||
},
|
||||
{
|
||||
"label": "Test 2: Accessing .git/config",
|
||||
"cmd": "curl -s -I https://deploy.ghaymah.systems/.git/config",
|
||||
"output": vuln019_git,
|
||||
"highlights": ["200 OK", "Content-Type: text/html"]
|
||||
}
|
||||
]
|
||||
))
|
||||
page.wait_for_timeout(500)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln019_dotfile_access.png"), full_page=True)
|
||||
print(" OK vuln019_dotfile_access.png")
|
||||
|
||||
# --- VULN-001 OpenAPI Spec (additional) ---
|
||||
print(" -> VULN-001 (extra): OpenAPI spec screenshot...")
|
||||
page.goto("https://genai.ghaymah.systems/api/v1/openapi.json", timeout=15000)
|
||||
page.wait_for_timeout(3000)
|
||||
page.screenshot(path=os.path.join(DEST, "vuln001_openapi_json.png"), full_page=False)
|
||||
print(" OK vuln001_openapi_json.png")
|
||||
|
||||
browser.close()
|
||||
|
||||
# ========== Summary ==========
|
||||
files = [f for f in os.listdir(DEST) if f.endswith('.png')]
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"DONE -- Generated {len(files)} evidence screenshots:")
|
||||
for f in sorted(files):
|
||||
size = os.path.getsize(os.path.join(DEST, f))
|
||||
print(f" {f} ({size/1024:.1f} KB)")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||