Merge pull request #27 from BlessedRebuS/chore/fix-merge-conflicts
Sync Main and Dev with All Feature Branches
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -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"]
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -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 cloud‑native deception server designed to detect, delay, and analyze malicious web crawlers and automated scanners.
|
**Krawl** is a cloud‑native 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
38
config.yaml
Normal 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
|
||||||
@@ -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
8
entrypoint.sh
Normal 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 "$@"
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
106
src/config.py
106
src/config.py
@@ -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
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
154
src/handler.py
154
src/handler.py
@@ -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')
|
||||||
|
|||||||
@@ -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
65
src/server_errors.py
Normal 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
112
src/sql_errors.py
Normal 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)
|
||||||
66
src/templates/html/generic_search.html
Normal file
66
src/templates/html/generic_search.html
Normal 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>
|
||||||
74
src/templates/html/input_form.html
Normal file
74
src/templates/html/input_form.html
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
73
src/xss_detector.py
Normal 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>
|
||||||
|
"""
|
||||||
@@ -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 ==="
|
||||||
|
|||||||
78
tests/test_sql_injection.sh
Normal file
78
tests/test_sql_injection.sh
Normal 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 "========================================="
|
||||||
173
wordlists.json
173
wordlists.json
@@ -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": "(\\||;|`|\\$\\(|&&)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user