From 61ba574e92cc86444001a767508dc3e79f469247 Mon Sep 17 00:00:00 2001 From: Patrick Di Fazio Date: Sat, 27 Dec 2025 19:17:27 +0100 Subject: [PATCH] Added POST log and dashboard for used credentials --- src/handler.py | 15 ++++++++ src/logger.py | 28 +++++++++++++++ src/server.py | 4 ++- src/templates/dashboard_template.py | 28 +++++++++++++++ src/tracker.py | 56 ++++++++++++++++++++++++++++- 5 files changed, 129 insertions(+), 2 deletions(-) diff --git a/src/handler.py b/src/handler.py index 9d8abe2..ac7ca22 100644 --- a/src/handler.py +++ b/src/handler.py @@ -3,6 +3,7 @@ import logging import random import time +from datetime import datetime from http.server import BaseHTTPRequestHandler from typing import Optional, List @@ -25,6 +26,7 @@ class Handler(BaseHTTPRequestHandler): counter: int = 0 app_logger: logging.Logger = None access_logger: logging.Logger = None + credential_logger: logging.Logger = None def _get_client_ip(self) -> str: """Extract client IP address from request, checking proxy headers first""" @@ -213,6 +215,19 @@ class Handler(BaseHTTPRequestHandler): self.access_logger.warning(f"[POST DATA] {post_data[:200]}") + # Parse and log credentials + username, password = self.tracker.parse_credentials(post_data) + if username or password: + # Log to dedicated credentials.log file + timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + credential_line = f"{timestamp}|{client_ip}|{username or 'N/A'}|{password or 'N/A'}|{self.path}" + self.credential_logger.info(credential_line) + + # Also record in tracker for dashboard + self.tracker.record_credential_attempt(client_ip, self.path, username or 'N/A', password or 'N/A') + + self.access_logger.warning(f"[CREDENTIALS CAPTURED] {client_ip} - Username: {username or 'N/A'} - Path: {self.path}") + # send the post data (body) to the record_access function so the post data can be used to detect suspicious things. self.tracker.record_access(client_ip, self.path, user_agent, post_data) diff --git a/src/logger.py b/src/logger.py index 68b8278..9f09236 100644 --- a/src/logger.py +++ b/src/logger.py @@ -77,6 +77,22 @@ class LoggerManager: access_stream_handler.setFormatter(log_format) self._access_logger.addHandler(access_stream_handler) + # Setup credential logger (special format, no stream handler) + self._credential_logger = logging.getLogger("krawl.credentials") + self._credential_logger.setLevel(logging.INFO) + self._credential_logger.handlers.clear() + + # Credential logger uses a simple format: timestamp|ip|username|password|path + credential_format = logging.Formatter("%(message)s") + + credential_file_handler = RotatingFileHandler( + os.path.join(log_dir, "credentials.log"), + maxBytes=max_bytes, + backupCount=backup_count + ) + credential_file_handler.setFormatter(credential_format) + self._credential_logger.addHandler(credential_file_handler) + self._initialized = True @property @@ -93,6 +109,13 @@ class LoggerManager: self.initialize() return self._access_logger + @property + def credentials(self) -> logging.Logger: + """Get the credentials logger.""" + if not self._initialized: + self.initialize() + return self._credential_logger + # Module-level singleton instance _logger_manager = LoggerManager() @@ -108,6 +131,11 @@ def get_access_logger() -> logging.Logger: return _logger_manager.access +def get_credential_logger() -> logging.Logger: + """Get the credential logger instance.""" + return _logger_manager.credentials + + def initialize_logging(log_dir: str = "logs") -> None: """Initialize the logging system.""" _logger_manager.initialize(log_dir) diff --git a/src/server.py b/src/server.py index 861e9f2..fd8f7d2 100644 --- a/src/server.py +++ b/src/server.py @@ -11,7 +11,7 @@ from http.server import HTTPServer from config import Config from tracker import AccessTracker from handler import Handler -from logger import initialize_logging, get_app_logger, get_access_logger +from logger import initialize_logging, get_app_logger, get_access_logger, get_credential_logger def print_usage(): @@ -45,6 +45,7 @@ def main(): initialize_logging() app_logger = get_app_logger() access_logger = get_access_logger() + credential_logger = get_credential_logger() config = Config.from_env() @@ -55,6 +56,7 @@ def main(): Handler.counter = config.canary_token_tries Handler.app_logger = app_logger Handler.access_logger = access_logger + Handler.credential_logger = credential_logger if len(sys.argv) == 2: try: diff --git a/src/templates/dashboard_template.py b/src/templates/dashboard_template.py index 3f5524d..a267278 100644 --- a/src/templates/dashboard_template.py +++ b/src/templates/dashboard_template.py @@ -45,6 +45,12 @@ def generate_dashboard(stats: dict) -> str: for log in stats.get('attack_types', [])[-10:] ]) or 'No attacks detected' + # Generate credential attempts rows + credential_rows = '\n'.join([ + f'{log["ip"]}{log["username"]}{log["password"]}{log["path"]}{log["timestamp"].split("T")[1][:8]}' + for log in stats.get('credential_attempts', [])[-20:] + ]) or 'No credentials captured yet' + return f""" @@ -159,6 +165,10 @@ def generate_dashboard(stats: dict) -> str:
{stats.get('honeypot_ips', 0)}
Honeypot Caught
+
+
{len(stats.get('credential_attempts', []))}
+
Credentials Captured
+
@@ -194,6 +204,24 @@ def generate_dashboard(stats: dict) -> str:
+
+

🔑 Captured Credentials

+ + + + + + + + + + + + {credential_rows} + +
IP AddressUsernamePasswordPathTime
+
+

😈 Detected Attack Types

diff --git a/src/tracker.py b/src/tracker.py index 6e733f4..717a4c3 100644 --- a/src/tracker.py +++ b/src/tracker.py @@ -4,6 +4,7 @@ from typing import Dict, List, Tuple from collections import defaultdict from datetime import datetime import re +import urllib.parse class AccessTracker: @@ -13,6 +14,7 @@ class AccessTracker: self.path_counts: Dict[str, int] = defaultdict(int) self.user_agent_counts: Dict[str, int] = defaultdict(int) self.access_log: List[Dict] = [] + self.credential_attempts: List[Dict] = [] self.suspicious_patterns = [ 'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests', 'scanner', 'nikto', 'sqlmap', 'nmap', 'masscan', 'nessus', 'acunetix', @@ -31,6 +33,57 @@ class AccessTracker: # Track IPs that accessed honeypot paths from robots.txt self.honeypot_triggered: Dict[str, List[str]] = defaultdict(list) + def parse_credentials(self, post_data: str) -> Tuple[str, str]: + """ + Parse username and password from POST data. + Returns tuple (username, password) or (None, None) if not found. + """ + if not post_data: + return None, None + + username = None + password = None + + try: + # Parse URL-encoded form data + parsed = urllib.parse.parse_qs(post_data) + + # Common username field names + username_fields = ['username', 'user', 'login', 'email', 'log', 'userid', 'account'] + for field in username_fields: + if field in parsed and parsed[field]: + username = parsed[field][0] + break + + # Common password field names + password_fields = ['password', 'pass', 'passwd', 'pwd', 'passphrase'] + for field in password_fields: + if field in parsed and parsed[field]: + password = parsed[field][0] + break + + except Exception: + # If parsing fails, try simple regex patterns + username_match = re.search(r'(?:username|user|login|email|log)=([^&\s]+)', post_data, re.IGNORECASE) + password_match = re.search(r'(?:password|pass|passwd|pwd)=([^&\s]+)', post_data, re.IGNORECASE) + + if username_match: + username = urllib.parse.unquote_plus(username_match.group(1)) + if password_match: + password = urllib.parse.unquote_plus(password_match.group(1)) + + return username, password + + def record_credential_attempt(self, ip: str, path: str, username: str, password: str): + """Record a credential login attempt""" + self.credential_attempts.append({ + 'ip': ip, + 'path': path, + 'username': username, + 'password': password, + 'timestamp': datetime.now().isoformat() + }) + def record_access(self, ip: str, path: str, user_agent: str = '', body: str = ''): """Record an access attempt""" self.ip_counts[ip] += 1 @@ -146,5 +199,6 @@ class AccessTracker: 'top_user_agents': self.get_top_user_agents(10), 'recent_suspicious': self.get_suspicious_accesses(20), 'honeypot_triggered_ips': self.get_honeypot_triggered_ips(), - 'attack_types': self.get_attack_type_accesses(20) + 'attack_types': self.get_attack_type_accesses(20), + 'credential_attempts': self.credential_attempts[-50:] # Last 50 attempts }