First commit
This commit is contained in:
48
src/config.py
Normal file
48
src/config.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Configuration class for the deception server"""
|
||||
port: int = 5000
|
||||
delay: int = 100 # milliseconds
|
||||
links_length_range: Tuple[int, int] = (5, 15)
|
||||
links_per_page_range: Tuple[int, int] = (10, 15)
|
||||
char_space: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
max_counter: int = 10
|
||||
canary_token_url: Optional[str] = None
|
||||
canary_token_tries: int = 10
|
||||
dashboard_secret_path: str = None
|
||||
api_server_url: Optional[str] = None
|
||||
api_server_port: int = 8080
|
||||
api_server_path: str = "/api/v2/users"
|
||||
probability_error_codes: int = 0 # Percentage (0-100)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> 'Config':
|
||||
"""Create configuration from environment variables"""
|
||||
return cls(
|
||||
port=int(os.getenv('PORT', 5000)),
|
||||
delay=int(os.getenv('DELAY', 100)),
|
||||
links_length_range=(
|
||||
int(os.getenv('LINKS_MIN_LENGTH', 5)),
|
||||
int(os.getenv('LINKS_MAX_LENGTH', 15))
|
||||
),
|
||||
links_per_page_range=(
|
||||
int(os.getenv('LINKS_MIN_PER_PAGE', 10)),
|
||||
int(os.getenv('LINKS_MAX_PER_PAGE', 15))
|
||||
),
|
||||
char_space=os.getenv('CHAR_SPACE', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'),
|
||||
max_counter=int(os.getenv('MAX_COUNTER', 10)),
|
||||
canary_token_url=os.getenv('CANARY_TOKEN_URL'),
|
||||
canary_token_tries=int(os.getenv('CANARY_TOKEN_TRIES', 10)),
|
||||
dashboard_secret_path=os.getenv('DASHBOARD_SECRET_PATH', f'/{os.urandom(16).hex()}'),
|
||||
api_server_url=os.getenv('API_SERVER_URL'),
|
||||
api_server_port=int(os.getenv('API_SERVER_PORT', 8080)),
|
||||
api_server_path=os.getenv('API_SERVER_PATH', '/api/v2/users'),
|
||||
probability_error_codes=int(os.getenv('PROBABILITY_ERROR_CODES', 5))
|
||||
)
|
||||
214
src/dashboard_template.py
Normal file
214
src/dashboard_template.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/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'<tr><td class="rank">{i+1}</td><td>{ip}</td><td>{count}</td></tr>'
|
||||
for i, (ip, count) in enumerate(stats['top_ips'])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
||||
|
||||
# Generate paths rows
|
||||
top_paths_rows = '\n'.join([
|
||||
f'<tr><td class="rank">{i+1}</td><td>{path}</td><td>{count}</td></tr>'
|
||||
for i, (path, count) in enumerate(stats['top_paths'])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
||||
|
||||
# Generate User-Agent rows
|
||||
top_ua_rows = '\n'.join([
|
||||
f'<tr><td class="rank">{i+1}</td><td style="word-break: break-all;">{ua[:80]}</td><td>{count}</td></tr>'
|
||||
for i, (ua, count) in enumerate(stats['top_user_agents'])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
||||
|
||||
# Generate suspicious accesses rows
|
||||
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>'
|
||||
for log in stats['recent_suspicious'][-10:]
|
||||
]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Krawl Dashboard</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #0d1117;
|
||||
color: #c9d1d9;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
h1 {{
|
||||
color: #58a6ff;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}}
|
||||
.stats-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}}
|
||||
.stat-card {{
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}}
|
||||
.stat-card.alert {{
|
||||
border-color: #f85149;
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #58a6ff;
|
||||
}}
|
||||
.stat-value.alert {{
|
||||
color: #f85149;
|
||||
}}
|
||||
.stat-label {{
|
||||
font-size: 14px;
|
||||
color: #8b949e;
|
||||
margin-top: 5px;
|
||||
}}
|
||||
.table-container {{
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
h2 {{
|
||||
color: #58a6ff;
|
||||
margin-top: 0;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #30363d;
|
||||
}}
|
||||
th {{
|
||||
background: #0d1117;
|
||||
color: #58a6ff;
|
||||
font-weight: 600;
|
||||
}}
|
||||
tr:hover {{
|
||||
background: #1c2128;
|
||||
}}
|
||||
.rank {{
|
||||
color: #8b949e;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.alert-section {{
|
||||
background: #1c1917;
|
||||
border-left: 4px solid #f85149;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🕷️ Krawl Dashboard</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['total_accesses']}</div>
|
||||
<div class="stat-label">Total Accesses</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['unique_ips']}</div>
|
||||
<div class="stat-label">Unique IPs</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['unique_paths']}</div>
|
||||
<div class="stat-label">Unique Paths</div>
|
||||
</div>
|
||||
<div class="stat-card alert">
|
||||
<div class="stat-value alert">{stats['suspicious_accesses']}</div>
|
||||
<div class="stat-label">Suspicious Accesses</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container alert-section">
|
||||
<h2>⚠️ Recent Suspicious Activity</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Path</th>
|
||||
<th>User-Agent</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{suspicious_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top IP Addresses</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>IP Address</th>
|
||||
<th>Access Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top_ips_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top Paths</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Path</th>
|
||||
<th>Access Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top_paths_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top User-Agents</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>User-Agent</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top_ua_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
190
src/generators.py
Normal file
190
src/generators.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Generators for creating random fake data (credentials, API keys, etc.)
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
import json
|
||||
from templates import html_templates
|
||||
from wordlists import get_wordlists
|
||||
|
||||
|
||||
def random_username() -> str:
|
||||
"""Generate random username"""
|
||||
wl = get_wordlists()
|
||||
return random.choice(wl.username_prefixes) + random.choice(wl.username_suffixes)
|
||||
|
||||
|
||||
def random_password() -> str:
|
||||
"""Generate random password"""
|
||||
wl = get_wordlists()
|
||||
templates = [
|
||||
lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=12)),
|
||||
lambda: f"{random.choice(wl.password_prefixes)}{random.randint(100, 999)}!",
|
||||
lambda: f"{random.choice(wl.simple_passwords)}{random.randint(1000, 9999)}",
|
||||
lambda: ''.join(random.choices(string.ascii_lowercase, k=8)),
|
||||
]
|
||||
return random.choice(templates)()
|
||||
|
||||
|
||||
def random_email(username: str = None) -> str:
|
||||
"""Generate random email"""
|
||||
wl = get_wordlists()
|
||||
if not username:
|
||||
username = random_username()
|
||||
return f"{username}@{random.choice(wl.email_domains)}"
|
||||
|
||||
|
||||
def random_api_key() -> str:
|
||||
"""Generate random API key"""
|
||||
wl = get_wordlists()
|
||||
key = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||
return random.choice(wl.api_key_prefixes) + key
|
||||
|
||||
|
||||
def random_database_name() -> str:
|
||||
"""Generate random database name"""
|
||||
wl = get_wordlists()
|
||||
return random.choice(wl.database_names)
|
||||
|
||||
|
||||
def credentials_txt() -> str:
|
||||
"""Generate fake credentials.txt with random data"""
|
||||
content = "# Production Credentials\n\n"
|
||||
for i in range(random.randint(3, 7)):
|
||||
username = random_username()
|
||||
password = random_password()
|
||||
content += f"{username}:{password}\n"
|
||||
return content
|
||||
|
||||
|
||||
def passwords_txt() -> str:
|
||||
"""Generate fake passwords.txt with random data"""
|
||||
content = "# Password List\n"
|
||||
content += f"Admin Password: {random_password()}\n"
|
||||
content += f"Database Password: {random_password()}\n"
|
||||
content += f"API Key: {random_api_key()}\n\n"
|
||||
content += "User Passwords:\n"
|
||||
for i in range(random.randint(5, 10)):
|
||||
username = random_username()
|
||||
password = random_password()
|
||||
content += f"{username} = {password}\n"
|
||||
return content
|
||||
|
||||
|
||||
def users_json() -> str:
|
||||
"""Generate fake users.json with random data"""
|
||||
wl = get_wordlists()
|
||||
users = []
|
||||
for i in range(random.randint(3, 8)):
|
||||
username = random_username()
|
||||
users.append({
|
||||
"id": i + 1,
|
||||
"username": username,
|
||||
"email": random_email(username),
|
||||
"password": random_password(),
|
||||
"role": random.choice(wl.user_roles),
|
||||
"api_token": random_api_key()
|
||||
})
|
||||
return json.dumps({"users": users}, indent=2)
|
||||
|
||||
|
||||
def api_keys_json() -> str:
|
||||
"""Generate fake api_keys.json with random data"""
|
||||
keys = {
|
||||
"stripe": {
|
||||
"public_key": "pk_live_" + ''.join(random.choices(string.ascii_letters + string.digits, k=24)),
|
||||
"secret_key": random_api_key()
|
||||
},
|
||||
"aws": {
|
||||
"access_key_id": "AKIA" + ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)),
|
||||
"secret_access_key": ''.join(random.choices(string.ascii_letters + string.digits + '+/', k=40))
|
||||
},
|
||||
"sendgrid": {
|
||||
"api_key": "SG." + ''.join(random.choices(string.ascii_letters + string.digits, k=48))
|
||||
},
|
||||
"twilio": {
|
||||
"account_sid": "AC" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=32)),
|
||||
"auth_token": ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
|
||||
}
|
||||
}
|
||||
return json.dumps(keys, indent=2)
|
||||
|
||||
|
||||
def api_response(path: str) -> str:
|
||||
"""Generate fake API JSON responses with random data"""
|
||||
wl = get_wordlists()
|
||||
|
||||
def random_users(count: int = 3):
|
||||
users = []
|
||||
for i in range(count):
|
||||
username = random_username()
|
||||
users.append({
|
||||
"id": i + 1,
|
||||
"username": username,
|
||||
"email": random_email(username),
|
||||
"role": random.choice(wl.user_roles)
|
||||
})
|
||||
return users
|
||||
|
||||
responses = {
|
||||
'/api/users': json.dumps({
|
||||
"users": random_users(random.randint(2, 5)),
|
||||
"total": random.randint(50, 500)
|
||||
}, indent=2),
|
||||
'/api/v1/users': json.dumps({
|
||||
"status": "success",
|
||||
"data": [{
|
||||
"id": random.randint(1, 100),
|
||||
"name": random_username(),
|
||||
"api_key": random_api_key()
|
||||
}]
|
||||
}, indent=2),
|
||||
'/api/v2/secrets': json.dumps({
|
||||
"database": {
|
||||
"host": random.choice(wl.database_hosts),
|
||||
"username": random_username(),
|
||||
"password": random_password(),
|
||||
"database": random_database_name()
|
||||
},
|
||||
"api_keys": {
|
||||
"stripe": random_api_key(),
|
||||
"aws": 'AKIA' + ''.join(random.choices(string.ascii_uppercase + string.digits, k=16))
|
||||
}
|
||||
}, indent=2),
|
||||
'/api/config': json.dumps({
|
||||
"app_name": random.choice(wl.application_names),
|
||||
"debug": random.choice([True, False]),
|
||||
"secret_key": random_api_key(),
|
||||
"database_url": f"postgresql://{random_username()}:{random_password()}@localhost/{random_database_name()}"
|
||||
}, indent=2),
|
||||
'/.env': f"""APP_NAME={random.choice(wl.application_names)}
|
||||
DEBUG={random.choice(['true', 'false'])}
|
||||
APP_KEY=base64:{''.join(random.choices(string.ascii_letters + string.digits, k=32))}=
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE={random_database_name()}
|
||||
DB_USERNAME={random_username()}
|
||||
DB_PASSWORD={random_password()}
|
||||
AWS_ACCESS_KEY_ID=AKIA{''.join(random.choices(string.ascii_uppercase + string.digits, k=16))}
|
||||
AWS_SECRET_ACCESS_KEY={''.join(random.choices(string.ascii_letters + string.digits + '+/', k=40))}
|
||||
STRIPE_SECRET={random_api_key()}
|
||||
"""
|
||||
}
|
||||
return responses.get(path, json.dumps({"error": "Not found"}, indent=2))
|
||||
|
||||
|
||||
def directory_listing(path: str) -> str:
|
||||
"""Generate fake directory listing using wordlists"""
|
||||
wl = get_wordlists()
|
||||
|
||||
files = wl.directory_files
|
||||
dirs = wl.directory_dirs
|
||||
|
||||
selected_files = [(f, random.randint(1024, 1024*1024))
|
||||
for f in random.sample(files, min(6, len(files)))]
|
||||
|
||||
return html_templates.directory_listing(path, dirs, selected_files)
|
||||
342
src/handler.py
Normal file
342
src/handler.py
Normal file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import random
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from config import Config
|
||||
from tracker import AccessTracker
|
||||
from templates import html_templates
|
||||
from templates.dashboard_template import generate_dashboard
|
||||
from generators import (
|
||||
credentials_txt, passwords_txt, users_json, api_keys_json,
|
||||
api_response, directory_listing
|
||||
)
|
||||
from wordlists import get_wordlists
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
"""HTTP request handler for the deception server"""
|
||||
webpages: Optional[List[str]] = None
|
||||
config: Config = None
|
||||
tracker: AccessTracker = None
|
||||
counter: int = 0
|
||||
|
||||
def _get_client_ip(self) -> str:
|
||||
"""Extract client IP address from request, checking proxy headers first"""
|
||||
# Headers might not be available during early error logging
|
||||
if hasattr(self, 'headers') and self.headers:
|
||||
# Check X-Forwarded-For header (set by load balancers/proxies)
|
||||
forwarded_for = self.headers.get('X-Forwarded-For')
|
||||
if forwarded_for:
|
||||
# X-Forwarded-For can contain multiple IPs, get the first (original client)
|
||||
return forwarded_for.split(',')[0].strip()
|
||||
|
||||
# Check X-Real-IP header (set by nginx and other proxies)
|
||||
real_ip = self.headers.get('X-Real-IP')
|
||||
if real_ip:
|
||||
return real_ip.strip()
|
||||
|
||||
# Fallback to direct connection IP
|
||||
return self.client_address[0]
|
||||
|
||||
def _get_user_agent(self) -> str:
|
||||
"""Extract user agent from request"""
|
||||
return self.headers.get('User-Agent', '')
|
||||
|
||||
def _should_return_error(self) -> bool:
|
||||
"""Check if we should return an error based on probability"""
|
||||
if self.config.probability_error_codes <= 0:
|
||||
return False
|
||||
return random.randint(1, 100) <= self.config.probability_error_codes
|
||||
|
||||
def _get_random_error_code(self) -> int:
|
||||
"""Get a random error code from wordlists"""
|
||||
wl = get_wordlists()
|
||||
error_codes = wl.error_codes
|
||||
if not error_codes:
|
||||
error_codes = [400, 401, 403, 404, 500, 502, 503]
|
||||
return random.choice(error_codes)
|
||||
|
||||
def generate_page(self, seed: str) -> str:
|
||||
"""Generate a webpage containing random links or canary token"""
|
||||
random.seed(seed)
|
||||
num_pages = random.randint(*self.config.links_per_page_range)
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Krawl</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #0d1117;
|
||||
color: #c9d1d9;
|
||||
margin: 0;
|
||||
padding: 40px 20px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}}
|
||||
h1 {{
|
||||
color: #f85149;
|
||||
text-align: center;
|
||||
font-size: 48px;
|
||||
margin: 60px 0 30px;
|
||||
}}
|
||||
.counter {{
|
||||
color: #f85149;
|
||||
text-align: center;
|
||||
font-size: 56px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 60px;
|
||||
}}
|
||||
.links-container {{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}}
|
||||
.link-box {{
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 15px 30px;
|
||||
min-width: 300px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}}
|
||||
.link-box:hover {{
|
||||
background: #1c2128;
|
||||
border-color: #58a6ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(88, 166, 255, 0.2);
|
||||
}}
|
||||
a {{
|
||||
color: #58a6ff;
|
||||
text-decoration: none;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}}
|
||||
a:hover {{
|
||||
color: #79c0ff;
|
||||
}}
|
||||
.canary-token {{
|
||||
background: #1c1917;
|
||||
border: 2px solid #f85149;
|
||||
border-radius: 8px;
|
||||
padding: 30px 50px;
|
||||
margin: 40px auto;
|
||||
max-width: 800px;
|
||||
overflow-x: auto;
|
||||
}}
|
||||
.canary-token a {{
|
||||
color: #f85149;
|
||||
font-size: 18px;
|
||||
white-space: nowrap;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Krawl me! 🕸</h1>
|
||||
<div class="counter">{Handler.counter}</div>
|
||||
|
||||
<div class="links-container">
|
||||
"""
|
||||
|
||||
if Handler.counter <= 0 and self.config.canary_token_url:
|
||||
html += f"""
|
||||
<div class="link-box canary-token">
|
||||
<a href="{self.config.canary_token_url}">{self.config.canary_token_url}</a>
|
||||
</div>
|
||||
"""
|
||||
|
||||
if self.webpages is None:
|
||||
for _ in range(num_pages):
|
||||
address = ''.join([
|
||||
random.choice(self.config.char_space)
|
||||
for _ in range(random.randint(*self.config.links_length_range))
|
||||
])
|
||||
html += f"""
|
||||
<div class="link-box">
|
||||
<a href="{address}">{address}</a>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
for _ in range(num_pages):
|
||||
address = random.choice(self.webpages)
|
||||
html += f"""
|
||||
<div class="link-box">
|
||||
<a href="{address}">{address}</a>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
return html
|
||||
|
||||
def do_HEAD(self):
|
||||
"""Sends header information"""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
"""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)
|
||||
|
||||
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')
|
||||
print(f"[POST DATA] {post_data[:200]}")
|
||||
|
||||
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())
|
||||
|
||||
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
|
||||
|
||||
return False
|
||||
|
||||
def do_GET(self):
|
||||
"""Responds to webpage requests"""
|
||||
client_ip = self._get_client_ip()
|
||||
user_agent = self._get_user_agent()
|
||||
|
||||
if self.config.dashboard_secret_path and self.path == self.config.dashboard_secret_path:
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
try:
|
||||
stats = self.tracker.get_stats()
|
||||
self.wfile.write(generate_dashboard(stats).encode())
|
||||
except Exception as e:
|
||||
print(f"Error generating dashboard: {e}")
|
||||
return
|
||||
|
||||
self.tracker.record_access(client_ip, self.path, user_agent)
|
||||
|
||||
if self.tracker.is_suspicious_user_agent(user_agent):
|
||||
print(f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {self.path}")
|
||||
|
||||
if self._should_return_error():
|
||||
error_code = self._get_random_error_code()
|
||||
print(f"[ERROR] Returning {error_code} to {client_ip} - {self.path}")
|
||||
self.send_response(error_code)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if self.serve_special_path(self.path):
|
||||
return
|
||||
|
||||
time.sleep(self.config.delay / 1000.0)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
try:
|
||||
self.wfile.write(self.generate_page(self.path).encode())
|
||||
|
||||
Handler.counter -= 1
|
||||
|
||||
if Handler.counter < 0:
|
||||
Handler.counter = self.config.canary_token_tries
|
||||
except Exception as e:
|
||||
print(f"Error generating page: {e}")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Override to customize logging"""
|
||||
client_ip = self._get_client_ip()
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {client_ip} - {format % args}")
|
||||
83
src/server.py
Normal file
83
src/server.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Main server module for the deception honeypot.
|
||||
Run this file to start the server.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from http.server import HTTPServer
|
||||
|
||||
from config import Config
|
||||
from tracker import AccessTracker
|
||||
from handler import Handler
|
||||
|
||||
|
||||
def print_usage():
|
||||
"""Print usage information"""
|
||||
print(f'Usage: {sys.argv[0]} [FILE]\n')
|
||||
print('FILE is file containing a list of webpage names to serve, one per line.')
|
||||
print('If no file is provided, random links will be generated.\n')
|
||||
print('Environment Variables:')
|
||||
print(' PORT - Server port (default: 5000)')
|
||||
print(' DELAY - Response delay in ms (default: 100)')
|
||||
print(' LINKS_MIN_LENGTH - Min link length (default: 5)')
|
||||
print(' LINKS_MAX_LENGTH - Max link length (default: 15)')
|
||||
print(' LINKS_MIN_PER_PAGE - Min links per page (default: 10)')
|
||||
print(' LINKS_MAX_PER_PAGE - Max links per page (default: 15)')
|
||||
print(' MAX_COUNTER - Max counter value (default: 10)')
|
||||
print(' CANARY_TOKEN_URL - Canary token URL to display')
|
||||
print(' CANARY_TOKEN_TRIES - Number of tries before showing token (default: 10)')
|
||||
print(' DASHBOARD_SECRET_PATH - Secret path for dashboard (auto-generated if not set)')
|
||||
print(' PROBABILITY_ERROR_CODES - Probability (0-100) to return HTTP error codes (default: 0)')
|
||||
print(' CHAR_SPACE - Characters for random links')
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the deception server"""
|
||||
if '-h' in sys.argv or '--help' in sys.argv:
|
||||
print_usage()
|
||||
exit(0)
|
||||
|
||||
config = Config.from_env()
|
||||
|
||||
tracker = AccessTracker()
|
||||
|
||||
Handler.config = config
|
||||
Handler.tracker = tracker
|
||||
Handler.counter = config.canary_token_tries
|
||||
|
||||
if len(sys.argv) == 2:
|
||||
try:
|
||||
with open(sys.argv[1], 'r') as f:
|
||||
Handler.webpages = f.readlines()
|
||||
|
||||
if not Handler.webpages:
|
||||
print('The file provided was empty. Using randomly generated links.')
|
||||
Handler.webpages = None
|
||||
except IOError:
|
||||
print('Can\'t read input file. Using randomly generated links.')
|
||||
|
||||
try:
|
||||
print(f'Starting deception server on port {config.port}...')
|
||||
print(f'Dashboard available at: {config.dashboard_secret_path}')
|
||||
if config.canary_token_url:
|
||||
print(f'Canary token will appear after {config.canary_token_tries} tries')
|
||||
else:
|
||||
print('No canary token configured (set CANARY_TOKEN_URL to enable)')
|
||||
|
||||
server = HTTPServer(('0.0.0.0', config.port), Handler)
|
||||
print('Server started. Use <Ctrl-C> to stop.')
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print('\nStopping server...')
|
||||
server.socket.close()
|
||||
print('Server stopped')
|
||||
except Exception as e:
|
||||
print(f'Error starting HTTP server on port {config.port}: {e}')
|
||||
print(f'Make sure you are root, if needed, and that port {config.port} is open.')
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
241
src/templates/dashboard_template.py
Normal file
241
src/templates/dashboard_template.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/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"""
|
||||
|
||||
# Generate IP rows
|
||||
top_ips_rows = '\n'.join([
|
||||
f'<tr><td class="rank">{i+1}</td><td>{ip}</td><td>{count}</td></tr>'
|
||||
for i, (ip, count) in enumerate(stats['top_ips'])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
||||
|
||||
# Generate paths rows
|
||||
top_paths_rows = '\n'.join([
|
||||
f'<tr><td class="rank">{i+1}</td><td>{path}</td><td>{count}</td></tr>'
|
||||
for i, (path, count) in enumerate(stats['top_paths'])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
||||
|
||||
# Generate User-Agent rows
|
||||
top_ua_rows = '\n'.join([
|
||||
f'<tr><td class="rank">{i+1}</td><td style="word-break: break-all;">{ua[:80]}</td><td>{count}</td></tr>'
|
||||
for i, (ua, count) in enumerate(stats['top_user_agents'])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No data</td></tr>'
|
||||
|
||||
# Generate suspicious accesses rows
|
||||
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>'
|
||||
for log in stats['recent_suspicious'][-10:]
|
||||
]) or '<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>'
|
||||
|
||||
# Generate honeypot triggered IPs rows
|
||||
honeypot_rows = '\n'.join([
|
||||
f'<tr><td>{ip}</td><td style="word-break: break-all;">{", ".join(paths)}</td><td>{len(paths)}</td></tr>'
|
||||
for ip, paths in stats.get('honeypot_triggered_ips', [])
|
||||
]) or '<tr><td colspan="3" style="text-align:center;">No honeypot triggers yet</td></tr>'
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Krawl Dashboard</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #0d1117;
|
||||
color: #c9d1d9;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
h1 {{
|
||||
color: #58a6ff;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}}
|
||||
.stats-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}}
|
||||
.stat-card {{
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}}
|
||||
.stat-card.alert {{
|
||||
border-color: #f85149;
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #58a6ff;
|
||||
}}
|
||||
.stat-value.alert {{
|
||||
color: #f85149;
|
||||
}}
|
||||
.stat-label {{
|
||||
font-size: 14px;
|
||||
color: #8b949e;
|
||||
margin-top: 5px;
|
||||
}}
|
||||
.table-container {{
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
h2 {{
|
||||
color: #58a6ff;
|
||||
margin-top: 0;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #30363d;
|
||||
}}
|
||||
th {{
|
||||
background: #0d1117;
|
||||
color: #58a6ff;
|
||||
font-weight: 600;
|
||||
}}
|
||||
tr:hover {{
|
||||
background: #1c2128;
|
||||
}}
|
||||
.rank {{
|
||||
color: #8b949e;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.alert-section {{
|
||||
background: #1c1917;
|
||||
border-left: 4px solid #f85149;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🕷️ Krawl Dashboard</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['total_accesses']}</div>
|
||||
<div class="stat-label">Total Accesses</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['unique_ips']}</div>
|
||||
<div class="stat-label">Unique IPs</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{stats['unique_paths']}</div>
|
||||
<div class="stat-label">Unique Paths</div>
|
||||
</div>
|
||||
<div class="stat-card alert">
|
||||
<div class="stat-value alert">{stats['suspicious_accesses']}</div>
|
||||
<div class="stat-label">Suspicious Accesses</div>
|
||||
</div>
|
||||
<div class="stat-card alert">
|
||||
<div class="stat-value alert">{stats.get('honeypot_ips', 0)}</div>
|
||||
<div class="stat-label">Honeypot Caught</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container alert-section">
|
||||
<h2>🍯 Honeypot Triggers</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Accessed Paths</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{honeypot_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container alert-section">
|
||||
<h2>⚠️ Recent Suspicious Activity</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Path</th>
|
||||
<th>User-Agent</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{suspicious_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top IP Addresses</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>IP Address</th>
|
||||
<th>Access Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top_ips_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top Paths</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Path</th>
|
||||
<th>Access Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top_paths_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h2>Top User-Agents</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>User-Agent</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top_ua_rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
229
src/templates/html_templates.py
Normal file
229
src/templates/html_templates.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
HTML templates for the deception server.
|
||||
Edit these templates to customize the appearance of fake pages.
|
||||
"""
|
||||
|
||||
|
||||
def login_form() -> str:
|
||||
"""Generate fake login page"""
|
||||
return """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Admin Login</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f0f0f0; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 300px; }
|
||||
h2 { margin-top: 0; color: #333; }
|
||||
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>Admin Login</h2>
|
||||
<form action="/admin/login" method="post">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def login_error() -> str:
|
||||
"""Generate fake login error page"""
|
||||
return """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login Failed</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background: #f0f0f0; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||
.login-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 300px; }
|
||||
h2 { margin-top: 0; color: #333; }
|
||||
.error { color: #d63301; background: #ffebe8; border: 1px solid #d63301; padding: 12px; margin-bottom: 20px; border-radius: 4px; }
|
||||
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
|
||||
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
a { color: #007bff; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>Admin Login</h2>
|
||||
<div class="error"><strong>ERROR:</strong> Invalid username or password.</div>
|
||||
<form action="/admin/login" method="post">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p style="margin-top: 20px; text-align: center;"><a href="/forgot-password">Forgot your password?</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def wordpress() -> str:
|
||||
"""Generate fake WordPress page"""
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>My Blog – Just another WordPress site</title>
|
||||
<link rel='dns-prefetch' href='//s.w.org' />
|
||||
<link rel='stylesheet' id='wp-block-library-css' href='/wp-includes/css/dist/block-library/style.min.css' type='text/css' media='all' />
|
||||
<link rel='stylesheet' id='twentytwentythree-style-css' href='/wp-content/themes/twentytwentythree/style.css' type='text/css' media='all' />
|
||||
<link rel='https://api.w.org/' href='/wp-json/' />
|
||||
<meta name="generator" content="WordPress 6.4.2" />
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; margin: 0; padding: 0; background: #fff; }
|
||||
.site-header { background: #23282d; color: white; padding: 20px; border-bottom: 4px solid #0073aa; }
|
||||
.site-header h1 { margin: 0; font-size: 28px; }
|
||||
.site-header p { margin: 5px 0 0; color: #d0d0d0; }
|
||||
.site-content { max-width: 1200px; margin: 40px auto; padding: 0 20px; }
|
||||
.entry { background: #fff; margin-bottom: 40px; padding: 30px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
.entry-title { font-size: 32px; margin-top: 0; color: #23282d; }
|
||||
.entry-meta { color: #666; font-size: 14px; margin-bottom: 20px; }
|
||||
.entry-content { line-height: 1.8; color: #444; }
|
||||
.site-footer { background: #f7f7f7; padding: 20px; text-align: center; color: #666; border-top: 1px solid #ddd; margin-top: 60px; }
|
||||
a { color: #0073aa; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="home blog wp-embed-responsive">
|
||||
<div id="page" class="site">
|
||||
<header id="masthead" class="site-header">
|
||||
<div class="site-branding">
|
||||
<h1 class="site-title">My Blog</h1>
|
||||
<p class="site-description">Just another WordPress site</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="content" class="site-content">
|
||||
<article id="post-1" class="entry">
|
||||
<header class="entry-header">
|
||||
<h2 class="entry-title">Hello world!</h2>
|
||||
<div class="entry-meta">
|
||||
<span class="posted-on">Posted on <time datetime="2024-12-01">December 1, 2024</time></span>
|
||||
<span class="byline"> by <span class="author">admin</span></span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="entry-content">
|
||||
<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article id="post-2" class="entry">
|
||||
<header class="entry-header">
|
||||
<h2 class="entry-title">About This Site</h2>
|
||||
<div class="entry-meta">
|
||||
<span class="posted-on">Posted on <time datetime="2024-11-28">November 28, 2024</time></span>
|
||||
<span class="byline"> by <span class="author">admin</span></span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="entry-content">
|
||||
<p>This is a sample page. You can use it to write about your site, yourself, or anything else you'd like.</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<footer id="colophon" class="site-footer">
|
||||
<div class="site-info">
|
||||
Proudly powered by <a href="https://wordpress.org/">WordPress</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script type='text/javascript' src='/wp-includes/js/wp-embed.min.js'></script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def phpmyadmin() -> str:
|
||||
"""Generate fake phpMyAdmin page"""
|
||||
return """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>phpMyAdmin</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; background: #f0f0f0; }
|
||||
.header { background: #2979ff; color: white; padding: 10px 20px; }
|
||||
.login { background: white; width: 400px; margin: 100px auto; padding: 30px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
input { width: 100%; padding: 8px; margin: 8px 0; border: 1px solid #ddd; }
|
||||
button { padding: 10px 20px; background: #2979ff; color: white; border: none; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><h1>phpMyAdmin</h1></div>
|
||||
<div class="login">
|
||||
<h2>MySQL Server Login</h2>
|
||||
<form action="/phpMyAdmin/index.php" method="post">
|
||||
<input type="text" name="pma_username" placeholder="Username">
|
||||
<input type="password" name="pma_password" placeholder="Password">
|
||||
<button type="submit">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
def directory_listing(path: str, dirs: list, files: list) -> str:
|
||||
"""Generate fake directory listing"""
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Index of {path}</title>
|
||||
<style>
|
||||
body {{ font-family: monospace; background: #fff; padding: 20px; }}
|
||||
h1 {{ border-bottom: 1px solid #ccc; padding-bottom: 10px; }}
|
||||
table {{ width: 100%; border-collapse: collapse; }}
|
||||
th {{ text-align: left; padding: 10px; background: #f0f0f0; }}
|
||||
td {{ padding: 8px; border-bottom: 1px solid #eee; }}
|
||||
a {{ color: #0066cc; text-decoration: none; }}
|
||||
a:hover {{ text-decoration: underline; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of {path}</h1>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Last Modified</th><th>Size</th></tr>
|
||||
<tr><td><a href="../">Parent Directory</a></td><td>-</td><td>-</td></tr>
|
||||
"""
|
||||
|
||||
for d in dirs:
|
||||
html += f'<tr><td><a href="{d}">{d}</a></td><td>2024-12-01 10:30</td><td>-</td></tr>\n'
|
||||
|
||||
for f, size in files:
|
||||
html += f'<tr><td><a href="{f}">{f}</a></td><td>2024-12-01 14:22</td><td>{size}</td></tr>\n'
|
||||
|
||||
html += '</table></body></html>'
|
||||
return html
|
||||
114
src/tracker.py
Normal file
114
src/tracker.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AccessTracker:
|
||||
"""Track IP addresses and paths accessed"""
|
||||
def __init__(self):
|
||||
self.ip_counts: Dict[str, int] = defaultdict(int)
|
||||
self.path_counts: Dict[str, int] = defaultdict(int)
|
||||
self.user_agent_counts: Dict[str, int] = defaultdict(int)
|
||||
self.access_log: List[Dict] = []
|
||||
self.suspicious_patterns = [
|
||||
'bot', 'crawler', 'spider', 'scraper', 'curl', 'wget', 'python-requests',
|
||||
'scanner', 'nikto', 'sqlmap', 'nmap', 'masscan', 'nessus', 'acunetix',
|
||||
'burp', 'zap', 'w3af', 'metasploit', 'nuclei', 'gobuster', 'dirbuster'
|
||||
]
|
||||
# Track IPs that accessed honeypot paths from robots.txt
|
||||
self.honeypot_triggered: Dict[str, List[str]] = defaultdict(list)
|
||||
|
||||
def record_access(self, ip: str, path: str, user_agent: str = ''):
|
||||
"""Record an access attempt"""
|
||||
self.ip_counts[ip] += 1
|
||||
self.path_counts[path] += 1
|
||||
if user_agent:
|
||||
self.user_agent_counts[user_agent] += 1
|
||||
|
||||
is_suspicious = self.is_suspicious_user_agent(user_agent) or self.is_honeypot_path(path)
|
||||
|
||||
# Track if this IP accessed a honeypot path
|
||||
if self.is_honeypot_path(path):
|
||||
self.honeypot_triggered[ip].append(path)
|
||||
|
||||
self.access_log.append({
|
||||
'ip': ip,
|
||||
'path': path,
|
||||
'user_agent': user_agent,
|
||||
'suspicious': is_suspicious,
|
||||
'honeypot_triggered': self.is_honeypot_path(path),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
def is_honeypot_path(self, path: str) -> bool:
|
||||
"""Check if path is one of the honeypot traps from robots.txt"""
|
||||
honeypot_paths = [
|
||||
'/admin',
|
||||
'/admin/',
|
||||
'/backup',
|
||||
'/backup/',
|
||||
'/config',
|
||||
'/config/',
|
||||
'/private',
|
||||
'/private/',
|
||||
'/database',
|
||||
'/database/',
|
||||
'/credentials.txt',
|
||||
'/passwords.txt',
|
||||
'/admin_notes.txt',
|
||||
'/api_keys.json',
|
||||
'/.env',
|
||||
'/wp-admin',
|
||||
'/wp-admin/',
|
||||
'/phpmyadmin',
|
||||
'/phpMyAdmin/'
|
||||
]
|
||||
return path in honeypot_paths or any(hp in path.lower() for hp in ['/backup', '/admin', '/config', '/private', '/database', 'phpmyadmin'])
|
||||
|
||||
def is_suspicious_user_agent(self, user_agent: str) -> bool:
|
||||
"""Check if user agent matches suspicious patterns"""
|
||||
if not user_agent:
|
||||
return True
|
||||
ua_lower = user_agent.lower()
|
||||
return any(pattern in ua_lower for pattern in self.suspicious_patterns)
|
||||
|
||||
def get_top_ips(self, limit: int = 10) -> List[Tuple[str, int]]:
|
||||
"""Get top N IP addresses by access count"""
|
||||
return sorted(self.ip_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
|
||||
|
||||
def get_top_paths(self, limit: int = 10) -> List[Tuple[str, int]]:
|
||||
"""Get top N paths by access count"""
|
||||
return sorted(self.path_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
|
||||
|
||||
def get_top_user_agents(self, limit: int = 10) -> List[Tuple[str, int]]:
|
||||
"""Get top N user agents by access count"""
|
||||
return sorted(self.user_agent_counts.items(), key=lambda x: x[1], reverse=True)[:limit]
|
||||
|
||||
def get_suspicious_accesses(self, limit: int = 20) -> List[Dict]:
|
||||
"""Get recent suspicious accesses"""
|
||||
suspicious = [log for log in self.access_log if log.get('suspicious', False)]
|
||||
return suspicious[-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()]
|
||||
|
||||
def get_stats(self) -> Dict:
|
||||
"""Get statistics summary"""
|
||||
suspicious_count = sum(1 for log in self.access_log if log.get('suspicious', False))
|
||||
honeypot_count = sum(1 for log in self.access_log if log.get('honeypot_triggered', False))
|
||||
return {
|
||||
'total_accesses': len(self.access_log),
|
||||
'unique_ips': len(self.ip_counts),
|
||||
'unique_paths': len(self.path_counts),
|
||||
'suspicious_accesses': suspicious_count,
|
||||
'honeypot_triggered': honeypot_count,
|
||||
'honeypot_ips': len(self.honeypot_triggered),
|
||||
'top_ips': self.get_top_ips(10),
|
||||
'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()
|
||||
}
|
||||
123
src/wordlists.py
Normal file
123
src/wordlists.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Wordlists loader - reads all wordlists from wordlists.json
|
||||
This allows easy customization without touching Python code.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Wordlists:
|
||||
"""Loads and provides access to wordlists from wordlists.json"""
|
||||
|
||||
def __init__(self):
|
||||
self._data = self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
"""Load wordlists from JSON file"""
|
||||
config_path = Path(__file__).parent.parent / 'wordlists.json'
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"⚠️ Warning: {config_path} not found, using default values")
|
||||
return self._get_defaults()
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"⚠️ Warning: Invalid JSON in {config_path}: {e}")
|
||||
return self._get_defaults()
|
||||
|
||||
def _get_defaults(self):
|
||||
"""Fallback default wordlists if JSON file is missing or invalid"""
|
||||
return {
|
||||
"usernames": {
|
||||
"prefixes": ["admin", "user", "root"],
|
||||
"suffixes": ["", "_prod", "_dev"]
|
||||
},
|
||||
"passwords": {
|
||||
"prefixes": ["P@ssw0rd", "Admin"],
|
||||
"simple": ["test", "demo", "password"]
|
||||
},
|
||||
"emails": {
|
||||
"domains": ["example.com", "test.com"]
|
||||
},
|
||||
"api_keys": {
|
||||
"prefixes": ["sk_live_", "api_", ""]
|
||||
},
|
||||
"databases": {
|
||||
"names": ["production", "main_db"],
|
||||
"hosts": ["localhost", "db.internal"]
|
||||
},
|
||||
"applications": {
|
||||
"names": ["WebApp", "Dashboard"]
|
||||
},
|
||||
"users": {
|
||||
"roles": ["Administrator", "User"]
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def username_prefixes(self):
|
||||
return self._data.get("usernames", {}).get("prefixes", [])
|
||||
|
||||
@property
|
||||
def username_suffixes(self):
|
||||
return self._data.get("usernames", {}).get("suffixes", [])
|
||||
|
||||
@property
|
||||
def password_prefixes(self):
|
||||
return self._data.get("passwords", {}).get("prefixes", [])
|
||||
|
||||
@property
|
||||
def simple_passwords(self):
|
||||
return self._data.get("passwords", {}).get("simple", [])
|
||||
|
||||
@property
|
||||
def email_domains(self):
|
||||
return self._data.get("emails", {}).get("domains", [])
|
||||
|
||||
@property
|
||||
def api_key_prefixes(self):
|
||||
return self._data.get("api_keys", {}).get("prefixes", [])
|
||||
|
||||
@property
|
||||
def database_names(self):
|
||||
return self._data.get("databases", {}).get("names", [])
|
||||
|
||||
@property
|
||||
def database_hosts(self):
|
||||
return self._data.get("databases", {}).get("hosts", [])
|
||||
|
||||
@property
|
||||
def application_names(self):
|
||||
return self._data.get("applications", {}).get("names", [])
|
||||
|
||||
@property
|
||||
def user_roles(self):
|
||||
return self._data.get("users", {}).get("roles", [])
|
||||
|
||||
@property
|
||||
def directory_files(self):
|
||||
return self._data.get("directory_listing", {}).get("files", [])
|
||||
|
||||
@property
|
||||
def directory_dirs(self):
|
||||
return self._data.get("directory_listing", {}).get("directories", [])
|
||||
|
||||
@property
|
||||
def error_codes(self):
|
||||
return self._data.get("error_codes", [])
|
||||
|
||||
|
||||
_wordlists_instance = None
|
||||
|
||||
def get_wordlists():
|
||||
"""Get the singleton Wordlists instance"""
|
||||
global _wordlists_instance
|
||||
if _wordlists_instance is None:
|
||||
_wordlists_instance = Wordlists()
|
||||
return _wordlists_instance
|
||||
|
||||
Reference in New Issue
Block a user