Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 8,665 Bytes
d543fc1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | """
admin_panel_scanner.py — Admin Panel & Exposed Dashboard Scanner
================================================================
PHASE 1: Baseline filtering added — only reports panels that are NOT the
site's generic SPA catch-all response.
PHASE 2: Content-signature validation for product-specific panels.
Probes a targeted wordlist of admin, monitoring, and developer dashboards
that are often left exposed. More targeted than the generic directory scanner.
"""
import urllib.request, urllib.error
from scanners.base_scanner import BaseScanner
from scanners.core.signatures import matches_signature
ADMIN_PATHS = [
# Generic admin panels
"/admin", "/admin/", "/admin/login", "/admin/dashboard",
"/_admin", "/administrator", "/admincp", "/admin1", "/admin2",
"/backend", "/backend/login", "/manage", "/management",
"/control", "/controlpanel", "/cp", "/cpanel",
# PHP tooling
"/phpmyadmin", "/pma", "/phpMyAdmin", "/phpmyadmin/", "/mysql",
"/adminer", "/adminer.php", "/db", "/database",
# Python/Django
"/django-admin", "/django/admin", "/_admin/",
# Java / Spring / JEE
"/manager", "/manager/html", "/host-manager", "/console",
"/actuator", "/actuator/health", "/actuator/env",
"/actuator/beans", "/actuator/mappings", "/actuator/info",
"/jolokia", "/jolokia/list", "/druid", "/druid/login.html",
# CI/CD & DevOps
"/jenkins", "/jenkins/", "/jenkins/login",
"/gitlab", "/gitlab/users/sign_in",
"/sonarqube", "/sonar",
# Monitoring / Observability
"/grafana", "/grafana/login",
"/kibana", "/kibana/app/kibana",
"/prometheus", "/metrics", "/_prometheus/metrics",
"/jaeger", "/zipkin",
# Messaging & Queues
"/rabbitmq", "/rabbitmq-management",
"/activemq", "/activemq/admin",
"/kafka", "/kafka-ui",
# Container / Cloud
"/portainer", "/rancher", "/kubernetes",
"/_cluster/health", "/_cat/nodes", # Elasticsearch
# CMS / CRM
"/wp-admin", "/wp-login.php",
"/typo3", "/typo3/backend",
"/joomla/administrator", "/index.php?option=com_admin",
# Misc
"/setup", "/setup.php", "/install", "/install.php",
"/config", "/config.php", "/.env", "/server-status",
"/server-info", "/status", "/info.php", "/phpinfo.php",
]
ADMIN_KEYWORDS = [
"login", "dashboard", "admin", "username", "password",
"sign in", "control panel", "management", "welcome back",
"<form", "Log In", "Sign In",
]
# Map paths → signature keys for product-specific validation
_PATH_SIG_MAP: dict[str, str] = {
"/jenkins": "jenkins",
"/jenkins/": "jenkins",
"/jenkins/login": "jenkins",
"/grafana": "grafana",
"/grafana/login": "grafana",
"/kibana": "kibana",
"/kibana/app/kibana": "kibana",
"/prometheus": "prometheus",
"/phpmyadmin": "phpmyadmin",
"/pma": "phpmyadmin",
"/phpMyAdmin": "phpmyadmin",
"/phpmyadmin/": "phpmyadmin",
"/adminer": "adminer",
"/adminer.php": "adminer",
"/phpinfo.php": "phpinfo",
"/info.php": "phpinfo",
"/wp-login.php": "wp_login",
"/wp-admin": "wp_login",
"/_cluster/health": "elasticsearch",
"/actuator": "spring_actuator",
"/actuator/health": "spring_actuator",
"/actuator/env": "spring_actuator_env",
"/portainer": "portainer",
}
class AdminPanelScanner(BaseScanner):
SCANNER_NAME = "Admin Panel & Dashboard Scanner"
_SCANNER_KEY = "admin_panel"
def __init__(self, scan_id, target, domain, **kwargs):
super().__init__(scan_id, target, domain, **kwargs)
def run(self) -> list:
self.log("INFO", f"[AdminPanel] Probing {len(ADMIN_PATHS)} admin/dashboard paths on {self.target}...")
base = self.target.rstrip("/")
found = []
for path in ADMIN_PATHS:
url = base + path
status, body = self._probe(url)
if status == 200 and body is not None:
# PHASE 1: Suppress if response is the site's SPA/404 catch-all
if self._is_baseline(status, body):
self.log("INFO", f"[AdminPanel] SUPPRESSED (baseline match, {len(body)}b): {url}")
continue
# Minimum content threshold — avoid near-empty redirect bodies
if len(body) < 200:
self.log("INFO", f"[AdminPanel] SKIPPED (body too small, {len(body)}b): {url}")
continue
# PHASE 2: Product-specific signature check
sig_key = _PATH_SIG_MAP.get(path)
if sig_key:
if not matches_signature(sig_key, body, log_fn=lambda m: self.log("INFO", m), url=url):
continue
else:
# Generic admin check: must contain admin keywords AND a form element
body_lower = body.lower()
has_keyword = any(kw.lower() in body_lower for kw in ADMIN_KEYWORDS)
has_form = "<form" in body_lower
if not (has_keyword and has_form):
self.log("INFO", f"[AdminPanel] SKIPPED (no keyword+form match): {url}")
continue
found.append((path, status, len(body)))
self.log("WARNING", f"[AdminPanel] FOUND: {url} ({status}, {len(body)}b)")
elif status in (401, 403):
# Access denied — still confirms presence of a panel
found.append((path, status, 0))
self.log("INFO", f"[AdminPanel] Protected (HTTP {status}): {url}")
if found:
public = [(p, s, b) for p, s, b in found if s == 200]
protected = [(p, s, b) for p, s, b in found if s in (401, 403)]
if public:
self.add_vuln(
title=f"Unauthenticated Admin Panels Found ({len(public)})",
severity="Critical",
category="Exposed Admin Panel",
cvss_score=9.8,
confidence="Confirmed",
description=(
f"The following admin/dashboard endpoints are publicly accessible (HTTP 200) "
f"without authentication:\n\n" +
"\n".join(f"- `{base + p}` ({b} bytes)" for p, s, b in public[:10])
),
remediation=(
"1. Immediately restrict admin panels to internal IPs / VPN only.\n"
"2. Require MFA on all administrative interfaces.\n"
"3. Remove development tools (phpMyAdmin, Adminer) from production.\n"
"4. Disable Spring Actuator endpoints: management.endpoints.enabled-by-default=false"
),
evidence=f"{len(public)} unauthenticated admin path(s) found, verified against site baseline.",
)
if protected:
self.add_vuln(
title=f"Admin Panels Discovered But Access-Controlled ({len(protected)})",
severity="Medium",
category="Exposed Admin Panel",
cvss_score=5.3,
confidence="Likely",
description=(
f"Admin panels exist but return 401/403:\n\n" +
"\n".join(f"- `{base + p}` (HTTP {s})" for p, s, b in protected[:10]) +
"\n\nWhile protected, their existence aids attacker reconnaissance "
"and they remain vulnerable to credential stuffing."
),
remediation=(
"Restrict admin paths to internal networks. Return 404 (not 403) "
"for admin paths when accessed externally to reduce information disclosure."
),
)
else:
self.log("SUCCESS", "[AdminPanel] No admin panels or dashboards found.")
return self.vulns
def _probe(self, url):
try:
req = urllib.request.Request(url, headers=self._make_headers())
with urllib.request.urlopen(req, timeout=4, context=self.get_ssl_context()) as r:
return r.status, r.read().decode("utf-8", errors="ignore")
except urllib.error.HTTPError as e:
return e.code, ""
except Exception as e:
self.log("ERROR", f"[AdminPanel] _probe error: {e}")
return 0, ""
|