diff --git a/src/dashboard_template.py b/src/dashboard_template.py deleted file mode 100644 index 4bcde8b..0000000 --- a/src/dashboard_template.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 - -""" -Dashboard template for viewing honeypot statistics. -Customize this template to change the dashboard appearance. -""" - - -def generate_dashboard(stats: dict) -> str: - """Generate dashboard HTML with access statistics""" - - top_ips_rows = '\n'.join([ - f'{i+1}{ip}{count}' - for i, (ip, count) in enumerate(stats['top_ips']) - ]) or 'No data' - - # Generate paths rows - top_paths_rows = '\n'.join([ - f'{i+1}{path}{count}' - for i, (path, count) in enumerate(stats['top_paths']) - ]) or 'No data' - - # Generate User-Agent rows - top_ua_rows = '\n'.join([ - f'{i+1}{ua[:80]}{count}' - for i, (ua, count) in enumerate(stats['top_user_agents']) - ]) or 'No data' - - # Generate suspicious accesses rows - suspicious_rows = '\n'.join([ - f'{log["ip"]}{log["path"]}{log["user_agent"][:60]}{log["timestamp"].split("T")[1][:8]}' - for log in stats['recent_suspicious'][-10:] - ]) or 'No suspicious activity detected' - - return f""" - - - - Krawl Dashboard - - - -
-

🕷️ Krawl Dashboard

- -
-
-
{stats['total_accesses']}
-
Total Accesses
-
-
-
{stats['unique_ips']}
-
Unique IPs
-
-
-
{stats['unique_paths']}
-
Unique Paths
-
-
-
{stats['suspicious_accesses']}
-
Suspicious Accesses
-
-
- -
-

⚠️ Recent Suspicious Activity

- - - - - - - - - - - {suspicious_rows} - -
IP AddressPathUser-AgentTime
-
- -
-

Top IP Addresses

- - - - - - - - - - {top_ips_rows} - -
#IP AddressAccess Count
-
- -
-

Top Paths

- - - - - - - - - - {top_paths_rows} - -
#PathAccess Count
-
- -
-

Top User-Agents

- - - - - - - - - - {top_ua_rows} - -
#User-AgentCount
-
-
- - -""" diff --git a/src/handler.py b/src/handler.py index 2768c6b..81f48fa 100644 --- a/src/handler.py +++ b/src/handler.py @@ -197,96 +197,114 @@ class Handler(BaseHTTPRequestHandler): """Handle POST requests (mainly login attempts)""" client_ip = self._get_client_ip() user_agent = self._get_user_agent() - - self.tracker.record_access(client_ip, self.path, user_agent) - + post_data = "" + print(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}") content_length = int(self.headers.get('Content-Length', 0)) if content_length > 0: - post_data = self.rfile.read(content_length).decode('utf-8') + post_data = self.rfile.read(content_length).decode('utf-8', errors="replace") + print(f"[POST DATA] {post_data[:200]}") + + # 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) time.sleep(1) - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_templates.login_error().encode()) + try: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.login_error().encode()) + except BrokenPipeError: + # Client disconnected before receiving response, ignore silently + pass + except Exception as e: + # Log other exceptions but don't crash + print(f"[ERROR] Failed to send response to {client_ip}: {str(e)}") def serve_special_path(self, path: str) -> bool: """Serve special paths like robots.txt, API endpoints, etc.""" - if path == '/robots.txt': - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.end_headers() - self.wfile.write(html_templates.robots_txt().encode()) - return True - - if path in ['/credentials.txt', '/passwords.txt', '/admin_notes.txt']: - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.end_headers() - if 'credentials' in path: - self.wfile.write(credentials_txt().encode()) - else: - self.wfile.write(passwords_txt().encode()) - return True - - if path in ['/users.json', '/api_keys.json', '/config.json']: - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - if 'users' in path: - self.wfile.write(users_json().encode()) - elif 'api_keys' in path: - self.wfile.write(api_keys_json().encode()) - else: - self.wfile.write(api_response('/api/config').encode()) - return True - - if path in ['/admin', '/admin/', '/admin/login', '/login', '/wp-login.php']: - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_templates.login_form().encode()) - return True - - if path == '/wp-admin' or path == '/wp-admin/': - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_templates.login_form().encode()) - return True - - if path in ['/wp-content/', '/wp-includes/'] or 'wordpress' in path.lower(): - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_templates.wordpress().encode()) - return True - - if 'phpmyadmin' in path.lower() or path in ['/pma/', '/phpMyAdmin/']: - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_templates.phpmyadmin().encode()) - return True - - if path.startswith('/api/') or path.startswith('/api') or path in ['/.env']: - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(api_response(path).encode()) - return True - - if path in ['/backup/', '/uploads/', '/private/', '/admin/', '/config/', '/database/']: - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(directory_listing(path).encode()) - return True + try: + if path == '/robots.txt': + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(html_templates.robots_txt().encode()) + return True + + if path in ['/credentials.txt', '/passwords.txt', '/admin_notes.txt']: + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + if 'credentials' in path: + self.wfile.write(credentials_txt().encode()) + else: + self.wfile.write(passwords_txt().encode()) + return True + + if path in ['/users.json', '/api_keys.json', '/config.json']: + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + if 'users' in path: + self.wfile.write(users_json().encode()) + elif 'api_keys' in path: + self.wfile.write(api_keys_json().encode()) + else: + self.wfile.write(api_response('/api/config').encode()) + return True + + if path in ['/admin', '/admin/', '/admin/login', '/login']: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.login_form().encode()) + return True + + # WordPress login page + if path in ['/wp-login.php', '/wp-login', '/wp-admin', '/wp-admin/']: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.wp_login().encode()) + return True + + if path in ['/wp-content/', '/wp-includes/'] or 'wordpress' in path.lower(): + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.wordpress().encode()) + return True + + if 'phpmyadmin' in path.lower() or path in ['/pma/', '/phpMyAdmin/']: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.phpmyadmin().encode()) + return True + + if path.startswith('/api/') or path.startswith('/api') or path in ['/.env']: + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(api_response(path).encode()) + return True + + if path in ['/backup/', '/uploads/', '/private/', '/admin/', '/config/', '/database/']: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(directory_listing(path).encode()) + return True + except BrokenPipeError: + # Client disconnected, ignore silently + pass + except Exception as e: + print(f"[ERROR] Failed to serve special path {path}: {str(e)}") + pass return False @@ -302,6 +320,8 @@ class Handler(BaseHTTPRequestHandler): try: stats = self.tracker.get_stats() self.wfile.write(generate_dashboard(stats).encode()) + except BrokenPipeError: + pass except Exception as e: print(f"Error generating dashboard: {e}") return @@ -333,6 +353,9 @@ class Handler(BaseHTTPRequestHandler): if Handler.counter < 0: Handler.counter = self.config.canary_token_tries + except BrokenPipeError: + # Client disconnected, ignore silently + pass except Exception as e: print(f"Error generating page: {e}") diff --git a/src/templates/__init__.py b/src/templates/__init__.py new file mode 100644 index 0000000..3eb9f72 --- /dev/null +++ b/src/templates/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +""" +Templates package for the deception server. +""" + +from .template_loader import load_template, clear_cache, TemplateNotFoundError +from . import html_templates + +__all__ = [ + 'load_template', + 'clear_cache', + 'TemplateNotFoundError', + 'html_templates', +] diff --git a/src/templates/dashboard_template.py b/src/templates/dashboard_template.py index d4c6421..3f5524d 100644 --- a/src/templates/dashboard_template.py +++ b/src/templates/dashboard_template.py @@ -39,6 +39,12 @@ def generate_dashboard(stats: dict) -> str: for ip, paths in stats.get('honeypot_triggered_ips', []) ]) or 'No honeypot triggers yet' + # Generate attack types rows + attack_type_rows = '\n'.join([ + f'{log["ip"]}{log["path"]}{", ".join(log["attack_types"])}{log["user_agent"][:60]}{log["timestamp"].split("T")[1][:8]}' + for log in stats.get('attack_types', [])[-10:] + ]) or 'No attacks detected' + return f""" @@ -188,6 +194,24 @@ def generate_dashboard(stats: dict) -> str: +
+

😈 Detected Attack Types

+ + + + + + + + + + + + {attack_type_rows} + +
IP AddressPathAttack TypesUser-AgentTime
+
+

Top IP Addresses

diff --git a/src/templates/html/directory_listing.html b/src/templates/html/directory_listing.html new file mode 100644 index 0000000..75b7f03 --- /dev/null +++ b/src/templates/html/directory_listing.html @@ -0,0 +1,20 @@ + + +Index of {path} + + + +

Index of {path}

+
+ + +{rows} +
NameLast ModifiedSize
Parent Directory--
\ No newline at end of file diff --git a/src/templates/html/directory_row.html b/src/templates/html/directory_row.html new file mode 100644 index 0000000..faf407f --- /dev/null +++ b/src/templates/html/directory_row.html @@ -0,0 +1 @@ +{name}{date}{size} diff --git a/src/templates/html/login_error.html b/src/templates/html/login_error.html new file mode 100644 index 0000000..3727c20 --- /dev/null +++ b/src/templates/html/login_error.html @@ -0,0 +1,108 @@ + + + + + + + Error + + + +
+

⚠ Error

+ +
+ Login Failed. Please try again. +
+ +

If the problem persists, please contact support.

+ + + + ← Back to Home +
+ + \ No newline at end of file diff --git a/src/templates/html/login_form.html b/src/templates/html/login_form.html new file mode 100644 index 0000000..247355e --- /dev/null +++ b/src/templates/html/login_form.html @@ -0,0 +1,156 @@ + + + + + + + Admin Login + + + +
+

Admin Panel

+

Please log in to continue

+ +
+ + + + + + +
+ + +
+ + +
+ + +
+ + \ No newline at end of file diff --git a/src/templates/html/phpmyadmin.html b/src/templates/html/phpmyadmin.html new file mode 100644 index 0000000..b19e9d7 --- /dev/null +++ b/src/templates/html/phpmyadmin.html @@ -0,0 +1,167 @@ + + + + + + + phpMyAdmin + + + +
+

phpMyAdmin

+
+
+

MySQL Server Login

+
+ + + + + + + + +
+ + +
+ + + +
+ + +
+ + diff --git a/src/templates/html/robots.txt b/src/templates/html/robots.txt new file mode 100644 index 0000000..2bae8ca --- /dev/null +++ b/src/templates/html/robots.txt @@ -0,0 +1,21 @@ +User-agent: * +Disallow: /admin/ +Disallow: /api/ +Disallow: /backup/ +Disallow: /config/ +Disallow: /database/ +Disallow: /private/ +Disallow: /uploads/ +Disallow: /wp-admin/ +Disallow: /login/ +Disallow: /admin/login +Disallow: /phpMyAdmin/ +Disallow: /admin/login.php +Disallow: /api/v1/users +Disallow: /api/v2/secrets +Disallow: /.env +Disallow: /credentials.txt +Disallow: /passwords.txt +Disallow: /.git/ +Disallow: /backup.sql +Disallow: /db_backup.sql diff --git a/src/templates/html/wordpress.html b/src/templates/html/wordpress.html new file mode 100644 index 0000000..034d973 --- /dev/null +++ b/src/templates/html/wordpress.html @@ -0,0 +1,73 @@ + + + + + + My Blog – Just another WordPress site + + + + + + + + +
+ + +
+
+
+

Hello world!

+ +
+
+

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+
+ +
+
+

About This Site

+ +
+
+

This is a sample page. You can use it to write about your site, yourself, or anything else you'd like.

+
+
+
+ + +
+ + + \ No newline at end of file diff --git a/src/templates/html/wp_login.html b/src/templates/html/wp_login.html new file mode 100644 index 0000000..c20eff4 --- /dev/null +++ b/src/templates/html/wp_login.html @@ -0,0 +1,238 @@ + + + + + + + Log In ‹ WordPress — WordPress + + + +
+

WordPress Login

+
+

+ + +

+
+ +
+ + +
+
+

+ +

+

+ + + +

+
+ +

+ ← Go to WordPress +

+ +
+ + + diff --git a/src/templates/html_templates.py b/src/templates/html_templates.py index e17df75..c6ad09a 100644 --- a/src/templates/html_templates.py +++ b/src/templates/html_templates.py @@ -2,228 +2,51 @@ """ HTML templates for the deception server. -Edit these templates to customize the appearance of fake pages. +Templates are loaded from the html/ subdirectory. """ +from .template_loader import load_template + def login_form() -> str: """Generate fake login page""" - return """ - - - - Admin Login - - - -
-

Admin Login

-
- - - -
-
- -""" + return load_template("login_form") def login_error() -> str: """Generate fake login error page""" - return """ - - - - Login Failed - - - -
-

Admin Login

-
ERROR: Invalid username or password.
-
- - - -
-

Forgot your password?

-
- -""" + return load_template("login_error") def wordpress() -> str: """Generate fake WordPress page""" - return """ - - - - - My Blog – Just another WordPress site - - - - - - - - -
- - -
-
-
-

Hello world!

- -
-
-

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
- -
-
-

About This Site

- -
-
-

This is a sample page. You can use it to write about your site, yourself, or anything else you'd like.

-
-
-
- - -
- - -""" + return load_template("wordpress") def phpmyadmin() -> str: """Generate fake phpMyAdmin page""" - return """ - - - phpMyAdmin - - - -

phpMyAdmin

-
-

MySQL Server Login

-
- - - -
-
- -""" + return load_template("phpmyadmin") + + +def wp_login() -> str: + """Generate fake WordPress login page""" + return load_template("wp_login") def robots_txt() -> str: """Generate juicy robots.txt""" - return """User-agent: * -Disallow: /admin/ -Disallow: /api/ -Disallow: /backup/ -Disallow: /config/ -Disallow: /database/ -Disallow: /private/ -Disallow: /uploads/ -Disallow: /wp-admin/ -Disallow: /phpMyAdmin/ -Disallow: /admin/login.php -Disallow: /api/v1/users -Disallow: /api/v2/secrets -Disallow: /.env -Disallow: /credentials.txt -Disallow: /passwords.txt -Disallow: /.git/ -Disallow: /backup.sql -Disallow: /db_backup.sql -""" + return load_template("robots.txt") def directory_listing(path: str, dirs: list, files: list) -> str: """Generate fake directory listing""" - html = f""" - -Index of {path} - - - -

Index of {path}

- - - -""" - + row_template = load_template("directory_row") + + rows = "" for d in dirs: - html += f'\n' - + rows += row_template.format(href=d, name=d, date="2024-12-01 10:30", size="-") + for f, size in files: - html += f'\n' - - html += '
NameLast ModifiedSize
Parent Directory--
{d}2024-12-01 10:30-
{f}2024-12-01 14:22{size}
' - return html + rows += row_template.format(href=f, name=f, date="2024-12-01 14:22", size=size) + + return load_template("directory_listing", path=path, rows=rows) diff --git a/src/templates/template_loader.py b/src/templates/template_loader.py new file mode 100644 index 0000000..fd1febc --- /dev/null +++ b/src/templates/template_loader.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +""" +Template loader for HTML templates. +Loads templates from the html/ subdirectory and supports string formatting for dynamic content. +""" + +from pathlib import Path +from typing import Dict + + +class TemplateNotFoundError(Exception): + """Raised when a template file cannot be found.""" + pass + + +# Module-level cache for loaded templates +_template_cache: Dict[str, str] = {} + +# Base directory for template files +_TEMPLATE_DIR = Path(__file__).parent / "html" + + +def load_template(name: str, **kwargs) -> str: + """ + Load a template by name and optionally substitute placeholders. + + Args: + name: Template name (without extension for HTML, with extension for others like .txt) + **kwargs: Key-value pairs for placeholder substitution using str.format() + + Returns: + Rendered template string + + Raises: + TemplateNotFoundError: If template file doesn't exist + + Example: + >>> load_template("login_form") # Loads html/login_form.html + >>> load_template("robots.txt") # Loads html/robots.txt + >>> load_template("directory_listing", path="/var/www", rows="...") + """ + # debug + # print(f"Loading Template: {name}") + + # Check cache first + if name not in _template_cache: + # Determine file path based on whether name has an extension + if '.' in name: + file_path = _TEMPLATE_DIR / name + else: + file_path = _TEMPLATE_DIR / f"{name}.html" + + if not file_path.exists(): + raise TemplateNotFoundError(f"Template '{name}' not found at {file_path}") + + _template_cache[name] = file_path.read_text(encoding='utf-8') + + template = _template_cache[name] + + # Apply substitutions if kwargs provided + if kwargs: + template = template.format(**kwargs) + return template + + +def clear_cache() -> None: + """Clear the template cache. Useful for testing or development.""" + _template_cache.clear() diff --git a/src/tracker.py b/src/tracker.py index 8a73a4c..6e733f4 100644 --- a/src/tracker.py +++ b/src/tracker.py @@ -3,6 +3,7 @@ from typing import Dict, List, Tuple from collections import defaultdict from datetime import datetime +import re class AccessTracker: @@ -17,17 +18,35 @@ class AccessTracker: 'scanner', 'nikto', 'sqlmap', 'nmap', 'masscan', 'nessus', 'acunetix', 'burp', 'zap', 'w3af', 'metasploit', 'nuclei', 'gobuster', 'dirbuster' ] + + # common attack types such as xss, shell injection, probes + self.attack_types = { + 'path_traversal': r'\.\.', + 'sql_injection': r"('|--|;|\bOR\b|\bUNION\b|\bSELECT\b|\bDROP\b)", + 'xss_attempt': r'( 0: + attack_findings.extend(self.detect_attack_type(body)) + + is_suspicious = self.is_suspicious_user_agent(user_agent) or self.is_honeypot_path(path) or len(attack_findings) > 0 + # Track if this IP accessed a honeypot path if self.is_honeypot_path(path): @@ -39,9 +58,20 @@ class AccessTracker: 'user_agent': user_agent, 'suspicious': is_suspicious, 'honeypot_triggered': self.is_honeypot_path(path), + 'attack_types':attack_findings, 'timestamp': datetime.now().isoformat() }) + def detect_attack_type(self, data:str) -> list[str]: + """ + Returns a list of all attack types found in path data + """ + findings = [] + for name, pattern in self.attack_types.items(): + if re.search(pattern, data, re.IGNORECASE): + findings.append(name) + return findings + def is_honeypot_path(self, path: str) -> bool: """Check if path is one of the honeypot traps from robots.txt""" honeypot_paths = [ @@ -91,6 +121,11 @@ class AccessTracker: suspicious = [log for log in self.access_log if log.get('suspicious', False)] return suspicious[-limit:] + def get_attack_type_accesses(self, limit: int = 20) -> List[Dict]: + """Get recent accesses with detected attack types""" + attacks = [log for log in self.access_log if log.get('attack_types')] + return attacks[-limit:] + def get_honeypot_triggered_ips(self) -> List[Tuple[str, List[str]]]: """Get IPs that accessed honeypot paths""" return [(ip, paths) for ip, paths in self.honeypot_triggered.items()] @@ -110,5 +145,6 @@ class AccessTracker: 'top_paths': self.get_top_paths(10), 'top_user_agents': self.get_top_user_agents(10), 'recent_suspicious': self.get_suspicious_accesses(20), - 'honeypot_triggered_ips': self.get_honeypot_triggered_ips() + 'honeypot_triggered_ips': self.get_honeypot_triggered_ips(), + 'attack_types': self.get_attack_type_accesses(20) } diff --git a/tests/sim_attacks.sh b/tests/sim_attacks.sh new file mode 100755 index 0000000..d4a72b2 --- /dev/null +++ b/tests/sim_attacks.sh @@ -0,0 +1,20 @@ +#!/bin/bash +TARGET="http://localhost:5000" + +echo "=== Testing Path Traversal ===" +curl -s "$TARGET/../../etc/passwd" + +echo -e "\n=== Testing SQL Injection ===" +curl -s -X POST "$TARGET/login" -d "user=' OR 1=1--" + +echo -e "\n=== Testing XSS ===" +curl -s -X POST "$TARGET/comment" -d "msg=" + +echo -e "\n=== Testing Common Probes ===" +curl -s "$TARGET/.env" +curl -s "$TARGET/wp-admin/" + +echo -e "\n=== Testing Shell Injection ===" +curl -s -X POST "$TARGET/ping" -d "host=127.0.0.1; cat /etc/passwd" + +echo -e "\n=== Done ===" \ No newline at end of file