Merge branch 'dev' into feat/randomized-server-header

This commit is contained in:
Patrick Di Fazio
2025-12-30 00:02:44 +01:00
committed by GitHub
12 changed files with 259 additions and 65 deletions

View File

@@ -186,6 +186,7 @@ To customize the deception server installation several **environment variables**
| `DASHBOARD_SECRET_PATH` | Custom dashboard path | Auto-generated | | `DASHBOARD_SECRET_PATH` | Custom dashboard path | Auto-generated |
| `PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` | | `PROBABILITY_ERROR_CODES` | Error response probability (0-100%) | `0` |
| `SERVER_HEADER` | HTTP Server header for deception, if not set use random server header | | | `SERVER_HEADER` | HTTP Server header for deception, if not set use random server header | |
| `TIMEZONE` | IANA timezone for logs and dashboard (e.g., `America/New_York`, `Europe/Rome`) | System timezone |
## robots.txt ## robots.txt
The actual (juicy) robots.txt configuration is the following The actual (juicy) robots.txt configuration is the following

View File

@@ -1,44 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: krawl-server
namespace: krawl
labels:
app: krawl-server
spec:
replicas: 1
selector:
matchLabels:
app: krawl-server
template:
metadata:
labels:
app: krawl-server
spec:
containers:
- name: krawl
image: ghcr.io/blessedrebus/krawl:latest
imagePullPolicy: Always
ports:
- containerPort: 5000
name: http
protocol: TCP
envFrom:
- configMapRef:
name: krawl-config
volumeMounts:
- name: wordlists
mountPath: /app/wordlists.json
subPath: wordlists.json
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: wordlists
configMap:
name: krawl-wordlists

View File

@@ -25,6 +25,8 @@ services:
# - CANARY_TOKEN_URL=http://canarytokens.com/api/users/YOUR_TOKEN/passwords.txt # - CANARY_TOKEN_URL=http://canarytokens.com/api/users/YOUR_TOKEN/passwords.txt
# Optional: Set custom dashboard path (auto-generated if not set) # Optional: Set custom dashboard path (auto-generated if not set)
# - DASHBOARD_SECRET_PATH=/my-secret-dashboard # - DASHBOARD_SECRET_PATH=/my-secret-dashboard
# Optional: Set timezone for logs and dashboard (e.g., America/New_York, Europe/Rome)
# - TIMEZONE=UTC
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "python3", "-c", "import requests; requests.get('http://localhost:5000')"] test: ["CMD", "python3", "-c", "import requests; requests.get('http://localhost:5000')"]

View File

@@ -20,4 +20,7 @@ data:
{{- end }} {{- end }}
{{- if .Values.config.serverHeader }} {{- if .Values.config.serverHeader }}
SERVER_HEADER: {{ .Values.config.serverHeader | quote }} SERVER_HEADER: {{ .Values.config.serverHeader | quote }}
{{- end }} {{- end }}
{{- if .Values.config.timezone }}
TIMEZONE: {{ .Values.config.timezone | quote }}
{{- end }}

View File

@@ -76,6 +76,7 @@ config:
# serverHeader: "Apache/2.2.22 (Ubuntu)" # serverHeader: "Apache/2.2.22 (Ubuntu)"
# dashboardSecretPath: "/my-secret-dashboard" # dashboardSecretPath: "/my-secret-dashboard"
# canaryTokenUrl: set-your-canary-token-url-here # canaryTokenUrl: set-your-canary-token-url-here
# timezone: "UTC" # IANA timezone (e.g., "America/New_York", "Europe/Rome"). If not set, system timezone is used.
networkPolicy: networkPolicy:
enabled: true enabled: true

View File

@@ -14,4 +14,5 @@ data:
CANARY_TOKEN_TRIES: "10" CANARY_TOKEN_TRIES: "10"
PROBABILITY_ERROR_CODES: "0" PROBABILITY_ERROR_CODES: "0"
SERVER_HEADER: "Apache/2.2.22 (Ubuntu)" SERVER_HEADER: "Apache/2.2.22 (Ubuntu)"
# CANARY_TOKEN_URL: set-your-canary-token-url-here # CANARY_TOKEN_URL: set-your-canary-token-url-here
# TIMEZONE: "UTC" # IANA timezone (e.g., "America/New_York", "Europe/Rome")

View File

@@ -3,6 +3,8 @@
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Tuple from typing import Optional, Tuple
from zoneinfo import ZoneInfo
import time
@dataclass @dataclass
@@ -22,6 +24,40 @@ class Config:
api_server_path: str = "/api/v2/users" api_server_path: str = "/api/v2/users"
probability_error_codes: int = 0 # Percentage (0-100) probability_error_codes: int = 0 # Percentage (0-100)
server_header: Optional[str] = None server_header: Optional[str] = None
timezone: str = None # IANA timezone (e.g., 'America/New_York', 'Europe/Rome')
@staticmethod
# Try to fetch timezone before if not set
def get_system_timezone() -> str:
"""Get the system's default timezone"""
try:
if os.path.islink('/etc/localtime'):
tz_path = os.readlink('/etc/localtime')
if 'zoneinfo/' in tz_path:
return tz_path.split('zoneinfo/')[-1]
local_tz = time.tzname[time.daylight]
if local_tz and local_tz != 'UTC':
return local_tz
except Exception:
pass
# Default fallback to UTC
return 'UTC'
def get_timezone(self) -> ZoneInfo:
"""Get configured timezone as ZoneInfo object"""
if self.timezone:
try:
return ZoneInfo(self.timezone)
except Exception:
pass
system_tz = self.get_system_timezone()
try:
return ZoneInfo(system_tz)
except Exception:
return ZoneInfo('UTC')
@classmethod @classmethod
def from_env(cls) -> 'Config': def from_env(cls) -> 'Config':
@@ -45,6 +81,8 @@ class Config:
api_server_url=os.getenv('API_SERVER_URL'), api_server_url=os.getenv('API_SERVER_URL'),
api_server_port=int(os.getenv('API_SERVER_PORT', 8080)), api_server_port=int(os.getenv('API_SERVER_PORT', 8080)),
api_server_path=os.getenv('API_SERVER_PATH', '/api/v2/users'), api_server_path=os.getenv('API_SERVER_PATH', '/api/v2/users'),
probability_error_codes=int(os.getenv('PROBABILITY_ERROR_CODES', 5)), probability_error_codes=int(os.getenv('PROBABILITY_ERROR_CODES', 0)),
server_header=os.getenv('SERVER_HEADER') server_header=os.getenv('SERVER_HEADER')
timezone=os.getenv('TIMEZONE') # If not set, will use system timezone
) )

View File

@@ -8,6 +8,23 @@ Provides two loggers: app (application) and access (HTTP access logs).
import logging import logging
import os import os
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from typing import Optional
from zoneinfo import ZoneInfo
from datetime import datetime
class TimezoneFormatter(logging.Formatter):
"""Custom formatter that respects configured timezone"""
def __init__(self, fmt=None, datefmt=None, timezone: Optional[ZoneInfo] = None):
super().__init__(fmt, datefmt)
self.timezone = timezone or ZoneInfo('UTC')
def formatTime(self, record, datefmt=None):
"""Override formatTime to use configured timezone"""
dt = datetime.fromtimestamp(record.created, tz=self.timezone)
if datefmt:
return dt.strftime(datefmt)
return dt.isoformat()
class LoggerManager: class LoggerManager:
@@ -20,23 +37,27 @@ class LoggerManager:
cls._instance._initialized = False cls._instance._initialized = False
return cls._instance return cls._instance
def initialize(self, log_dir: str = "logs") -> None: def initialize(self, log_dir: str = "logs", timezone: Optional[ZoneInfo] = None) -> None:
""" """
Initialize the logging system with rotating file handlers. Initialize the logging system with rotating file handlers.
Args: Args:
log_dir: Directory for log files (created if not exists) log_dir: Directory for log files (created if not exists)
timezone: ZoneInfo timezone for log timestamps (defaults to UTC)
""" """
if self._initialized: if self._initialized:
return return
self.timezone = timezone or ZoneInfo('UTC')
# Create log directory if it doesn't exist # Create log directory if it doesn't exist
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
# Common format for all loggers # Common format for all loggers
log_format = logging.Formatter( log_format = TimezoneFormatter(
"[%(asctime)s] %(levelname)s - %(message)s", "[%(asctime)s] %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S" datefmt="%Y-%m-%d %H:%M:%S",
timezone=self.timezone
) )
# Rotation settings: 1MB max, 5 backups # Rotation settings: 1MB max, 5 backups
@@ -83,7 +104,7 @@ class LoggerManager:
self._credential_logger.handlers.clear() self._credential_logger.handlers.clear()
# Credential logger uses a simple format: timestamp|ip|username|password|path # Credential logger uses a simple format: timestamp|ip|username|password|path
credential_format = logging.Formatter("%(message)s") credential_format = TimezoneFormatter("%(message)s", timezone=self.timezone)
credential_file_handler = RotatingFileHandler( credential_file_handler = RotatingFileHandler(
os.path.join(log_dir, "credentials.log"), os.path.join(log_dir, "credentials.log"),
@@ -136,6 +157,6 @@ def get_credential_logger() -> logging.Logger:
return _logger_manager.credentials return _logger_manager.credentials
def initialize_logging(log_dir: str = "logs") -> None: def initialize_logging(log_dir: str = "logs", timezone: Optional[ZoneInfo] = None) -> None:
"""Initialize the logging system.""" """Initialize the logging system."""
_logger_manager.initialize(log_dir) _logger_manager.initialize(log_dir, timezone)

View File

@@ -33,6 +33,8 @@ def print_usage():
print(' PROBABILITY_ERROR_CODES - Probability (0-100) to return HTTP error codes (default: 0)') print(' PROBABILITY_ERROR_CODES - Probability (0-100) to return HTTP error codes (default: 0)')
print(' CHAR_SPACE - Characters for random links') print(' CHAR_SPACE - Characters for random links')
print(' SERVER_HEADER - HTTP Server header for deception (default: Apache/2.2.22 (Ubuntu))') print(' SERVER_HEADER - HTTP Server header for deception (default: Apache/2.2.22 (Ubuntu))')
print(' TIMEZONE - IANA timezone for logs/dashboard (e.g., America/New_York, Europe/Rome)')
print(' If not set, system timezone will be used')
def main(): def main():
@@ -41,15 +43,19 @@ def main():
print_usage() print_usage()
exit(0) exit(0)
# Initialize logging config = Config.from_env()
initialize_logging()
# Get timezone configuration
tz = config.get_timezone()
# Initialize logging with timezone
initialize_logging(timezone=tz)
app_logger = get_app_logger() app_logger = get_app_logger()
access_logger = get_access_logger() access_logger = get_access_logger()
credential_logger = get_credential_logger() credential_logger = get_credential_logger()
config = Config.from_env() # Initialize tracker with timezone
tracker = AccessTracker(timezone=tz)
tracker = AccessTracker()
Handler.config = config Handler.config = config
Handler.tracker = tracker Handler.tracker = tracker
@@ -71,6 +77,7 @@ def main():
try: try:
app_logger.info(f'Starting deception server on port {config.port}...') app_logger.info(f'Starting deception server on port {config.port}...')
app_logger.info(f'Timezone configured: {tz.key}')
app_logger.info(f'Dashboard available at: {config.dashboard_secret_path}') app_logger.info(f'Dashboard available at: {config.dashboard_secret_path}')
if config.canary_token_url: if config.canary_token_url:
app_logger.info(f'Canary token will appear after {config.canary_token_tries} tries') app_logger.info(f'Canary token will appear after {config.canary_token_tries} tries')

View File

@@ -5,6 +5,18 @@ Dashboard template for viewing honeypot statistics.
Customize this template to change the dashboard appearance. Customize this template to change the dashboard appearance.
""" """
from datetime import datetime
def format_timestamp(iso_timestamp: str) -> str:
"""Format ISO timestamp for display (YYYY-MM-DD HH:MM:SS)"""
try:
dt = datetime.fromisoformat(iso_timestamp)
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
# Fallback for old format
return iso_timestamp.split("T")[1][:8] if "T" in iso_timestamp else iso_timestamp
def generate_dashboard(stats: dict) -> str: def generate_dashboard(stats: dict) -> str:
"""Generate dashboard HTML with access statistics""" """Generate dashboard HTML with access statistics"""
@@ -29,7 +41,7 @@ def generate_dashboard(stats: dict) -> str:
# Generate suspicious accesses rows # Generate suspicious accesses rows
suspicious_rows = '\n'.join([ suspicious_rows = '\n'.join([
f'<tr><td>{log["ip"]}</td><td>{log["path"]}</td><td style="word-break: break-all;">{log["user_agent"][:60]}</td><td>{log["timestamp"].split("T")[1][:8]}</td></tr>' f'<tr><td>{log["ip"]}</td><td>{log["path"]}</td><td style="word-break: break-all;">{log["user_agent"][:60]}</td><td>{format_timestamp(log["timestamp"])}</td></tr>'
for log in stats['recent_suspicious'][-10:] for log in stats['recent_suspicious'][-10:]
]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>' ]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
@@ -41,13 +53,13 @@ def generate_dashboard(stats: dict) -> str:
# Generate attack types rows # Generate attack types rows
attack_type_rows = '\n'.join([ attack_type_rows = '\n'.join([
f'<tr><td>{log["ip"]}</td><td>{log["path"]}</td><td>{", ".join(log["attack_types"])}</td><td style="word-break: break-all;">{log["user_agent"][:60]}</td><td>{log["timestamp"].split("T")[1][:8]}</td></tr>' f'<tr><td>{log["ip"]}</td><td>{log["path"]}</td><td>{", ".join(log["attack_types"])}</td><td style="word-break: break-all;">{log["user_agent"][:60]}</td><td>{format_timestamp(log["timestamp"])}</td></tr>'
for log in stats.get('attack_types', [])[-10:] for log in stats.get('attack_types', [])[-10:]
]) or '<tr><td colspan="4" style="text-align:center;">No attacks detected</td></tr>' ]) or '<tr><td colspan="4" style="text-align:center;">No attacks detected</td></tr>'
# Generate credential attempts rows # Generate credential attempts rows
credential_rows = '\n'.join([ credential_rows = '\n'.join([
f'<tr><td>{log["ip"]}</td><td>{log["username"]}</td><td>{log["password"]}</td><td>{log["path"]}</td><td>{log["timestamp"].split("T")[1][:8]}</td></tr>' f'<tr><td>{log["ip"]}</td><td>{log["username"]}</td><td>{log["password"]}</td><td>{log["path"]}</td><td>{format_timestamp(log["timestamp"])}</td></tr>'
for log in stats.get('credential_attempts', [])[-20:] for log in stats.get('credential_attempts', [])[-20:]
]) or '<tr><td colspan="5" style="text-align:center;">No credentials captured yet</td></tr>' ]) or '<tr><td colspan="5" style="text-align:center;">No credentials captured yet</td></tr>'

View File

@@ -1,20 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import Dict, List, Tuple from typing import Dict, List, Tuple, Optional
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo
import re import re
import urllib.parse import urllib.parse
class AccessTracker: class AccessTracker:
"""Track IP addresses and paths accessed""" """Track IP addresses and paths accessed"""
def __init__(self): def __init__(self, timezone: Optional[ZoneInfo] = None):
self.ip_counts: Dict[str, int] = defaultdict(int) self.ip_counts: Dict[str, int] = defaultdict(int)
self.path_counts: Dict[str, int] = defaultdict(int) self.path_counts: Dict[str, int] = defaultdict(int)
self.user_agent_counts: Dict[str, int] = defaultdict(int) self.user_agent_counts: Dict[str, int] = defaultdict(int)
self.access_log: List[Dict] = [] self.access_log: List[Dict] = []
self.credential_attempts: List[Dict] = [] self.credential_attempts: List[Dict] = []
self.timezone = timezone or ZoneInfo('UTC')
self.suspicious_patterns = [ self.suspicious_patterns = [
'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests', 'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests',
'scanner', 'nikto', 'sqlmap', 'nmap', 'masscan', 'nessus', 'acunetix', 'scanner', 'nikto', 'sqlmap', 'nmap', 'masscan', 'nessus', 'acunetix',
@@ -81,7 +83,7 @@ class AccessTracker:
'path': path, 'path': path,
'username': username, 'username': username,
'password': password, 'password': password,
'timestamp': datetime.now().isoformat() 'timestamp': datetime.now(self.timezone).isoformat()
}) })
def record_access(self, ip: str, path: str, user_agent: str = '', body: str = ''): def record_access(self, ip: str, path: str, user_agent: str = '', body: str = ''):
@@ -112,7 +114,7 @@ class AccessTracker:
'suspicious': is_suspicious, 'suspicious': is_suspicious,
'honeypot_triggered': self.is_honeypot_path(path), 'honeypot_triggered': self.is_honeypot_path(path),
'attack_types':attack_findings, 'attack_types':attack_findings,
'timestamp': datetime.now().isoformat() 'timestamp': datetime.now(self.timezone).isoformat()
}) })
def detect_attack_type(self, data:str) -> list[str]: def detect_attack_type(self, data:str) -> list[str]:

150
tests/test_credentials.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# This script sends various POST requests with credentials to the honeypot
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Configuration
HOST="localhost"
PORT="5000"
BASE_URL="http://${HOST}:${PORT}"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Krawl Credential Logging Test Script${NC}"
echo -e "${BLUE}========================================${NC}\n"
# Check if server is running
echo -e "${YELLOW}Checking if server is running on ${BASE_URL}...${NC}"
if ! curl -s -f "${BASE_URL}/health" > /dev/null 2>&1; then
echo -e "${RED}❌ Server is not running. Please start the Krawl server first.${NC}"
echo -e "${YELLOW}Run: python3 src/server.py${NC}"
exit 1
fi
echo -e "${GREEN}✓ Server is running${NC}\n"
# Test 1: Simple login form POST
echo -e "${YELLOW}Test 1: POST to /login with form data${NC}"
curl -s -X POST "${BASE_URL}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=admin123" \
> /dev/null
echo -e "${GREEN}✓ Sent: admin / admin123${NC}\n"
sleep 1
# Test 2: Admin panel login
echo -e "${YELLOW}Test 2: POST to /admin with credentials${NC}"
curl -s -X POST "${BASE_URL}/admin" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "user=root&pass=toor&submit=Login" \
> /dev/null
echo -e "${GREEN}✓ Sent: root / toor${NC}\n"
sleep 1
# Test 3: WordPress login attempt
echo -e "${YELLOW}Test 3: POST to /wp-login.php${NC}"
curl -s -X POST "${BASE_URL}/wp-login.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "log=wpuser&pwd=Password1&wp-submit=Log+In" \
> /dev/null
echo -e "${GREEN}✓ Sent: wpuser / Password1${NC}\n"
sleep 1
# Test 4: JSON formatted credentials
echo -e "${YELLOW}Test 4: POST to /api/login with JSON${NC}"
curl -s -X POST "${BASE_URL}/api/login" \
-H "Content-Type: application/json" \
-d '{"username":"apiuser","password":"apipass123","remember":true}' \
> /dev/null
echo -e "${GREEN}✓ Sent: apiuser / apipass123${NC}\n"
sleep 1
# Test 5: SSH-style login
echo -e "${YELLOW}Test 5: POST to /ssh with credentials${NC}"
curl -s -X POST "${BASE_URL}/ssh" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=sshuser&password=P@ssw0rd!" \
> /dev/null
echo -e "${GREEN}✓ Sent: sshuser / P@ssw0rd!${NC}\n"
sleep 1
# Test 6: Database admin
echo -e "${YELLOW}Test 6: POST to /phpmyadmin with credentials${NC}"
curl -s -X POST "${BASE_URL}/phpmyadmin" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "pma_username=dbadmin&pma_password=dbpass123&server=1" \
> /dev/null
echo -e "${GREEN}✓ Sent: dbadmin / dbpass123${NC}\n"
sleep 1
# Test 7: Multiple fields with email
echo -e "${YELLOW}Test 7: POST to /register with email${NC}"
curl -s -X POST "${BASE_URL}/register" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=test@example.com&username=newuser&password=NewPass123&confirm_password=NewPass123" \
> /dev/null
echo -e "${GREEN}✓ Sent: newuser / NewPass123 (email: test@example.com)${NC}\n"
sleep 1
# Test 8: FTP credentials
echo -e "${YELLOW}Test 8: POST to /ftp/login${NC}"
curl -s -X POST "${BASE_URL}/ftp/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "ftpuser=ftpadmin&ftppass=ftp123456" \
> /dev/null
echo -e "${GREEN}✓ Sent: ftpadmin / ftp123456${NC}\n"
sleep 1
# Test 9: Common brute force attempt
echo -e "${YELLOW}Test 9: Multiple attempts (simulating brute force)${NC}"
for i in {1..3}; do
curl -s -X POST "${BASE_URL}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=pass${i}" \
> /dev/null
echo -e "${GREEN}✓ Attempt $i: admin / pass${i}${NC}"
sleep 0.5
done
echo ""
sleep 1
# Test 10: Special characters in credentials
echo -e "${YELLOW}Test 10: POST with special characters${NC}"
curl -s -X POST "${BASE_URL}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "username=user@domain.com" \
--data-urlencode "password=P@\$\$w0rd!#%" \
> /dev/null
echo -e "${GREEN}✓ Sent: user@domain.com / P@\$\$w0rd!#%${NC}\n"
echo -e "${BLUE}========================================${NC}"
echo -e "${GREEN}✓ All credential tests completed!${NC}"
echo -e "${BLUE}========================================${NC}\n"
echo -e "${YELLOW}Check the results:${NC}"
echo -e " 1. View the log file: ${GREEN}cat src/logs/credentials.log${NC}"
echo -e " 2. View the dashboard: ${GREEN}${BASE_URL}/dashboard${NC}"
echo -e " 3. Check recent logs: ${GREEN}tail -20 src/logs/krawl.log${NC}\n"
# Display last 10 credential entries if log file exists
if [ -f "src/logs/credentials.log" ]; then
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Last 10 Captured Credentials:${NC}"
echo -e "${BLUE}========================================${NC}"
tail -10 src/logs/credentials.log
echo ""
fi
echo -e "${YELLOW}💡 Tip: Open ${BASE_URL}/dashboard in your browser to see the credentials in real-time!${NC}"