From 5f8bb73546a9447fdf855134b0c7c42244810d42 Mon Sep 17 00:00:00 2001 From: Patrick Di Fazio Date: Sat, 3 Jan 2026 17:14:58 +0100 Subject: [PATCH] added random SQL errors, random server errors, XSS baits --- src/data/krawl.db | Bin 0 -> 69632 bytes src/handler.py | 158 ++++++++++++++++++++++++- src/server_errors.py | 65 ++++++++++ src/sql_errors.py | 112 ++++++++++++++++++ src/templates/html/generic_search.html | 66 +++++++++++ src/templates/html/input_form.html | 74 ++++++++++++ src/templates/html/robots.txt | 10 ++ src/templates/html_templates.py | 10 ++ src/tracker.py | 22 ++-- src/wordlists.py | 12 ++ src/xss_detector.py | 73 ++++++++++++ 11 files changed, 589 insertions(+), 13 deletions(-) create mode 100644 src/data/krawl.db create mode 100644 src/server_errors.py create mode 100644 src/sql_errors.py create mode 100644 src/templates/html/generic_search.html create mode 100644 src/templates/html/input_form.html create mode 100644 src/xss_detector.py diff --git a/src/data/krawl.db b/src/data/krawl.db new file mode 100644 index 0000000000000000000000000000000000000000..759ffb958d54f426a0a424446aa7baec7af9d275 GIT binary patch literal 69632 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 dSRv6B%QRsj`B str: + """Extract query string from the request path""" + parsed = urlparse(self.path) + return parsed.query + + def _handle_sql_endpoint(self, path: str) -> bool: + """ + Handle SQL injection honeypot endpoints. + Returns True if the path was handled, False otherwise. + """ + # SQL-vulnerable endpoints + sql_endpoints = ['/api/search', '/api/sql', '/api/database'] + + base_path = urlparse(path).path + if base_path not in sql_endpoints: + return False + + try: + # Get query parameters + query_string = self._parse_query_string() + + # Log SQL injection attempt + client_ip = self._get_client_ip() + user_agent = self._get_user_agent() + + # Always check for SQL injection patterns + error_msg, content_type, status_code = generate_sql_error_response(query_string or "") + + if error_msg: + # SQL injection detected - log and return error + self.access_logger.warning(f"[SQL INJECTION DETECTED] {client_ip} - {base_path} - Query: {query_string[:100] if query_string else 'empty'}") + self.send_response(status_code) + self.send_header('Content-type', content_type) + self.end_headers() + self.wfile.write(error_msg.encode()) + else: + # No injection detected - return fake data + self.access_logger.info(f"[SQL ENDPOINT] {client_ip} - {base_path} - Query: {query_string[:100] if query_string else 'empty'}") + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + response_data = get_sql_response_with_data(base_path, query_string or "") + self.wfile.write(response_data.encode()) + + return True + + except BrokenPipeError: + # Client disconnected + return True + except Exception as e: + self.app_logger.error(f"Error handling SQL endpoint {path}: {str(e)}") + # Still send a response even on error + try: + self.send_response(500) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(b'{"error": "Internal server error"}') + except: + pass + return True def generate_page(self, seed: str) -> str: """Generate a webpage containing random links or canary token""" @@ -207,6 +272,68 @@ class Handler(BaseHTTPRequestHandler): user_agent = self._get_user_agent() post_data = "" + from urllib.parse import urlparse + base_path = urlparse(self.path).path + + if base_path in ['/api/search', '/api/sql', '/api/database']: + content_length = int(self.headers.get('Content-Length', 0)) + if content_length > 0: + post_data = self.rfile.read(content_length).decode('utf-8', errors="replace") + + self.access_logger.info(f"[SQL ENDPOINT POST] {client_ip} - {base_path} - Data: {post_data[:100] if post_data else 'empty'}") + + error_msg, content_type, status_code = generate_sql_error_response(post_data) + + try: + if error_msg: + self.access_logger.warning(f"[SQL INJECTION DETECTED POST] {client_ip} - {base_path}") + self.send_response(status_code) + self.send_header('Content-type', content_type) + self.end_headers() + self.wfile.write(error_msg.encode()) + else: + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + response_data = get_sql_response_with_data(base_path, post_data) + self.wfile.write(response_data.encode()) + except BrokenPipeError: + pass + except Exception as e: + self.app_logger.error(f"Error in SQL POST handler: {str(e)}") + return + + if base_path == '/api/contact': + content_length = int(self.headers.get('Content-Length', 0)) + if content_length > 0: + post_data = self.rfile.read(content_length).decode('utf-8', errors="replace") + + parsed_data = {} + for pair in post_data.split('&'): + if '=' in pair: + key, value = pair.split('=', 1) + from urllib.parse import unquote_plus + parsed_data[unquote_plus(key)] = unquote_plus(value) + + xss_detected = any(detect_xss_pattern(v) for v in parsed_data.values()) + + if xss_detected: + self.access_logger.warning(f"[XSS ATTEMPT DETECTED] {client_ip} - {base_path} - Data: {post_data[:200]}") + else: + self.access_logger.info(f"[XSS ENDPOINT POST] {client_ip} - {base_path}") + + try: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + response_html = generate_xss_response(parsed_data) + self.wfile.write(response_html.encode()) + except BrokenPipeError: + pass + except Exception as e: + self.app_logger.error(f"Error in XSS POST handler: {str(e)}") + return + self.access_logger.warning(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}") content_length = int(self.headers.get('Content-Length', 0)) @@ -215,20 +342,16 @@ 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) time.sleep(1) @@ -248,6 +371,10 @@ class Handler(BaseHTTPRequestHandler): def serve_special_path(self, path: str) -> bool: """Serve special paths like robots.txt, API endpoints, etc.""" + # Check SQL injection honeypot endpoints first + if self._handle_sql_endpoint(path): + return True + try: if path == '/robots.txt': self.send_response(200) @@ -285,7 +412,28 @@ class Handler(BaseHTTPRequestHandler): self.wfile.write(html_templates.login_form().encode()) return True - # WordPress login page + if path in ['/users', '/user', '/database', '/db', '/search']: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.product_search().encode()) + return True + + if path in ['/info', '/input', '/contact', '/feedback', '/comment']: + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_templates.input_form().encode()) + return True + + if path == '/server': + error_html, content_type = generate_server_error() + self.send_response(500) + self.send_header('Content-type', content_type) + self.end_headers() + self.wfile.write(error_html.encode()) + return True + if path in ['/wp-login.php', '/wp-login', '/wp-admin', '/wp-admin/']: self.send_response(200) self.send_header('Content-type', 'text/html') diff --git a/src/server_errors.py b/src/server_errors.py new file mode 100644 index 0000000..7591c64 --- /dev/null +++ b/src/server_errors.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +import random +from wordlists import get_wordlists + + +def generate_server_error() -> tuple[str, str]: + wl = get_wordlists() + server_errors = wl.server_errors + + if not server_errors: + return ("500 Internal Server Error", "text/html") + + server_type = random.choice(list(server_errors.keys())) + server_config = server_errors[server_type] + + error_codes = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 500: "Internal Server Error", + 502: "Bad Gateway", + 503: "Service Unavailable" + } + + code = random.choice(list(error_codes.keys())) + message = error_codes[code] + + template = server_config.get('template', '') + version = random.choice(server_config.get('versions', ['1.0'])) + + html = template.replace('{code}', str(code)) + html = html.replace('{message}', message) + html = html.replace('{version}', version) + + if server_type == 'apache': + os = random.choice(server_config.get('os', ['Ubuntu'])) + html = html.replace('{os}', os) + html = html.replace('{host}', 'localhost') + + return (html, "text/html") + + +def get_server_header(server_type: str = None) -> str: + wl = get_wordlists() + server_errors = wl.server_errors + + if not server_errors: + return "nginx/1.18.0" + + if not server_type: + server_type = random.choice(list(server_errors.keys())) + + server_config = server_errors.get(server_type, {}) + version = random.choice(server_config.get('versions', ['1.0'])) + + server_headers = { + 'nginx': f"nginx/{version}", + 'apache': f"Apache/{version}", + 'iis': f"Microsoft-IIS/{version}", + 'tomcat': f"Apache-Coyote/1.1" + } + + return server_headers.get(server_type, "nginx/1.18.0") diff --git a/src/sql_errors.py b/src/sql_errors.py new file mode 100644 index 0000000..dc84886 --- /dev/null +++ b/src/sql_errors.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import random +import re +from typing import Optional, Tuple +from wordlists import get_wordlists + + +def detect_sql_injection_pattern(query_string: str) -> Optional[str]: + if not query_string: + return None + + query_lower = query_string.lower() + + patterns = { + 'quote': [r"'", r'"', r'`'], + 'comment': [r'--', r'#', r'/\*', r'\*/'], + 'union': [r'\bunion\b', r'\bunion\s+select\b'], + 'boolean': [r'\bor\b.*=.*', r'\band\b.*=.*', r"'.*or.*'.*=.*'"], + 'time_based': [r'\bsleep\b', r'\bwaitfor\b', r'\bdelay\b', r'\bbenchmark\b'], + 'stacked': [r';.*select', r';.*drop', r';.*insert', r';.*update', r';.*delete'], + 'command': [r'\bexec\b', r'\bexecute\b', r'\bxp_cmdshell\b'], + 'info_schema': [r'information_schema', r'table_schema', r'table_name'], + } + + for injection_type, pattern_list in patterns.items(): + for pattern in pattern_list: + if re.search(pattern, query_lower): + return injection_type + + return None + + +def get_random_sql_error(db_type: str = None, injection_type: str = None) -> Tuple[str, str]: + wl = get_wordlists() + sql_errors = wl.sql_errors + + if not sql_errors: + return ("Database error occurred", "text/plain") + + if not db_type: + db_type = random.choice(list(sql_errors.keys())) + + db_errors = sql_errors.get(db_type, {}) + + if injection_type and injection_type in db_errors: + errors = db_errors[injection_type] + elif 'generic' in db_errors: + errors = db_errors['generic'] + else: + all_errors = [] + for error_list in db_errors.values(): + if isinstance(error_list, list): + all_errors.extend(error_list) + errors = all_errors if all_errors else ["Database error occurred"] + + error_message = random.choice(errors) if errors else "Database error occurred" + + if '{table}' in error_message: + tables = ['users', 'products', 'orders', 'customers', 'accounts', 'sessions'] + error_message = error_message.replace('{table}', random.choice(tables)) + + if '{column}' in error_message: + columns = ['id', 'name', 'email', 'password', 'username', 'created_at'] + error_message = error_message.replace('{column}', random.choice(columns)) + + return (error_message, "text/plain") + + +def generate_sql_error_response(query_string: str, db_type: str = None) -> Tuple[str, str, int]: + injection_type = detect_sql_injection_pattern(query_string) + + if not injection_type: + return (None, None, None) + + error_message, content_type = get_random_sql_error(db_type, injection_type) + + status_code = 500 + + if random.random() < 0.3: + status_code = 200 + + return (error_message, content_type, status_code) + + +def get_sql_response_with_data(path: str, params: str) -> str: + import json + from generators import random_username, random_email, random_password + + injection_type = detect_sql_injection_pattern(params) + + if injection_type in ['union', 'boolean', 'stacked']: + data = { + "success": True, + "results": [ + { + "id": i, + "username": random_username(), + "email": random_email(), + "password_hash": random_password(), + "role": random.choice(["admin", "user", "moderator"]) + } + for i in range(1, random.randint(2, 5)) + ] + } + return json.dumps(data, indent=2) + + return json.dumps({ + "success": True, + "message": "Query executed successfully", + "results": [] + }, indent=2) diff --git a/src/templates/html/generic_search.html b/src/templates/html/generic_search.html new file mode 100644 index 0000000..90171bc --- /dev/null +++ b/src/templates/html/generic_search.html @@ -0,0 +1,66 @@ + + + + Search + + + +

Search

+
+ + +
+
+ + + + diff --git a/src/templates/html/input_form.html b/src/templates/html/input_form.html new file mode 100644 index 0000000..c03b1a8 --- /dev/null +++ b/src/templates/html/input_form.html @@ -0,0 +1,74 @@ + + + + Contact + + + +

Contact

+
+ + + + +
+
+ + + + diff --git a/src/templates/html/robots.txt b/src/templates/html/robots.txt index 2bae8ca..3618937 100644 --- a/src/templates/html/robots.txt +++ b/src/templates/html/robots.txt @@ -11,8 +11,18 @@ Disallow: /login/ Disallow: /admin/login Disallow: /phpMyAdmin/ Disallow: /admin/login.php +Disallow: /users +Disallow: /search +Disallow: /contact +Disallow: /info +Disallow: /input +Disallow: /feedback +Disallow: /server Disallow: /api/v1/users Disallow: /api/v2/secrets +Disallow: /api/search +Disallow: /api/sql +Disallow: /api/database Disallow: /.env Disallow: /credentials.txt Disallow: /passwords.txt diff --git a/src/templates/html_templates.py b/src/templates/html_templates.py index c6ad09a..a7cefbc 100644 --- a/src/templates/html_templates.py +++ b/src/templates/html_templates.py @@ -50,3 +50,13 @@ def directory_listing(path: str, dirs: list, files: list) -> str: 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) + + +def product_search() -> str: + """Generate product search page with SQL injection honeypot""" + return load_template("generic_search") + + +def input_form() -> str: + """Generate input form page for XSS honeypot""" + return load_template("input_form") diff --git a/src/tracker.py b/src/tracker.py index 717a4c3..8465031 100644 --- a/src/tracker.py +++ b/src/tracker.py @@ -5,6 +5,7 @@ from collections import defaultdict from datetime import datetime import re import urllib.parse +from wordlists import get_wordlists class AccessTracker: @@ -21,14 +22,19 @@ class AccessTracker: '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'( bool: + if not input_string: + return False + + wl = get_wordlists() + xss_pattern = wl.attack_patterns.get('xss_attempt', '') + + if not xss_pattern: + xss_pattern = r'( str: + xss_detected = False + reflected_content = [] + + for key, value in input_data.items(): + if detect_xss_pattern(value): + xss_detected = True + reflected_content.append(f"

{key}: {value}

") + + if xss_detected: + html = f""" + + + + Submission Received + + + +
+

Thank you for your submission!

+

We have received your information:

+ {''.join(reflected_content)} +

We will get back to you shortly.

+
+ + +""" + return html + + return """ + + + + Submission Received + + + +
+

Thank you for your submission!

+

Your message has been received and we will respond soon.

+
+ + +"""