| |
| |
| |
| |
| |
|
|
| import os |
| import time |
| import threading |
| import json |
| import base64 |
| import secrets |
| import string |
| import requests |
| from datetime import datetime |
| from urllib.parse import urlparse |
| import gradio as gr |
|
|
| |
| try: |
| from huggingface_hub import HfApi, create_repo |
| HF_HUB_AVAILABLE = True |
| except ImportError: |
| HF_HUB_AVAILABLE = False |
|
|
| |
| |
| |
| class AppState: |
| def __init__(self): |
| self.commands = [] |
| self.reports = [] |
| self.lock = threading.Lock() |
| |
| self.user_tokens = { |
| "hf_token": None, |
| "github_token": None |
| } |
| |
| self.server_url = None |
| self._detect_server_url() |
| |
| def _detect_server_url(self): |
| """Auto-detect the C2 server URL from environment or runtime context""" |
| |
| if os.getenv("C2_SERVER_URL"): |
| self.server_url = os.getenv("C2_SERVER_URL").rstrip("/") |
| return |
| |
| |
| space_host = os.getenv("SPACE_HOST") |
| if space_host: |
| |
| self.server_url = f"https://{space_host.lower()}".rstrip("/") |
| return |
| |
| |
| render_url = os.getenv("RENDER_EXTERNAL_URL") |
| if render_url: |
| self.server_url = render_url.rstrip("/") |
| return |
| |
| |
| port = os.getenv("PORT", "7860") |
| self.server_url = f"http://localhost:{port}".rstrip("/") |
|
|
| STATE = AppState() |
|
|
| |
| |
| |
| VALID_TYPES = { |
| "fetch_http_flood", "fetch_cache_bypass", "fetch_custom_headers", |
| "socket_tcp_probe", "socket_udp_probe", |
| } |
| ALLOWED_TARGETS = {"127.0.0.1", "localhost", "0.0.0.0"} |
|
|
| def validate_target(ip: str) -> bool: |
| return ip.startswith("127.") or ip in ALLOWED_TARGETS |
|
|
| def generate_random_name(prefix: str = "c2-client", length: int = 8) -> str: |
| """Generate a random, URL-safe name for deployments""" |
| chars = string.ascii_lowercase + string.digits |
| random_suffix = ''.join(secrets.choice(chars) for _ in range(length)) |
| return f"{prefix}-{random_suffix}" |
|
|
| |
| |
| |
| def save_user_tokens(hf_token: str, github_token: str) -> str: |
| """Store tokens in server memory only - never in templates""" |
| with STATE.lock: |
| |
| if hf_token and hf_token.strip(): |
| if hf_token.strip().startswith("hf_"): |
| STATE.user_tokens["hf_token"] = hf_token.strip() |
| else: |
| return "Invalid HF token format (must start with 'hf_')" |
| |
| |
| if github_token and github_token.strip(): |
| if github_token.strip().startswith("ghp_") or github_token.strip().startswith("gho_"): |
| STATE.user_tokens["github_token"] = github_token.strip() |
| else: |
| return "Invalid GitHub token format (must start with 'ghp_' or 'gho_')" |
| |
| return "Tokens saved securely (server memory only)" |
|
|
| def test_token_connections() -> str: |
| """Test both tokens without exposing them""" |
| results = [] |
| |
| |
| hf_token = STATE.user_tokens.get("hf_token") |
| if hf_token: |
| try: |
| if HF_HUB_AVAILABLE: |
| api = HfApi(token=hf_token) |
| user = api.whoami() |
| results.append(f"HF: {user.get('name', user.get('id', 'connected'))}") |
| else: |
| headers = {"Authorization": f"Bearer {hf_token}"} |
| res = requests.get("https://huggingface.co/api/whoami-v2", headers=headers, timeout=10) |
| if res.status_code == 200: |
| data = res.json() |
| results.append(f"HF: {data.get('name', 'connected')}") |
| else: |
| results.append(f"HF: HTTP {res.status_code}") |
| except Exception as e: |
| results.append(f"HF: {str(e)[:60]}") |
| else: |
| results.append("HF: Not set") |
| |
| |
| gh_token = STATE.user_tokens.get("github_token") |
| if gh_token: |
| try: |
| headers = {"Authorization": f"token {gh_token}", "Accept": "application/vnd.github.v3+json"} |
| res = requests.get("https://api.github.com/user", headers=headers, timeout=10) |
| res.raise_for_status() |
| login = res.json().get("login", "unknown") |
| results.append(f"GitHub: {login}") |
| except Exception as e: |
| results.append(f"GitHub: {str(e)[:60]}") |
| else: |
| results.append("GitHub: Not set") |
| |
| return " | ".join(results) |
|
|
| def clear_tokens() -> str: |
| """Wipe tokens from memory""" |
| with STATE.lock: |
| STATE.user_tokens["hf_token"] = None |
| STATE.user_tokens["github_token"] = None |
| return "Tokens cleared from memory" |
|
|
| |
| |
| |
| def deploy_to_hf_space(space_sdk: str = "docker") -> str: |
| """Create/update HF Space using stored token via API - NO template injection""" |
| hf_token = STATE.user_tokens.get("hf_token") |
| if not hf_token: |
| return "HF token not set. Please enter in Tokens tab first." |
| |
| try: |
| api = HfApi(token=hf_token) if HF_HUB_AVAILABLE else None |
| |
| |
| if api: |
| user = api.whoami() |
| username = user.get('name', user.get('id')) |
| else: |
| headers = {"Authorization": f"Bearer {hf_token}"} |
| res = requests.get("https://huggingface.co/api/whoami-v2", headers=headers, timeout=10) |
| res.raise_for_status() |
| username = res.json().get('name', 'unknown-user') |
| |
| |
| space_name = generate_random_name("c2-client") |
| repo_id = f"{username}/{space_name}" |
| |
| |
| if api: |
| create_repo( |
| repo_id=repo_id, |
| repo_type="space", |
| space_sdk=space_sdk, |
| token=hf_token, |
| exist_ok=True, |
| private=False |
| ) |
| else: |
| |
| headers = {"Authorization": f"Bearer {hf_token}"} |
| requests.post( |
| "https://huggingface.co/api/repos/create", |
| headers=headers, |
| json={"name": space_name, "type": "space", "space_sdk": space_sdk}, |
| timeout=10 |
| ) |
| |
| |
| template_files = _get_clean_template_files(space_sdk) |
| for filename, content in template_files.items(): |
| if api: |
| import tempfile |
| with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=filename) as tmp: |
| tmp.write(content) |
| tmp_path = tmp.name |
| try: |
| api.upload_file( |
| path_or_fileobj=tmp_path, |
| path_in_repo=filename, |
| repo_id=repo_id, |
| repo_type="space", |
| token=hf_token |
| ) |
| finally: |
| os.unlink(tmp_path) |
| else: |
| encoded = base64.b64encode(content.encode()).decode() |
| headers = {"Authorization": f"Bearer {hf_token}"} |
| |
| requests.put( |
| f"https://huggingface.co/api/models/{repo_id}/resolve/main/{filename}", |
| headers=headers, |
| json={"content": encoded, "commit_message": f"Deploy {filename}"}, |
| timeout=30 |
| ) |
| |
| return f"Deployed to: https://huggingface.co/spaces/{repo_id}\nClients will ping: {STATE.server_url}" |
| |
| except Exception as e: |
| return f"Deployment failed: {str(e)}" |
|
|
| def deploy_to_github_pages(branch: str = "gh-pages") -> str: |
| """Push to GitHub Pages using stored token via API - NO template injection""" |
| github_token = STATE.user_tokens.get("github_token") |
| if not github_token: |
| return "GitHub token not set. Please enter in Tokens tab first." |
| |
| try: |
| headers = { |
| "Authorization": f"token {github_token}", |
| "Accept": "application/vnd.github.v3+json" |
| } |
| |
| |
| user_res = requests.get("https://api.github.com/user", headers=headers, timeout=10) |
| user_res.raise_for_status() |
| username = user_res.json()["login"] |
| |
| |
| repo_name = generate_random_name("c2-client") |
| |
| |
| requests.post( |
| "https://api.github.com/user/repos", |
| headers=headers, |
| json={"name": repo_name, "private": False, "auto_init": True}, |
| timeout=10 |
| ) |
| |
| |
| try: |
| requests.get( |
| f"https://api.github.com/repos/{username}/{repo_name}/branches/{branch}", |
| headers=headers, timeout=5 |
| ).raise_for_status() |
| except: |
| |
| main_ref = requests.get( |
| f"https://api.github.com/repos/{username}/{repo_name}/git/refs/heads/main", |
| headers=headers, timeout=5 |
| ).json() |
| requests.post( |
| f"https://api.github.com/repos/{username}/{repo_name}/git/refs", |
| headers=headers, |
| json={"ref": f"refs/heads/{branch}", "sha": main_ref["object"]["sha"]}, |
| timeout=10 |
| ) |
| |
| |
| template_files = _get_clean_template_files("docker") |
| for filename, content in template_files.items(): |
| encoded = base64.b64encode(content.encode()).decode() |
| |
| |
| try: |
| existing = requests.get( |
| f"https://api.github.com/repos/{username}/{repo_name}/contents/{filename}?ref={branch}", |
| headers=headers, timeout=5 |
| ) |
| sha = existing.json().get("sha") if existing.status_code == 200 else None |
| except: |
| sha = None |
| |
| payload = { |
| "message": f"Deploy {filename} via C2", |
| "content": encoded, |
| "branch": branch |
| } |
| if sha: |
| payload["sha"] = sha |
| |
| requests.put( |
| f"https://api.github.com/repos/{username}/{repo_name}/contents/{filename}", |
| headers=headers, |
| json=payload, |
| timeout=10 |
| ) |
| |
| return f"Deployed to: https://{username}.github.io/{repo_name}/\nClients will ping: {STATE.server_url}" |
| |
| except Exception as e: |
| return f"GitHub deploy failed: {str(e)}" |
|
|
| def _get_clean_template_files(space_sdk: str = "docker") -> dict: |
| """Return template files with SERVER_URL injected, but NO tokens""" |
| files = {} |
| |
| |
| client_template = f'''# hf_space_client/client.py - ETHICAL DEMO CLIENT |
| import requests, socket, time, json, os, random, string |
| from datetime import datetime |
| |
| # Configuration |
| SERVER = os.getenv("SERVER_URL", "{STATE.server_url}") |
| DEFAULT_ALLOWED_TARGETS = {{"127.0.0.1", "localhost"}} |
| ALLOWED_TARGETS_HF = set(DEFAULT_ALLOWED_TARGETS) |
| |
| def validate_target_ip(ip): |
| if ip.startswith("127."): return True |
| return ip in ALLOWED_TARGETS_HF |
| |
| def log_client_message(msg): |
| print(f"[{{datetime.now().isoformat()}}] HF-Client: {{msg}}") |
| |
| def exec_fetch_http_flood(ip, port, sim_headers): |
| url = f"http://{{ip}}:{{port}}/" |
| try: |
| t0 = time.time() |
| res = requests.get(url, headers=sim_headers, timeout=3) |
| return {{"status": f"status_{{res.status_code}}", "latency_ms": round((time.time()-t0)*1000, 2), "bytes": len(res.content), "protocol": "http"}} |
| except Exception as e: |
| return {{"status": "error", "detail": str(e), "protocol": "http"}} |
| |
| def exec_fetch_cache_bypass(ip, port, sim_headers): |
| key = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12)) |
| url = f"http://{{ip}}:{{port}}/?_cache_bust={{key}}&_={{int(time.time()*1000)}}" |
| try: |
| t0 = time.time() |
| res = requests.get(url, headers=sim_headers, timeout=3) |
| return {{"status": "success", "latency_ms": round((time.time()-t0)*1000, 2), "bytes": len(res.content), "cache_key": key, "protocol": "http"}} |
| except Exception as e: |
| return {{"status": "error", "detail": str(e), "protocol": "http"}} |
| |
| def exec_fetch_custom_headers(ip, port, sim_headers): |
| url = f"http://{{ip}}:{{port}}/" |
| custom_headers = {{ **sim_headers, "X-Custom-Test": "sim_payload", "Accept": "application/json" }} |
| try: |
| t0 = time.time() |
| res = requests.get(url, headers=custom_headers, timeout=3) |
| return {{"status": f"status_{{res.status_code}}", "latency_ms": round((time.time()-t0)*1000, 2), "bytes": len(res.content), "protocol": "http"}} |
| except Exception as e: |
| return {{"status": "error", "detail": str(e), "protocol": "http"}} |
| |
| def exec_socket_tcp_probe(ip, port): |
| try: |
| t0 = time.time() |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| s.settimeout(2) |
| s.connect((ip, port)) |
| s.sendall(b"SIM_PKT_TCP_01_HEARTBEAT") |
| return {{"status": "tcp_connected", "latency_ms": round((time.time()-t0)*1000, 2), "bytes_sent": 24, "protocol": "tcp"}} |
| except Exception as e: |
| return {{"status": "tcp_failed", "detail": str(e), "protocol": "tcp"}} |
| |
| def exec_socket_udp_probe(ip, port): |
| try: |
| t0 = time.time() |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: |
| s.settimeout(2) |
| sent = s.sendto(b"SIM_PKT_UDP_01_HEARTBEAT", (ip, port)) |
| return {{"status": "udp_sent", "latency_ms": round((time.time()-t0)*1000, 2), "bytes_sent": sent, "protocol": "udp"}} |
| except Exception as e: |
| return {{"status": "udp_failed", "detail": str(e), "protocol": "udp"}} |
| |
| EXECUTORS = {{ |
| "fetch_http_flood": exec_fetch_http_flood, |
| "fetch_cache_bypass": exec_fetch_cache_bypass, |
| "fetch_custom_headers": exec_fetch_custom_headers, |
| "socket_tcp_probe": exec_socket_tcp_probe, |
| "socket_udp_probe": exec_socket_udp_probe |
| }} |
| |
| def run(): |
| log_client_message("="*50) |
| log_client_message("HUGGING FACE SPACE CLIENT (ETHICAL DEMO)") |
| log_client_message("="*50) |
| log_client_message(f"Polling Control Server: {{SERVER}}") |
| log_client_message("Educational simulation only - no malicious activity") |
| log_client_message("="*50) |
| |
| while True: |
| try: |
| cmd_response = requests.get(f"{{SERVER}}/api/poll_command", timeout=5).json() |
| if cmd_response.get("status") == "idle": |
| time.sleep(2); continue |
| |
| cmd = cmd_response |
| cmd_type, ip, port = cmd["type"], cmd["ip"], int(cmd["port"]) |
| sim_headers = cmd.get("simulation_headers", {{"X-Simulation": "true", "User-Agent": "EduSimClient/1.0"}}) |
| |
| log_client_message(f"Command (ID={{cmd['id']}}): {{cmd_type}} -> {{ip}}:{{port}}") |
| |
| if not validate_target_ip(ip): |
| report = {{"client": "hf_space", "command_id": cmd["id"], "status": "blocked_by_client_allowlist", "target": f"{{ip}}:{{port}}", "timestamp": time.time()}} |
| elif cmd_type in EXECUTORS: |
| if cmd_type.startswith("fetch_"): |
| res = EXECUTORS[cmd_type](ip, port, sim_headers) |
| else: |
| res = EXECUTORS[cmd_type](ip, port) |
| report = {{"client": "hf_space", "command_id": cmd["id"], "type": cmd_type, "target": f"{{ip}}:{{port}}", "requests_sent": 1, "result": res, "timestamp": time.time()}} |
| else: |
| report = {{"client": "hf_space", "command_id": cmd["id"], "status": "unsupported_command_type", "type": cmd_type, "target": f"{{ip}}:{{port}}", "timestamp": time.time()}} |
| |
| requests.post(f"{{SERVER}}/api/report", json=report, timeout=5) |
| except Exception as e: |
| log_client_message(f"Error: {{e}}") |
| time.sleep(2) |
| |
| if __name__ == "__main__": |
| run() |
| ''' |
| files["client.py"] = client_template |
| |
| |
| if space_sdk == "docker": |
| dockerfile_template = f'''FROM python:3.11-slim |
| |
| WORKDIR /app |
| |
| COPY requirements.txt . |
| RUN pip install --no-cache-dir -r requirements.txt |
| |
| COPY client.py . |
| |
| # Environment variable for server URL (can be overridden at runtime) |
| ENV SERVER_URL={STATE.server_url} |
| |
| CMD ["python", "client.py"] |
| ''' |
| files["Dockerfile"] = dockerfile_template |
| |
| |
| gh_pages_template = f'''<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>GitHub Pages Fetch Sim Client (Ethical Demo)</title> |
| <style> |
| body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 20px; background: #f8f9fa; color: #333; }} |
| pre {{ background: #fff; padding: 15px; border: 1px solid #ddd; border-radius: 8px; max-height: 60vh; overflow-y: auto; white-space: pre-wrap; font-size: 0.9em; }} |
| h2 {{ color: #007bff; border-bottom: 2px solid #eee; padding-bottom: 10px; }} |
| .warning {{ background: #ffe0e0; border: 1px solid #dc3545; color: #dc3545; padding: 15px; border-radius: 8px; margin-bottom: 25px; text-align: center; }} |
| .config-info {{ background: #e2f0fe; border: 1px solid #007bff; color: #007bff; padding: 10px; border-radius: 5px; margin-top: 15px; font-size: 0.9em; }} |
| </style> |
| </head> |
| <body> |
| <div class="warning"> |
| <h3>ETHICAL DEMONSTRATION ONLY</h3> |
| <p>This client demonstrates controlled network testing. NO malicious activity is performed. All requests target pre-approved IPs (loopback only).</p> |
| </div> |
| |
| <h2>GitHub Pages Fetch Simulation Client</h2> |
| <div class="config-info"> |
| <p><strong>Control Server:</strong> <code id="server-url-display">Loading...</code></p> |
| <p><strong>Allowed Targets:</strong> <code>127.x.x.x (loopback only)</code></p> |
| </div> |
| <pre id="log">Initializing...</pre> |
| |
| <script> |
| const SERVER = "{STATE.server_url}"; |
| const SIM_ALLOWED_TARGETS = ["localhost"]; |
| |
| document.getElementById("server-url-display").textContent = SERVER; |
| |
| function log(msg) {{ |
| const el = document.getElementById("log"); |
| el.textContent += `\\n${{new Date().toLocaleTimeString()}} - ${{msg}}`; |
| el.scrollTop = el.scrollHeight; |
| }} |
| |
| function validateTargetIp(ip) {{ |
| return ip.startsWith("127.") || SIM_ALLOWED_TARGETS.includes(ip); |
| }} |
| |
| async function executeFetch(type, ip, port, simHeaders) {{ |
| if (!validateTargetIp(ip)) {{ |
| log(`Blocked: IP '${{ip}}' not allowlisted`); |
| return {{ status: "blocked_by_client_allowlist", target_ip: ip }}; |
| }} |
| const base = `http://${{ip}}:${{port}}/`; |
| const opts = {{ method: "GET", headers: simHeaders, mode: "cors", cache: "no-store" }}; |
| const t0 = performance.now(); |
| try {{ |
| let response; |
| if (type === "fetch_http_flood") {{ |
| log(`HTTP GET to ${{base}}`); |
| response = await fetch(base, opts); |
| const txt = await response.text(); |
| return {{ status: `status_${{response.status}}`, latency_ms: performance.now() - t0, bytes: new Blob([txt]).size, protocol: "http" }}; |
| }} else if (type === "fetch_cache_bypass") {{ |
| const k = Math.random().toString(36).substring(2, 10); |
| const url = `${{base}}?_cache_bust=${{k}}&_=${{Date.now()}}`; |
| log(`Cache-bypass GET to ${{url}}`); |
| response = await fetch(url, opts); |
| const txt = await response.text(); |
| return {{ status: "success", latency_ms: performance.now() - t0, bytes: new Blob([txt]).size, cache_key: k, protocol: "http" }}; |
| }} else if (type === "fetch_custom_headers") {{ |
| opts.headers = {{ ...simHeaders, "X-Custom-Test": "sim_payload", "Accept": "application/json" }}; |
| log(`Custom headers GET to ${{base}}`); |
| response = await fetch(base, opts); |
| return {{ status: `status_${{response.status}}`, latency_ms: performance.now() - t0, protocol: "http" }}; |
| }} |
| return {{ status: "unsupported_fetch_type", type: type }}; |
| }} catch (e) {{ |
| log(`Fetch error: ${{e.message}}`); |
| return {{ status: "fetch_error", detail: e.message }}; |
| }} |
| }} |
| |
| async function poll() {{ |
| try {{ |
| const cmdResponse = await fetch(`${{SERVER}}/api/poll_command`).then(r => r.json()); |
| if (cmdResponse.status === "idle") {{ setTimeout(poll, 2000); return; }} |
| const cmd = cmdResponse; |
| const {{ id, type, ip, port, simulation_headers }} = cmd; |
| log(`Command (ID=${{id}}): ${{type}} -> ${{ip}}:${{port}}`); |
| if (!type.startsWith("fetch_")) {{ |
| const report = {{ client: "gh_pages", command_id: id, type, target: `${{ip}}:${{port}}`, requests_sent: 0, result: {{ status: "unsupported_by_browser" }}, timestamp: Date.now()/1000 }}; |
| await fetch(`${{SERVER}}/api/report`, {{ method: "POST", headers: {{"Content-Type":"application/json"}}, body: JSON.stringify(report) }}); |
| setTimeout(poll, 2000); return; |
| }} |
| const result = await executeFetch(type, ip, port, simulation_headers); |
| const report = {{ client: "gh_pages", command_id: id, type, target: `${{ip}}:${{port}}`, requests_sent: 1, result, timestamp: Date.now()/1000 }}; |
| await fetch(`${{SERVER}}/api/report`, {{ method: "POST", headers: {{"Content-Type":"application/json"}}, body: JSON.stringify(report) }}); |
| log(`Report: Status=${{result.status}}`); |
| }} catch (e) {{ log(`Error: ${{e.message}}`); }} |
| setTimeout(poll, 2000); |
| }} |
| log("Client Ready. Polling..."); |
| poll(); |
| </script> |
| </body> |
| </html> |
| ''' |
| files["index.html"] = gh_pages_template |
| |
| |
| files["requirements.txt"] = """requests>=2.31.0 |
| """ |
| |
| |
| files["README.md"] = f"""# C2 Client Simulation (Ethical Demo) |
| |
| This Space runs a client that polls the C2 server at: |
| **`{STATE.server_url}`** |
| |
| ## Security Notice |
| - This is an educational demonstration only |
| - All network requests are limited to loopback addresses (127.x.x.x) |
| - No actual malicious activity is performed |
| - Tokens are managed by the C2 server, not embedded in this Space |
| |
| ## Configuration |
| The client polls `{STATE.server_url}/api/poll_command` for commands and reports results to `{STATE.server_url}/api/report`. |
| |
| ## Runtime |
| - SDK: {space_sdk} |
| - Entry point: client.py |
| """ |
| |
| return files |
|
|
| |
| |
| |
| def get_dashboard_html(): |
| with STATE.lock: |
| recent = STATE.reports[-10:] |
| queued = len(STATE.commands) |
| |
| rows = [] |
| for r in reversed(recent): |
| ts = datetime.fromtimestamp(r.get("ts", time.time())).strftime("%H:%M:%S") |
| client = str(r.get('client', '?'))[:20] |
| cmd_type = str(r.get('type', '?'))[:25] |
| status = str(r.get('status', 'ok'))[:15] |
| color = "#22c55e" if status == "ok" else "#ef4444" |
| rows.append(f"""<tr style="border-bottom:1px solid #30363d"> |
| <td style="padding:8px;font-family:monospace">{client}</td> |
| <td style="padding:8px">{cmd_type}</td> |
| <td style="padding:8px;color:{color}">{status}</td> |
| <td style="padding:8px;color:#8b949e">{ts}</td> |
| </tr>""") |
| |
| rows_html = "".join(rows) if rows else "<tr><td colspan='4' style='padding:20px;text-align:center;color:#8b949e'>No reports yet</td></tr>" |
| |
| return f""" |
| <div style="padding:20px;border-radius:12px;background:#0d1117;color:white;border:1px solid #30363d"> |
| <h2 style="margin:0 0 15px">System Dashboard</h2> |
| <p><b>Queued:</b> {queued} | <b>Reports:</b> {len(recent)}</p> |
| </div> |
| <div style="margin-top:20px;overflow-x:auto"> |
| <table style="width:100%;color:#c9d1d9;border-collapse:collapse"> |
| <thead style="background:#161b22"><tr> |
| <th style="padding:12px;text-align:left">Client</th> |
| <th style="padding:12px;text-align:left">Type</th> |
| <th style="padding:12px;text-align:left">Status</th> |
| <th style="padding:12px;text-align:left">Time</th> |
| </tr></thead> |
| <tbody>{rows_html}</tbody> |
| </table> |
| </div>""" |
|
|
| def push_command(cmd_type: str, ip: str, port: float) -> str: |
| try: |
| port_int = int(port) |
| if not (1 <= port_int <= 65535): return "Port must be 1-65535" |
| if not validate_target(ip.strip()): return f"'{ip}' not allowed" |
| if cmd_type not in VALID_TYPES: return "Invalid command type" |
| with STATE.lock: |
| STATE.commands.append({ |
| "type": cmd_type, "ip": ip.strip(), "port": port_int, |
| "id": int(time.time()*1000), "source": "ui", |
| "simulation_headers": {"X-Simulation": "true", "User-Agent": "EduSimClient/1.0"} |
| }) |
| return f"Queued: {cmd_type} -> {ip}:{port_int}" |
| except Exception as e: return f"Error: {e}" |
|
|
| |
| def api_poll_command(): |
| """Endpoint for clients to poll for commands""" |
| with STATE.lock: |
| if STATE.commands: |
| return STATE.commands.pop(0) |
| return {"status": "idle"} |
|
|
| def api_report(report: dict): |
| """Endpoint for clients to submit reports""" |
| with STATE.lock: |
| report["ts"] = time.time() |
| STATE.reports.append(report) |
| |
| if len(STATE.reports) > 100: |
| STATE.reports = STATE.reports[-100:] |
| return {"status": "received"} |
|
|
| |
| |
| |
| css = """ |
| body { background:#0b0f19; color:#c9d1d9; font-family:system-ui,-apple-system,sans-serif } |
| .gradio-container { max-width:1000px !important; margin:auto !important; padding:20px !important } |
| button.primary { background:linear-gradient(135deg,#238636,#2ea043) !important; border:none !important } |
| input,select,textarea { background:#161b22 !important; border:1px solid #30363d !important; color:#c9d1d9 !important } |
| .markdown-details summary { cursor: pointer; color: #58a6ff; margin: 10px 0; } |
| .markdown-details[open] summary { margin-bottom: 15px; } |
| """ |
|
|
| with gr.Blocks() as demo: |
| gr.HTML(f"<style>{css}</style><title>Control Panel</title>") |
| gr.Markdown("# Control Panel\n*Network Testing Interface - HF Spaces Compatible*") |
| |
| with gr.Tabs(): |
| |
| with gr.Tab("Dashboard"): |
| dashboard = gr.HTML(value=get_dashboard_html()) |
| refresh = gr.Button("Refresh", variant="secondary", size="sm") |
| refresh.click(fn=get_dashboard_html, outputs=dashboard) |
| |
| with gr.Tab("Commands"): |
| gr.Markdown("### Send Command to Connected Clients") |
| with gr.Row(): |
| with gr.Column(scale=2): |
| cmd_type = gr.Dropdown(choices=sorted(VALID_TYPES), value="fetch_http_flood", label="Command Type") |
| target_ip = gr.Textbox(value="127.0.0.1", label="Target IP", placeholder="127.0.0.1") |
| target_port = gr.Number(value=80, label="Port", minimum=1, maximum=65535, step=1, precision=0) |
| with gr.Column(scale=1): |
| gr.Markdown("#### Allowed Targets\n- 127.x.x.x\n- localhost\n- External IPs blocked") |
| send = gr.Button("Send Command", variant="primary") |
| result = gr.Textbox(label="Result", interactive=False, lines=2) |
| send.click(fn=push_command, inputs=[cmd_type, target_ip, target_port], outputs=result) |
| |
| with gr.Tab("Tokens"): |
| gr.Markdown(""" |
| ### Enter Your Access Tokens |
| - Tokens stored only in server memory for this session |
| - Tokens NEVER embedded in deployed templates |
| - Used by C2 server for API authentication only |
| """) |
| |
| with gr.Row(): |
| with gr.Column(): |
| hf_token_input = gr.Textbox( |
| label="Hugging Face Token", |
| type="password", |
| placeholder="hf_xxx...", |
| info="Get at: https://huggingface.co/settings/tokens" |
| ) |
| with gr.Column(): |
| gh_token_input = gr.Textbox( |
| label="GitHub Token", |
| type="password", |
| placeholder="ghp_xxx...", |
| info="Get at: https://github.com/settings/tokens (scopes: repo, gist)" |
| ) |
| |
| with gr.Row(): |
| save_btn = gr.Button("Save Tokens", variant="primary") |
| test_btn = gr.Button("Test Connections", variant="secondary") |
| clear_btn = gr.Button("Clear", variant="stop") |
| |
| token_status = gr.Textbox(label="Status", interactive=False, lines=2) |
| |
| save_btn.click(fn=save_user_tokens, inputs=[hf_token_input, gh_token_input], outputs=[token_status]) |
| test_btn.click(fn=test_token_connections, inputs=[], outputs=[token_status]) |
| clear_btn.click(fn=clear_tokens, inputs=[], outputs=[token_status]) |
| |
| gr.Markdown(""" |
| <details class="markdown-details"> |
| <summary>How Token Security Works</summary> |
| |
| 1. Enter tokens - stored in Python server memory only |
| 2. Deployments use tokens via API calls (huggingface_hub, GitHub API) |
| 3. Template files contain NO token placeholders - safe to push publicly |
| 4. Deployed clients ping YOUR C2 server; server authenticates using stored tokens |
| 5. Tokens lost on Space restart (re-enter if needed) |
| |
| </details> |
| """, elem_classes=["markdown-details"]) |
| |
| with gr.Tab("Deploy"): |
| gr.Markdown("### Deploy Clean Templates Using Your Tokens") |
| |
| |
| gr.Markdown(f"**C2 Server URL**: `{STATE.server_url}`\n\n_Deployed clients will poll this URL for commands_") |
| |
| with gr.Tabs(): |
| with gr.Tab("Hugging Face Spaces"): |
| hf_sdk = gr.Dropdown(choices=["docker"], value="docker", label="SDK Type", interactive=False) |
| hf_deploy_btn = gr.Button("Deploy to HF Spaces", variant="primary") |
| hf_deploy_result = gr.Textbox(label="Result", interactive=False, lines=4) |
| hf_deploy_btn.click( |
| fn=deploy_to_hf_space, |
| inputs=[hf_sdk], |
| outputs=[hf_deploy_result] |
| ) |
| gr.Markdown("*Space name will be auto-generated (e.g., c2-client-a8x9k2m1)*\n*SDK: docker (Dockerfile template included)*") |
| |
| with gr.Tab("GitHub Pages"): |
| gh_branch = gr.Textbox(label="Branch", value="gh-pages") |
| gh_deploy_btn = gr.Button("Deploy to GitHub Pages", variant="primary") |
| gh_deploy_result = gr.Textbox(label="Result", interactive=False, lines=4) |
| gh_deploy_btn.click( |
| fn=deploy_to_github_pages, |
| inputs=[gh_branch], |
| outputs=[gh_deploy_result] |
| ) |
| gr.Markdown("*Repository name will be auto-generated (e.g., c2-client-a8x9k2m1)*") |
| |
| gr.Markdown(""" |
| > Important: |
| > - Deployed clients will poll your C2 server at the URL shown above |
| > - Templates contain NO tokens - authentication happens server-side |
| > - Ensure your C2 server is publicly accessible and implements /api/poll_command and /api/report endpoints |
| > - If commands stay queued, check that the deployed client can reach your C2 server URL |
| """) |
| |
| with gr.Tab("Info"): |
| space_host = os.getenv("SPACE_HOST", "your-space.hf.space") |
| gr.Markdown(f""" |
| ### HF Spaces Info |
| - Current URL: `https://{space_host}` |
| - Gradio Version: 6.0+ |
| - Security: Server-side target validation (loopback only) |
| - Token Storage: Runtime memory only (cleared on restart) |
| |
| ### API Endpoints for Clients |
| - POST /api/poll_command - Clients poll for commands |
| - POST /api/report - Clients submit execution reports |
| |
| ### Template Files Deployed |
| - client.py - Python client for HF Spaces/GitHub Pages |
| - Dockerfile - For HF Spaces with Docker SDK |
| - index.html - JavaScript client for GitHub Pages |
| - requirements.txt - Python dependencies |
| - README.md - Documentation |
| |
| ### Troubleshooting Queued Commands |
| 1. Verify deployed client can reach: {STATE.server_url} |
| 2. Check Space/repo logs for connection errors |
| 3. Ensure C2 server allows CORS if clients are on different origins |
| 4. Commands expire after 60 seconds if not picked up |
| """) |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| |
| demo.app.post("/api/poll_command")(api_poll_command) |
| demo.app.post("/api/report")(api_report) |
| |
| |
| demo.queue(max_size=10) |
| |
| |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=int(os.getenv("PORT", 7860)), |
| share=False, |
| show_error=True, |
| theme=gr.themes.Soft(), |
| ssr_mode=False, |
| debug=os.getenv("DEBUG", "false").lower() == "true" |
| ) |