Merge pull request #27 from BlessedRebuS/chore/fix-merge-conflicts

Sync Main and Dev with All Feature Branches
This commit is contained in:
Patrick Di Fazio
2026-01-04 17:10:57 +01:00
committed by GitHub
30 changed files with 1183 additions and 208 deletions

5
.gitignore vendored
View File

@@ -61,9 +61,12 @@ secrets/
*.log *.log
logs/ logs/
# Database # Data and databases
data/ data/
**/data/
*.db *.db
*.sqlite
*.sqlite3
# Temporary files # Temporary files
*.tmp *.tmp

View File

@@ -4,16 +4,25 @@ LABEL org.opencontainers.image.source=https://github.com/BlessedRebuS/Krawl
WORKDIR /app WORKDIR /app
# Install gosu for dropping privileges
RUN apt-get update && apt-get install -y --no-install-recommends gosu && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ /app/src/ COPY src/ /app/src/
COPY wordlists.json /app/ COPY wordlists.json /app/
COPY entrypoint.sh /app/
RUN useradd -m -u 1000 krawl && \ RUN useradd -m -u 1000 krawl && \
chown -R krawl:krawl /app mkdir -p /app/logs /app/data && \
chown -R krawl:krawl /app && \
USER krawl chmod +x /app/entrypoint.sh
EXPOSE 5000 EXPOSE 5000
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["python3", "src/server.py"] CMD ["python3", "src/server.py"]

View File

@@ -48,10 +48,11 @@
<br> <br>
</div> </div>
## Star History ## Demo
<img src="https://api.star-history.com/svg?repos=BlessedRebuS/Krawl&type=Date" width="600" alt="Star History Chart" /> Tip: crawl the `robots.txt` paths for additional fun
### Krawl URL: [http://demo.krawlme.com](http://demo.krawlme.com)
### View the dashboard [http://demo.krawlme.com/das_dashboard](http://demo.krawlme.com/das_dashboard)
## What is Krawl? ## What is Krawl?
**Krawl** is a cloudnative deception server designed to detect, delay, and analyze malicious web crawlers and automated scanners. **Krawl** is a cloudnative deception server designed to detect, delay, and analyze malicious web crawlers and automated scanners.
@@ -185,7 +186,7 @@ To customize the deception server installation several **environment variables**
| `CANARY_TOKEN_URL` | External canary token URL | None | | `CANARY_TOKEN_URL` | External canary token URL | None |
| `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 | `Apache/2.2.22 (Ubuntu)` |
| `TIMEZONE` | IANA timezone for logs and dashboard (e.g., `America/New_York`, `Europe/Rome`) | System timezone | | `TIMEZONE` | IANA timezone for logs and dashboard (e.g., `America/New_York`, `Europe/Rome`) | System timezone |
## robots.txt ## robots.txt
@@ -317,3 +318,6 @@ Contributions welcome! Please:
**This is a deception/honeypot system.** **This is a deception/honeypot system.**
Deploy in isolated environments and monitor carefully for security events. Deploy in isolated environments and monitor carefully for security events.
Use responsibly and in compliance with applicable laws and regulations. Use responsibly and in compliance with applicable laws and regulations.
## Star History
<img src="https://api.star-history.com/svg?repos=BlessedRebuS/Krawl&type=Date" width="600" alt="Star History Chart" />

38
config.yaml Normal file
View File

@@ -0,0 +1,38 @@
# Krawl Honeypot Configuration
server:
port: 5000
delay: 100 # Response delay in milliseconds
timezone: null # e.g., "America/New_York" or null for system default
# manually set the server header, if null a random one will be used.
server_header: "Apache/2.2.22 (Ubuntu)"
links:
min_length: 5
max_length: 15
min_per_page: 10
max_per_page: 15
char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
max_counter: 10
canary:
token_url: null # Optional canary token URL
token_tries: 10
dashboard:
# if set to "null" this will Auto-generates random path if not set
# can be set to "/dashboard" or similar <-- note this MUST include a forward slash
secret_path: dashboard
api:
server_url: null
server_port: 8080
server_path: "/api/v2/users"
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0 # 0-100 percentage

View File

@@ -10,23 +10,10 @@ services:
- "5000:5000" - "5000:5000"
volumes: volumes:
- ./wordlists.json:/app/wordlists.json:ro - ./wordlists.json:/app/wordlists.json:ro
- ./config.yaml:/app/config.yaml:ro
- ./logs:/app/logs
environment: environment:
- PORT=5000 - CONFIG_LOCATION=config.yaml
- DELAY=100
- LINKS_MIN_LENGTH=5
- LINKS_MAX_LENGTH=15
- LINKS_MIN_PER_PAGE=10
- LINKS_MAX_PER_PAGE=15
- MAX_COUNTER=10
- CANARY_TOKEN_TRIES=10
- PROBABILITY_ERROR_CODES=0
# - SERVER_HEADER=Apache/2.2.22 (Ubuntu)
# Optional: Set your canary token URL
# - CANARY_TOKEN_URL=http://canarytokens.com/api/users/YOUR_TOKEN/passwords.txt
# Optional: Set custom dashboard path (auto-generated if not set)
# - 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')"]

8
entrypoint.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Fix ownership of mounted directories
chown -R krawl:krawl /app/logs /app/data 2>/dev/null || true
# Drop to krawl user and run the application
exec gosu krawl "$@"

View File

@@ -5,25 +5,30 @@ metadata:
labels: labels:
{{- include "krawl.labels" . | nindent 4 }} {{- include "krawl.labels" . | nindent 4 }}
data: data:
PORT: {{ .Values.config.port | quote }} config.yaml: |
DELAY: {{ .Values.config.delay | quote }} # Krawl Honeypot Configuration
LINKS_MIN_LENGTH: {{ .Values.config.linksMinLength | quote }} server:
LINKS_MAX_LENGTH: {{ .Values.config.linksMaxLength | quote }} port: {{ .Values.config.server.port }}
LINKS_MIN_PER_PAGE: {{ .Values.config.linksMinPerPage | quote }} delay: {{ .Values.config.server.delay }}
LINKS_MAX_PER_PAGE: {{ .Values.config.linksMaxPerPage | quote }} timezone: {{ .Values.config.server.timezone | toYaml }}
MAX_COUNTER: {{ .Values.config.maxCounter | quote }} links:
CANARY_TOKEN_TRIES: {{ .Values.config.canaryTokenTries | quote }} min_length: {{ .Values.config.links.min_length }}
PROBABILITY_ERROR_CODES: {{ .Values.config.probabilityErrorCodes | quote }} max_length: {{ .Values.config.links.max_length }}
CANARY_TOKEN_URL: {{ .Values.config.canaryTokenUrl | quote }} min_per_page: {{ .Values.config.links.min_per_page }}
{{- if .Values.config.dashboardSecretPath }} max_per_page: {{ .Values.config.links.max_per_page }}
DASHBOARD_SECRET_PATH: {{ .Values.config.dashboardSecretPath | quote }} char_space: {{ .Values.config.links.char_space | quote }}
{{- end }} max_counter: {{ .Values.config.links.max_counter }}
{{- if .Values.config.serverHeader }} canary:
SERVER_HEADER: {{ .Values.config.serverHeader | quote }} token_url: {{ .Values.config.canary.token_url | toYaml }}
{{- end }} token_tries: {{ .Values.config.canary.token_tries }}
{{- if .Values.config.timezone }} dashboard:
TIMEZONE: {{ .Values.config.timezone | quote }} secret_path: {{ .Values.config.dashboard.secret_path | toYaml }}
{{- end }} api:
# Database configuration server_url: {{ .Values.config.api.server_url | toYaml }}
DATABASE_PATH: {{ .Values.database.path | quote }} server_port: {{ .Values.config.api.server_port }}
DATABASE_RETENTION_DAYS: {{ .Values.database.retentionDays | quote }} server_path: {{ .Values.config.api.server_path | quote }}
database:
path: {{ .Values.config.database.path | quote }}
retention_days: {{ .Values.config.database.retention_days }}
behavior:
probability_error_codes: {{ .Values.config.behavior.probability_error_codes }}

View File

@@ -38,18 +38,16 @@ spec:
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: ports:
- name: http - name: http
containerPort: {{ .Values.config.port }} containerPort: {{ .Values.config.server.port }}
protocol: TCP protocol: TCP
envFrom:
- configMapRef:
name: {{ include "krawl.fullname" . }}-config
env: env:
- name: DASHBOARD_SECRET_PATH - name: CONFIG_LOCATION
valueFrom: value: "config.yaml"
secretKeyRef:
name: {{ include "krawl.fullname" . }}
key: dashboard-path
volumeMounts: volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
- name: wordlists - name: wordlists
mountPath: /app/wordlists.json mountPath: /app/wordlists.json
subPath: wordlists.json subPath: wordlists.json
@@ -63,6 +61,9 @@ spec:
{{- toYaml . | nindent 12 }} {{- toYaml . | nindent 12 }}
{{- end }} {{- end }}
volumes: volumes:
- name: config
configMap:
name: {{ include "krawl.fullname" . }}-config
- name: wordlists - name: wordlists
configMap: configMap:
name: {{ include "krawl.fullname" . }}-wordlists name: {{ include "krawl.fullname" . }}-wordlists

View File

@@ -1,16 +0,0 @@
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "krawl.fullname" .)) -}}
{{- $dashboardPath := "" -}}
{{- if and $secret $secret.data -}}
{{- $dashboardPath = index $secret.data "dashboard-path" | b64dec -}}
{{- else -}}
{{- $dashboardPath = printf "/%s" (randAlphaNum 32) -}}
{{- end -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "krawl.fullname" . }}
labels:
{{- include "krawl.labels" . | nindent 4 }}
type: Opaque
stringData:
dashboard-path: {{ $dashboardPath | quote }}

View File

@@ -62,29 +62,36 @@ tolerations: []
affinity: {} affinity: {}
# Application configuration # Application configuration (config.yaml structure)
config: config:
port: 5000 server:
delay: 100 port: 5000
linksMinLength: 5 delay: 100
linksMaxLength: 15 timezone: null # IANA timezone (e.g., "America/New_York", "Europe/Rome"). If not set, system timezone is used.
linksMinPerPage: 10 links:
linksMaxPerPage: 15 min_length: 5
maxCounter: 10 max_length: 15
canaryTokenTries: 10 min_per_page: 10
probabilityErrorCodes: 0 max_per_page: 15
# timezone: "UTC" char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
# serverHeader: "Apache/2.2.22 (Ubuntu)" max_counter: 10
# dashboardSecretPath: "/my-secret-dashboard" canary:
# canaryTokenUrl: set-your-canary-token-url-here token_url: null # Set your canary token URL here
# timezone: "UTC" # IANA timezone (e.g., "America/New_York", "Europe/Rome"). If not set, system timezone is used. token_tries: 10
dashboard:
secret_path: null # Auto-generated if not set, or set to "/my-secret-dashboard"
api:
server_url: null
server_port: 8080
server_path: "/api/v2/users"
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0
# Database configuration # Database persistence configuration
database: database:
# Path to the SQLite database file
path: "data/krawl.db"
# Number of days to retain access logs and attack data
retentionDays: 30
# Persistence configuration # Persistence configuration
persistence: persistence:
enabled: true enabled: true

View File

@@ -10,19 +10,41 @@ metadata:
name: krawl-config name: krawl-config
namespace: krawl-system namespace: krawl-system
data: data:
PORT: "5000" config.yaml: |
DELAY: "100" # Krawl Honeypot Configuration
LINKS_MIN_LENGTH: "5" server:
LINKS_MAX_LENGTH: "15" port: 5000
LINKS_MIN_PER_PAGE: "10" delay: 100
LINKS_MAX_PER_PAGE: "15" timezone: null # e.g., "America/New_York" or null for system default
MAX_COUNTER: "10"
CANARY_TOKEN_TRIES: "10" links:
PROBABILITY_ERROR_CODES: "0" min_length: 5
# CANARY_TOKEN_URL: set-your-canary-token-url-here max_length: 15
# Database configuration min_per_page: 10
DATABASE_PATH: "data/krawl.db" max_per_page: 15
DATABASE_RETENTION_DAYS: "30" char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
max_counter: 10
canary:
token_url: null # Optional canary token URL
token_tries: 10
dashboard:
# Auto-generates random path if null
# Can be set to "/dashboard" or similar
secret_path: null
api:
server_url: null
server_port: 8080
server_path: "/api/v2/users"
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0 # 0-100 percentage
--- ---
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
@@ -227,6 +249,14 @@ data:
500, 500,
502, 502,
503 503
],
"server_headers": [
"Apache/2.4.41 (Ubuntu)",
"nginx/1.18.0",
"Microsoft-IIS/10.0",
"cloudflare",
"AmazonS3",
"gunicorn/20.1.0"
] ]
} }
--- ---
@@ -269,10 +299,14 @@ spec:
- containerPort: 5000 - containerPort: 5000
name: http name: http
protocol: TCP protocol: TCP
envFrom: env:
- configMapRef: - name: CONFIG_LOCATION
name: krawl-config value: "config.yaml"
volumeMounts: volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
- name: wordlists - name: wordlists
mountPath: /app/wordlists.json mountPath: /app/wordlists.json
subPath: wordlists.json subPath: wordlists.json
@@ -287,6 +321,9 @@ spec:
memory: "256Mi" memory: "256Mi"
cpu: "500m" cpu: "500m"
volumes: volumes:
- name: config
configMap:
name: krawl-config
- name: wordlists - name: wordlists
configMap: configMap:
name: krawl-wordlists name: krawl-wordlists
@@ -353,7 +390,7 @@ spec:
- podSelector: {} - podSelector: {}
- namespaceSelector: {} - namespaceSelector: {}
- ipBlock: - ipBlock:
cidr: 0.0.0.0/0 cidr: 0.0.0.0/0
ports: ports:
- protocol: TCP - protocol: TCP
port: 5000 port: 5000

View File

@@ -4,18 +4,38 @@ metadata:
name: krawl-config name: krawl-config
namespace: krawl-system namespace: krawl-system
data: data:
PORT: "5000" config.yaml: |
DELAY: "100" # Krawl Honeypot Configuration
LINKS_MIN_LENGTH: "5" server:
LINKS_MAX_LENGTH: "15" port: 5000
LINKS_MIN_PER_PAGE: "10" delay: 100
LINKS_MAX_PER_PAGE: "15" timezone: null # e.g., "America/New_York" or null for system default
MAX_COUNTER: "10"
CANARY_TOKEN_TRIES: "10" links:
PROBABILITY_ERROR_CODES: "0" min_length: 5
SERVER_HEADER: "Apache/2.2.22 (Ubuntu)" max_length: 15
# CANARY_TOKEN_URL: set-your-canary-token-url-here min_per_page: 10
# TIMEZONE: "UTC" # IANA timezone (e.g., "America/New_York", "Europe/Rome") max_per_page: 15
# Database configuration char_space: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
DATABASE_PATH: "data/krawl.db" max_counter: 10
DATABASE_RETENTION_DAYS: "30"
canary:
token_url: null # Optional canary token URL
token_tries: 10
dashboard:
# Auto-generates random path if null
# Can be set to "/dashboard" or similar
secret_path: null
api:
server_url: null
server_port: 8080
server_path: "/api/v2/users"
database:
path: "data/krawl.db"
retention_days: 30
behavior:
probability_error_codes: 0 # 0-100 percentage

View File

@@ -23,10 +23,14 @@ spec:
- containerPort: 5000 - containerPort: 5000
name: http name: http
protocol: TCP protocol: TCP
envFrom: env:
- configMapRef: - name: CONFIG_LOCATION
name: krawl-config value: "config.yaml"
volumeMounts: volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
- name: wordlists - name: wordlists
mountPath: /app/wordlists.json mountPath: /app/wordlists.json
subPath: wordlists.json subPath: wordlists.json
@@ -41,6 +45,9 @@ spec:
memory: "256Mi" memory: "256Mi"
cpu: "500m" cpu: "500m"
volumes: volumes:
- name: config
configMap:
name: krawl-config
- name: wordlists - name: wordlists
configMap: configMap:
name: krawl-wordlists name: krawl-wordlists

View File

@@ -1,5 +1,8 @@
# Krawl Honeypot Dependencies # Krawl Honeypot Dependencies
# Install with: pip install -r requirements.txt # Install with: pip install -r requirements.txt
# Configuration
PyYAML>=6.0
# Database ORM # Database ORM
SQLAlchemy>=2.0.0,<3.0.0 SQLAlchemy>=2.0.0,<3.0.0

View File

@@ -1,17 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple from typing import Optional, Tuple
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import time import time
import yaml
@dataclass @dataclass
class Config: class Config:
"""Configuration class for the deception server""" """Configuration class for the deception server"""
port: int = 5000 port: int = 5000
delay: int = 100 # milliseconds delay: int = 100 # milliseconds
server_header: str = ""
links_length_range: Tuple[int, int] = (5, 15) links_length_range: Tuple[int, int] = (5, 15)
links_per_page_range: Tuple[int, int] = (10, 15) links_per_page_range: Tuple[int, int] = (10, 15)
char_space: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' char_space: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
@@ -23,12 +28,12 @@ class Config:
api_server_port: int = 8080 api_server_port: int = 8080
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
# Database settings # Database settings
database_path: str = "data/krawl.db" database_path: str = "data/krawl.db"
database_retention_days: int = 30 database_retention_days: int = 30
timezone: str = None # IANA timezone (e.g., 'America/New_York', 'Europe/Rome') timezone: str = None # IANA timezone (e.g., 'America/New_York', 'Europe/Rome')
@staticmethod @staticmethod
# Try to fetch timezone before if not set # Try to fetch timezone before if not set
def get_system_timezone() -> str: def get_system_timezone() -> str:
@@ -38,16 +43,16 @@ class Config:
tz_path = os.readlink('/etc/localtime') tz_path = os.readlink('/etc/localtime')
if 'zoneinfo/' in tz_path: if 'zoneinfo/' in tz_path:
return tz_path.split('zoneinfo/')[-1] return tz_path.split('zoneinfo/')[-1]
local_tz = time.tzname[time.daylight] local_tz = time.tzname[time.daylight]
if local_tz and local_tz != 'UTC': if local_tz and local_tz != 'UTC':
return local_tz return local_tz
except Exception: except Exception:
pass pass
# Default fallback to UTC # Default fallback to UTC
return 'UTC' return 'UTC'
def get_timezone(self) -> ZoneInfo: def get_timezone(self) -> ZoneInfo:
"""Get configured timezone as ZoneInfo object""" """Get configured timezone as ZoneInfo object"""
if self.timezone: if self.timezone:
@@ -55,7 +60,7 @@ class Config:
return ZoneInfo(self.timezone) return ZoneInfo(self.timezone)
except Exception: except Exception:
pass pass
system_tz = self.get_system_timezone() system_tz = self.get_system_timezone()
try: try:
return ZoneInfo(system_tz) return ZoneInfo(system_tz)
@@ -63,31 +68,76 @@ class Config:
return ZoneInfo('UTC') return ZoneInfo('UTC')
@classmethod @classmethod
def from_env(cls) -> 'Config': def from_yaml(cls) -> 'Config':
"""Create configuration from environment variables""" """Create configuration from YAML file"""
config_location = os.getenv('CONFIG_LOCATION', 'config.yaml')
config_path = Path(__file__).parent.parent / config_location
try:
with open(config_path, 'r') as f:
data = yaml.safe_load(f)
except FileNotFoundError:
print(f"Error: Configuration file '{config_path}' not found.", file=sys.stderr)
print(f"Please create a config.yaml file or set CONFIG_LOCATION environment variable.", file=sys.stderr)
sys.exit(1)
except yaml.YAMLError as e:
print(f"Error: Invalid YAML in configuration file '{config_path}': {e}", file=sys.stderr)
sys.exit(1)
if data is None:
data = {}
# Extract nested values with defaults
server = data.get('server', {})
links = data.get('links', {})
canary = data.get('canary', {})
dashboard = data.get('dashboard', {})
api = data.get('api', {})
database = data.get('database', {})
behavior = data.get('behavior', {})
# Handle dashboard_secret_path - auto-generate if null/not set
dashboard_path = dashboard.get('secret_path')
if dashboard_path is None:
dashboard_path = f'/{os.urandom(16).hex()}'
else:
# ensure the dashboard path starts with a /
if dashboard_path[:1] != "/":
dashboard_path = f"/{dashboard_path}"
return cls( return cls(
port=int(os.getenv('PORT', 5000)), port=server.get('port', 5000),
delay=int(os.getenv('DELAY', 100)), delay=server.get('delay', 100),
server_header=server.get('server_header',""),
timezone=server.get('timezone'),
links_length_range=( links_length_range=(
int(os.getenv('LINKS_MIN_LENGTH', 5)), links.get('min_length', 5),
int(os.getenv('LINKS_MAX_LENGTH', 15)) links.get('max_length', 15)
), ),
links_per_page_range=( links_per_page_range=(
int(os.getenv('LINKS_MIN_PER_PAGE', 10)), links.get('min_per_page', 10),
int(os.getenv('LINKS_MAX_PER_PAGE', 15)) links.get('max_per_page', 15)
), ),
char_space=os.getenv('CHAR_SPACE', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), char_space=links.get('char_space', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'),
max_counter=int(os.getenv('MAX_COUNTER', 10)), max_counter=links.get('max_counter', 10),
canary_token_url=os.getenv('CANARY_TOKEN_URL'), canary_token_url=canary.get('token_url'),
canary_token_tries=int(os.getenv('CANARY_TOKEN_TRIES', 10)), canary_token_tries=canary.get('token_tries', 10),
dashboard_secret_path=os.getenv('DASHBOARD_SECRET_PATH', f'/{os.urandom(16).hex()}'), dashboard_secret_path=dashboard_path,
api_server_url=os.getenv('API_SERVER_URL'), api_server_url=api.get('server_url'),
api_server_port=int(os.getenv('API_SERVER_PORT', 8080)), api_server_port=api.get('server_port', 8080),
api_server_path=os.getenv('API_SERVER_PATH', '/api/v2/users'), api_server_path=api.get('server_path', '/api/v2/users'),
probability_error_codes=int(os.getenv('PROBABILITY_ERROR_CODES', 0)), probability_error_codes=behavior.get('probability_error_codes', 0),
server_header=os.getenv('SERVER_HEADER'), database_path=database.get('path', 'data/krawl.db'),
database_path=os.getenv('DATABASE_PATH', 'data/krawl.db'), database_retention_days=database.get('retention_days', 30),
database_retention_days=int(os.getenv('DATABASE_RETENTION_DAYS', 30)),
timezone=os.getenv('TIMEZONE') # If not set, will use system timezone
) )
_config_instance = None
def get_config() -> Config:
"""Get the singleton Config instance"""
global _config_instance
if _config_instance is None:
_config_instance = Config.from_yaml()
return _config_instance

View File

@@ -9,8 +9,7 @@ import string
import json import json
from templates import html_templates from templates import html_templates
from wordlists import get_wordlists from wordlists import get_wordlists
from config import Config from config import get_config
from logger import get_app_logger
def random_username() -> str: def random_username() -> str:
"""Generate random username""" """Generate random username"""
@@ -38,15 +37,12 @@ def random_email(username: str = None) -> str:
return f"{username}@{random.choice(wl.email_domains)}" return f"{username}@{random.choice(wl.email_domains)}"
def random_server_header() -> str: def random_server_header() -> str:
"""Generate random server header""" """Generate random server header from wordlists"""
config = get_config()
if Config.from_env().server_header: if config.server_header:
server_header = Config.from_env().server_header return config.server_header
else: wl = get_wordlists()
wl = get_wordlists() return random.choice(wl.server_headers)
server_header = random.choice(wl.server_headers)
return server_header
def random_api_key() -> str: def random_api_key() -> str:
"""Generate random API key""" """Generate random API key"""

View File

@@ -6,6 +6,7 @@ import time
from datetime import datetime from datetime import datetime
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from typing import Optional, List from typing import Optional, List
from urllib.parse import urlparse, parse_qs
from config import Config from config import Config
from tracker import AccessTracker from tracker import AccessTracker
@@ -16,6 +17,9 @@ from generators import (
api_response, directory_listing, random_server_header api_response, directory_listing, random_server_header
) )
from wordlists import get_wordlists from wordlists import get_wordlists
from sql_errors import generate_sql_error_response, get_sql_response_with_data
from xss_detector import detect_xss_pattern, generate_xss_response
from server_errors import generate_server_error
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
@@ -67,6 +71,67 @@ class Handler(BaseHTTPRequestHandler):
if not error_codes: if not error_codes:
error_codes = [400, 401, 403, 404, 500, 502, 503] error_codes = [400, 401, 403, 404, 500, 502, 503]
return random.choice(error_codes) return random.choice(error_codes)
def _parse_query_string(self) -> 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: def generate_page(self, seed: str) -> str:
"""Generate a webpage containing random links or canary token""" """Generate a webpage containing random links or canary token"""
@@ -207,6 +272,68 @@ class Handler(BaseHTTPRequestHandler):
user_agent = self._get_user_agent() user_agent = self._get_user_agent()
post_data = "" 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]}") self.access_logger.warning(f"[LOGIN ATTEMPT] {client_ip} - {self.path} - {user_agent[:50]}")
content_length = int(self.headers.get('Content-Length', 0)) content_length = int(self.headers.get('Content-Length', 0))
@@ -248,6 +375,10 @@ class Handler(BaseHTTPRequestHandler):
def serve_special_path(self, path: str) -> bool: def serve_special_path(self, path: str) -> bool:
"""Serve special paths like robots.txt, API endpoints, etc.""" """Serve special paths like robots.txt, API endpoints, etc."""
# Check SQL injection honeypot endpoints first
if self._handle_sql_endpoint(path):
return True
try: try:
if path == '/robots.txt': if path == '/robots.txt':
self.send_response(200) self.send_response(200)
@@ -285,7 +416,28 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(html_templates.login_form().encode()) self.wfile.write(html_templates.login_form().encode())
return True 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/']: if path in ['/wp-login.php', '/wp-login', '/wp-admin', '/wp-admin/']:
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/html') self.send_header('Content-type', 'text/html')

View File

@@ -8,7 +8,7 @@ Run this file to start the server.
import sys import sys
from http.server import HTTPServer from http.server import HTTPServer
from config import Config from config import get_config
from tracker import AccessTracker from tracker import AccessTracker
from handler import Handler from handler import Handler
from logger import initialize_logging, get_app_logger, get_access_logger, get_credential_logger from logger import initialize_logging, get_app_logger, get_access_logger, get_credential_logger
@@ -20,24 +20,29 @@ def print_usage():
print(f'Usage: {sys.argv[0]} [FILE]\n') print(f'Usage: {sys.argv[0]} [FILE]\n')
print('FILE is file containing a list of webpage names to serve, one per line.') 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('If no file is provided, random links will be generated.\n')
print('Environment Variables:') print('Configuration:')
print(' PORT - Server port (default: 5000)') print(' Configuration is loaded from a YAML file (default: config.yaml)')
print(' DELAY - Response delay in ms (default: 100)') print(' Set CONFIG_LOCATION environment variable to use a different file.\n')
print(' LINKS_MIN_LENGTH - Min link length (default: 5)') print(' Example config.yaml structure:')
print(' LINKS_MAX_LENGTH - Max link length (default: 15)') print(' server:')
print(' LINKS_MIN_PER_PAGE - Min links per page (default: 10)') print(' port: 5000')
print(' LINKS_MAX_PER_PAGE - Max links per page (default: 15)') print(' delay: 100')
print(' MAX_COUNTER - Max counter value (default: 10)') print(' timezone: null # or "America/New_York"')
print(' CANARY_TOKEN_URL - Canary token URL to display') print(' links:')
print(' CANARY_TOKEN_TRIES - Number of tries before showing token (default: 10)') print(' min_length: 5')
print(' DASHBOARD_SECRET_PATH - Secret path for dashboard (auto-generated if not set)') print(' max_length: 15')
print(' PROBABILITY_ERROR_CODES - Probability (0-100) to return HTTP error codes (default: 0)') print(' min_per_page: 10')
print(' CHAR_SPACE - Characters for random links') print(' max_per_page: 15')
print(' SERVER_HEADER - HTTP Server header for deception (default: Apache/2.2.22 (Ubuntu))') print(' canary:')
print(' DATABASE_PATH - Path to SQLite database (default: data/krawl.db)') print(' token_url: null')
print(' DATABASE_RETENTION_DAYS - Days to retain database records (default: 30)') print(' token_tries: 10')
print(' TIMEZONE - IANA timezone for logs/dashboard (e.g., America/New_York, Europe/Rome)') print(' dashboard:')
print(' If not set, system timezone will be used') print(' secret_path: null # auto-generated if not set')
print(' database:')
print(' path: "data/krawl.db"')
print(' retention_days: 30')
print(' behavior:')
print(' probability_error_codes: 0')
def main(): def main():
@@ -46,19 +51,17 @@ def main():
print_usage() print_usage()
exit(0) exit(0)
config = Config.from_env() config = get_config()
# Get timezone configuration # Get timezone configuration
tz = config.get_timezone() tz = config.get_timezone()
# Initialize logging with timezone # Initialize logging with timezone
initialize_logging(timezone=tz) 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 database for persistent storage # Initialize database for persistent storage
try: try:
initialize_database(config.database_path) initialize_database(config.database_path)

65
src/server_errors.py Normal file
View File

@@ -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")

112
src/sql_errors.py Normal file
View File

@@ -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)

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<title>Search</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
h1 {
color: #333;
}
input {
width: 100%;
padding: 8px;
margin: 10px 0;
box-sizing: border-box;
}
button {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
cursor: pointer;
}
button:hover {
background: #45a049;
}
#results {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
background: #f9f9f9;
display: none;
}
</style>
</head>
<body>
<h1>Search</h1>
<form id="searchForm">
<input type="text" id="searchQuery" placeholder="Enter search query..." required>
<button type="submit">Search</button>
</form>
<div id="results"></div>
<script>
document.getElementById('searchForm').addEventListener('submit', async (e) => {
e.preventDefault();
const query = document.getElementById('searchQuery').value;
const results = document.getElementById('results');
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const text = await response.text();
results.innerHTML = `<pre>${text}</pre>`;
results.style.display = 'block';
} catch (err) {
results.innerHTML = `<p>Error: ${err.message}</p>`;
results.style.display = 'block';
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<title>Contact</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
}
h1 {
color: #333;
}
input, textarea {
width: 100%;
padding: 8px;
margin: 10px 0;
border: 1px solid #ddd;
box-sizing: border-box;
}
textarea {
min-height: 100px;
}
button {
background: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
cursor: pointer;
}
button:hover {
background: #45a049;
}
#response {
margin-top: 20px;
padding: 10px;
display: none;
}
</style>
</head>
<body>
<h1>Contact</h1>
<form id="contactForm">
<input type="text" name="name" placeholder="Name" required>
<input type="email" name="email" placeholder="Email" required>
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Submit</button>
</form>
<div id="response"></div>
<script>
document.getElementById('contactForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(formData)
})
.then(response => response.text())
.then(text => {
document.getElementById('response').innerHTML = text;
document.getElementById('response').style.display = 'block';
})
.catch(error => {
document.getElementById('response').innerHTML = 'Error: ' + error.message;
document.getElementById('response').style.display = 'block';
});
});
</script>
</body>
</html>

View File

@@ -11,8 +11,18 @@ Disallow: /login/
Disallow: /admin/login Disallow: /admin/login
Disallow: /phpMyAdmin/ Disallow: /phpMyAdmin/
Disallow: /admin/login.php Disallow: /admin/login.php
Disallow: /users
Disallow: /search
Disallow: /contact
Disallow: /info
Disallow: /input
Disallow: /feedback
Disallow: /server
Disallow: /api/v1/users Disallow: /api/v1/users
Disallow: /api/v2/secrets Disallow: /api/v2/secrets
Disallow: /api/search
Disallow: /api/sql
Disallow: /api/database
Disallow: /.env Disallow: /.env
Disallow: /credentials.txt Disallow: /credentials.txt
Disallow: /passwords.txt Disallow: /passwords.txt

View File

@@ -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) 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) 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")

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import re import re
import urllib.parse import urllib.parse
from wordlists import get_wordlists
from database import get_database, DatabaseManager from database import get_database, DatabaseManager
@@ -37,14 +37,19 @@ class AccessTracker:
'burp', 'zap', 'w3af', 'metasploit', 'nuclei', 'gobuster', 'dirbuster' 'burp', 'zap', 'w3af', 'metasploit', 'nuclei', 'gobuster', 'dirbuster'
] ]
# Common attack types such as xss, shell injection, probes # Load attack patterns from wordlists
self.attack_types = { wl = get_wordlists()
'path_traversal': r'\.\.', self.attack_types = wl.attack_patterns
'sql_injection': r"('|--|;|\bOR\b|\bUNION\b|\bSELECT\b|\bDROP\b)",
'xss_attempt': r'(<script|javascript:|onerror=|onload=)', # Fallback if wordlists not loaded
'common_probes': r'(wp-admin|phpmyadmin|\.env|\.git|/admin|/config)', if not self.attack_types:
'shell_injection': r'(\||;|`|\$\(|&&)', self.attack_types = {
} 'path_traversal': r'\.\.',
'sql_injection': r"('|--|;|\bOR\b|\bUNION\b|\bSELECT\b|\bDROP\b)",
'xss_attempt': r'(<script|javascript:|onerror=|onload=)',
'common_probes': r'(wp-admin|phpmyadmin|\.env|\.git|/admin|/config)',
'shell_injection': r'(\||;|`|\$\(|&&)',
}
# Track IPs that accessed honeypot paths from robots.txt # Track IPs that accessed honeypot paths from robots.txt
self.honeypot_triggered: Dict[str, List[str]] = defaultdict(list) self.honeypot_triggered: Dict[str, List[str]] = defaultdict(list)

View File

@@ -114,6 +114,17 @@ class Wordlists:
return self._data.get("error_codes", []) return self._data.get("error_codes", [])
@property @property
def sql_errors(self):
return self._data.get("sql_errors", {})
@property
def attack_patterns(self):
return self._data.get("attack_patterns", {})
@property
def server_errors(self):
return self._data.get("server_errors", {})
def server_headers(self): def server_headers(self):
return self._data.get("server_headers", []) return self._data.get("server_headers", [])

73
src/xss_detector.py Normal file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import re
from typing import Optional
from wordlists import get_wordlists
def detect_xss_pattern(input_string: str) -> 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'(<script|</script|javascript:|onerror=|onload=|onclick=|<iframe|<img|<svg|eval\(|alert\()'
return bool(re.search(xss_pattern, input_string, re.IGNORECASE))
def generate_xss_response(input_data: dict) -> 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"<p><strong>{key}:</strong> {value}</p>")
if xss_detected:
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>Submission Received</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }}
.success {{ background: #d4edda; padding: 20px; border-radius: 8px; border: 1px solid #c3e6cb; }}
h2 {{ color: #155724; }}
p {{ margin: 10px 0; }}
</style>
</head>
<body>
<div class="success">
<h2>Thank you for your submission!</h2>
<p>We have received your information:</p>
{''.join(reflected_content)}
<p><em>We will get back to you shortly.</em></p>
</div>
</body>
</html>
"""
return html
return """
<!DOCTYPE html>
<html>
<head>
<title>Submission Received</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
.success { background: #d4edda; padding: 20px; border-radius: 8px; border: 1px solid #c3e6cb; }
h2 { color: #155724; }
</style>
</head>
<body>
<div class="success">
<h2>Thank you for your submission!</h2>
<p>Your message has been received and we will respond soon.</p>
</div>
</body>
</html>
"""

View File

@@ -17,4 +17,4 @@ curl -s "$TARGET/wp-admin/"
echo -e "\n=== Testing Shell Injection ===" echo -e "\n=== Testing Shell Injection ==="
curl -s -X POST "$TARGET/ping" -d "host=127.0.0.1; cat /etc/passwd" curl -s -X POST "$TARGET/ping" -d "host=127.0.0.1; cat /etc/passwd"
echo -e "\n=== Done ===" echo -e "\n=== Done ==="

View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Test script for SQL injection honeypot endpoints
BASE_URL="http://localhost:5000"
echo "========================================="
echo "Testing SQL Injection Honeypot Endpoints"
echo "========================================="
echo ""
# Test 1: Normal query
echo "Test 1: Normal GET request to /api/search"
curl -s "${BASE_URL}/api/search?q=test" | head -20
echo ""
echo "---"
echo ""
# Test 2: SQL injection with single quote
echo "Test 2: SQL injection with single quote"
curl -s "${BASE_URL}/api/search?id=1'" | head -20
echo ""
echo "---"
echo ""
# Test 3: UNION-based injection
echo "Test 3: UNION-based SQL injection"
curl -s "${BASE_URL}/api/search?id=1%20UNION%20SELECT%20*" | head -20
echo ""
echo "---"
echo ""
# Test 4: Boolean-based injection
echo "Test 4: Boolean-based SQL injection"
curl -s "${BASE_URL}/api/sql?user=admin'%20OR%201=1--" | head -20
echo ""
echo "---"
echo ""
# Test 5: Comment-based injection
echo "Test 5: Comment-based SQL injection"
curl -s "${BASE_URL}/api/database?q=test'--" | head -20
echo ""
echo "---"
echo ""
# Test 6: Time-based injection
echo "Test 6: Time-based SQL injection"
curl -s "${BASE_URL}/api/search?id=1%20AND%20SLEEP(5)" | head -20
echo ""
echo "---"
echo ""
# Test 7: POST request with SQL injection
echo "Test 7: POST request with SQL injection"
curl -s -X POST "${BASE_URL}/api/search" -d "username=admin'%20OR%201=1--&password=test" | head -20
echo ""
echo "---"
echo ""
# Test 8: Information schema query
echo "Test 8: Information schema injection"
curl -s "${BASE_URL}/api/sql?table=information_schema.tables" | head -20
echo ""
echo "---"
echo ""
# Test 9: Stacked queries
echo "Test 9: Stacked queries injection"
curl -s "${BASE_URL}/api/database?id=1;DROP%20TABLE%20users" | head -20
echo ""
echo "---"
echo ""
echo "========================================="
echo "Tests completed!"
echo "Check logs for detailed attack detection"
echo "========================================="

View File

@@ -194,12 +194,169 @@
502, 502,
503 503
], ],
"server_headers": [ "server_errors": {
"Apache/2.4.41 (Ubuntu)", "nginx": {
"nginx/1.18.0", "versions": ["1.18.0", "1.20.1", "1.22.0", "1.24.0"],
"Microsoft-IIS/10.0", "template": "<!DOCTYPE html>\n<html>\n<head>\n<title>{code} {message}</title>\n<style>\nbody {{\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n}}\n</style>\n</head>\n<body>\n<h1>An error occurred.</h1>\n<p>Sorry, the page you are looking for is currently unavailable.<br/>\nPlease try again later.</p>\n<p>If you are the system administrator of this resource then you should check the error log for details.</p>\n<p><em>Faithfully yours, nginx/{version}.</em></p>\n</body>\n</html>"
"cloudflare", },
"AmazonS3", "apache": {
"gunicorn/20.1.0" "versions": ["2.4.41", "2.4.52", "2.4.54", "2.4.57"],
] "os": ["Ubuntu", "Debian", "CentOS"],
"template": "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>{code} {message}</title>\n</head><body>\n<h1>{message}</h1>\n<p>The requested URL was not found on this server.</p>\n<hr>\n<address>Apache/{version} ({os}) Server at {host} Port 80</address>\n</body></html>"
},
"iis": {
"versions": ["10.0", "8.5", "8.0"],
"template": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\"/>\n<title>{code} - {message}</title>\n<style type=\"text/css\">\nbody{{margin:0;font-size:.7em;font-family:Verdana, Arial, Helvetica, sans-serif;background:#EEEEEE;}}\nfieldset{{padding:0 15px 10px 15px;}}\nh1{{font-size:2.4em;margin:0;color:#FFF;}}\nh2{{font-size:1.7em;margin:0;color:#CC0000;}}\nh3{{font-size:1.2em;margin:10px 0 0 0;color:#000000;}}\n#header{{width:96%;margin:0 0 0 0;padding:6px 2% 6px 2%;font-family:\"trebuchet MS\", Verdana, sans-serif;color:#FFF;\nbackground-color:#555555;}}\n#content{{margin:0 0 0 2%;position:relative;}}\n</style>\n</head>\n<body>\n<div id=\"header\"><h1>Server Error</h1></div>\n<div id=\"content\">\n <div class=\"content-container\"><fieldset>\n <h2>{code} - {message}</h2>\n <h3>The page cannot be displayed because an internal server error has occurred.</h3>\n </fieldset></div>\n</div>\n</body>\n</html>"
},
"tomcat": {
"versions": ["9.0.65", "10.0.27", "10.1.5"],
"template": "<!doctype html><html lang=\"en\"><head><title>HTTP Status {code} - {message}</title><style type=\"text/css\">body {{font-family:Tahoma,Arial,sans-serif;}} h1, h2, h3, b {{color:white;background-color:#525D76;}} h1 {{font-size:22px;}} h2 {{font-size:16px;}} h3 {{font-size:14px;}} p {{font-size:12px;}} a {{color:black;}} .line {{height:1px;background-color:#525D76;border:none;}}</style></head><body><h1>HTTP Status {code} - {message}</h1><hr class=\"line\" /><p><b>Type</b> Status Report</p><p><b>Description</b> The server encountered an internal error that prevented it from fulfilling this request.</p><hr class=\"line\" /><h3>Apache Tomcat/{version}</h3></body></html>"
}
},
"sql_errors": {
"mysql": {
"generic": [
"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' at line 1",
"Unknown column '{column}' in 'where clause'",
"Table '{table}' doesn't exist",
"Operand should contain 1 column(s)",
"Subquery returns more than 1 row",
"Duplicate entry 'admin' for key 'PRIMARY'"
],
"quote": [
"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''''' at line 1",
"Unclosed quotation mark after the character string ''",
"You have an error in your SQL syntax near '\\'' LIMIT 0,30'"
],
"union": [
"The used SELECT statements have a different number of columns",
"Operand should contain 1 column(s)",
"Mixing of GROUP columns (MIN(),MAX(),COUNT(),...) with no GROUP columns is illegal"
],
"boolean": [
"You have an error in your SQL syntax near 'OR 1=1' at line 1",
"Unknown column '1' in 'where clause'"
],
"time_based": [
"Query execution was interrupted",
"Lock wait timeout exceeded; try restarting transaction"
],
"comment": [
"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '--' at line 1"
]
},
"postgresql": {
"generic": [
"ERROR: syntax error at or near \"1\"",
"ERROR: column \"{column}\" does not exist",
"ERROR: relation \"{table}\" does not exist",
"ERROR: operator does not exist: integer = text",
"ERROR: invalid input syntax for type integer: \"admin\""
],
"quote": [
"ERROR: unterminated quoted string at or near \"'\"",
"ERROR: syntax error at or near \"'\"",
"ERROR: unterminated quoted identifier at or near \"'\""
],
"union": [
"ERROR: each UNION query must have the same number of columns",
"ERROR: UNION types integer and text cannot be matched"
],
"boolean": [
"ERROR: syntax error at or near \"OR\"",
"ERROR: invalid input syntax for type boolean: \"1=1\""
],
"time_based": [
"ERROR: canceling statement due to user request",
"ERROR: function pg_sleep(integer) does not exist"
],
"info_schema": [
"ERROR: permission denied for table {table}",
"ERROR: permission denied for schema information_schema"
]
},
"mssql": {
"generic": [
"Msg 102, Level 15, State 1, Line 1\nIncorrect syntax near '1'.",
"Msg 207, Level 16, State 1, Line 1\nInvalid column name '{column}'.",
"Msg 208, Level 16, State 1, Line 1\nInvalid object name '{table}'.",
"Msg 245, Level 16, State 1, Line 1\nConversion failed when converting the varchar value 'admin' to data type int."
],
"quote": [
"Msg 105, Level 15, State 1, Line 1\nUnclosed quotation mark after the character string ''.",
"Msg 102, Level 15, State 1, Line 1\nIncorrect syntax near '''."
],
"union": [
"Msg 205, Level 16, State 1, Line 1\nAll queries combined using a UNION, INTERSECT or EXCEPT operator must have an equal number of expressions in their target lists.",
"Msg 8167, Level 16, State 1, Line 1\nThe type of column \"{column}\" conflicts with the type of other columns specified in the UNION, INTERSECT, or EXCEPT list."
],
"boolean": [
"Msg 102, Level 15, State 1, Line 1\nIncorrect syntax near 'OR'."
],
"command": [
"Msg 15281, Level 16, State 1, Procedure xp_cmdshell, Line 1\nSQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell'"
]
},
"oracle": {
"generic": [
"ORA-00933: SQL command not properly ended",
"ORA-00904: \"{column}\": invalid identifier",
"ORA-00942: table or view \"{table}\" does not exist",
"ORA-01722: invalid number",
"ORA-01756: quoted string not properly terminated"
],
"quote": [
"ORA-01756: quoted string not properly terminated",
"ORA-00933: SQL command not properly ended"
],
"union": [
"ORA-01789: query block has incorrect number of result columns",
"ORA-01790: expression must have same datatype as corresponding expression"
],
"boolean": [
"ORA-00933: SQL command not properly ended",
"ORA-00920: invalid relational operator"
]
},
"sqlite": {
"generic": [
"near \"1\": syntax error",
"no such column: {column}",
"no such table: {table}",
"unrecognized token: \"'\"",
"incomplete input"
],
"quote": [
"unrecognized token: \"'\"",
"incomplete input",
"near \"'\": syntax error"
],
"union": [
"SELECTs to the left and right of UNION do not have the same number of result columns"
]
},
"mongodb": {
"generic": [
"MongoError: Can't canonicalize query: BadValue unknown operator: $where",
"MongoError: Failed to parse: { $where: \"this.{column} == '1'\" }",
"SyntaxError: unterminated string literal",
"MongoError: exception: invalid operator: $gt"
],
"quote": [
"SyntaxError: unterminated string literal",
"SyntaxError: missing } after property list"
],
"command": [
"MongoError: $where is not allowed in this context",
"MongoError: can't eval: security"
]
}
},
"attack_patterns": {
"path_traversal": "\\.\\.",
"sql_injection": "('|\"|`|--|#|/\\*|\\*/|\\bunion\\b|\\bunion\\s+select\\b|\\bor\\b.*=.*|\\band\\b.*=.*|'.*or.*'.*=.*'|\\bsleep\\b|\\bwaitfor\\b|\\bdelay\\b|\\bbenchmark\\b|;.*select|;.*drop|;.*insert|;.*update|;.*delete|\\bexec\\b|\\bexecute\\b|\\bxp_cmdshell\\b|information_schema|table_schema|table_name)",
"xss_attempt": "(<script|</script|javascript:|onerror=|onload=|onclick=|onmouseover=|onfocus=|onblur=|<iframe|<img|<svg|<embed|<object|<body|<input|eval\\(|alert\\(|prompt\\(|confirm\\(|document\\.|window\\.|<style|expression\\(|vbscript:|data:text/html)",
"common_probes": "(wp-admin|phpmyadmin|\\.env|\\.git|/admin|/config)",
"shell_injection": "(\\||;|`|\\$\\(|&&)"
}
} }