Add per-service customization, stealth hardening, and BYOS support
- HTTP: configurable server_header, response_code, fake_app presets (apache/nginx/wordpress/phpmyadmin/iis), extra_headers, custom_body, static files directory mount - SSH/Cowrie: configurable kernel_version, hardware_platform, ssh_banner, and users/passwords via COWRIE_USERDB_ENTRIES; switched to build mode so cowrie.cfg.j2 persona fields and userdb.txt generation work - SMTP: configurable banner and MTA hostname - MySQL: configurable version string in protocol greeting - Redis: configurable redis_version and os string in INFO response - BYOS: [custom-*] INI sections define bring-your-own Docker services - Stealth: rename all *_honeypot.py → server.py; replace HONEYPOT_NAME env var with NODE_NAME across all 22+ service templates and plugins; strip "honeypot" from all in-container file content - Config: DeckyConfig.service_config dict; INI [decky-N.svc] subsections; composer passes service_cfg to compose_fragment - 350 tests passing (100%) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__plugin_context-mode_context-mode__ctx_batch_execute"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -46,3 +46,11 @@ DECNET is a honeypot/deception network framework. It deploys fake machines (call
|
||||
- The logging/aggregation network must be isolated from the decoy network.
|
||||
- A publicly accessible real server acts as the bridge between the two networks.
|
||||
- Deckies should differ in exposed services and OS fingerprints to appear as a heterogeneous network.
|
||||
|
||||
## Development and testing
|
||||
|
||||
- For every new feature, pytests must me made.
|
||||
- Pytest is the main testing framework in use.
|
||||
- NEVER pass broken code to the user.
|
||||
- Broken means: not running, not passing 100% tests, etc.
|
||||
- After tests pass with 100%, always git commit your changes.
|
||||
|
||||
@@ -168,6 +168,7 @@ def _build_deckies_from_ini(
|
||||
base_image=distro.image,
|
||||
build_base=distro.build_base,
|
||||
hostname=hostname,
|
||||
service_config=spec.service_config,
|
||||
))
|
||||
return deckies
|
||||
|
||||
@@ -217,6 +218,20 @@ def deploy(
|
||||
f"[dim]Subnet:[/] {subnet_cidr} [dim]Gateway:[/] {effective_gateway} "
|
||||
f"[dim]Host IP:[/] {host_ip}")
|
||||
|
||||
# Register bring-your-own services from INI before validation
|
||||
if ini.custom_services:
|
||||
from decnet.custom_service import CustomService
|
||||
from decnet.services.registry import register_custom_service
|
||||
for cs in ini.custom_services:
|
||||
register_custom_service(
|
||||
CustomService(
|
||||
name=cs.name,
|
||||
image=cs.image,
|
||||
exec_cmd=cs.exec_cmd,
|
||||
ports=cs.ports,
|
||||
)
|
||||
)
|
||||
|
||||
effective_log_target = log_target or ini.log_target
|
||||
decky_configs = _build_deckies_from_ini(
|
||||
ini, subnet_cidr, effective_gateway, host_ip, randomize_services
|
||||
|
||||
@@ -46,7 +46,10 @@ def generate_compose(config: DecnetConfig) -> dict:
|
||||
# --- Service containers: share base network namespace ---
|
||||
for svc_name in decky.services:
|
||||
svc = get_service(svc_name)
|
||||
fragment = svc.compose_fragment(decky.name, log_target=config.log_target)
|
||||
svc_cfg = decky.service_config.get(svc_name, {})
|
||||
fragment = svc.compose_fragment(
|
||||
decky.name, log_target=config.log_target, service_cfg=svc_cfg
|
||||
)
|
||||
|
||||
# Inject the per-decky base image into build services so containers
|
||||
# vary by distro and don't all fingerprint as debian:bookworm-slim.
|
||||
|
||||
@@ -26,6 +26,7 @@ class DeckyConfig(BaseModel):
|
||||
base_image: str # Docker image for the base/IP-holder container
|
||||
build_base: str = "debian:bookworm-slim" # apt-compatible image for service Dockerfiles
|
||||
hostname: str
|
||||
service_config: dict[str, dict] = {} # optional per-service persona config
|
||||
|
||||
@field_validator("services")
|
||||
@classmethod
|
||||
|
||||
41
decnet/custom_service.py
Normal file
41
decnet/custom_service.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Bring-your-own-service (BYOS) support.
|
||||
|
||||
CustomService wraps a user-defined service from an INI [custom-*] section.
|
||||
It is instantiated dynamically and registered via register_custom_service(),
|
||||
not through the auto-discovery mechanism in the registry.
|
||||
"""
|
||||
|
||||
from decnet.services.base import BaseService
|
||||
|
||||
|
||||
class CustomService(BaseService):
|
||||
"""A user-defined service that runs an arbitrary Docker image."""
|
||||
|
||||
def __init__(self, name: str, image: str, exec_cmd: str, ports: list[int] | None = None):
|
||||
self.name = name
|
||||
self.default_image = image
|
||||
self.ports = ports or []
|
||||
self._exec_cmd = exec_cmd
|
||||
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
slug = self.name.replace("_", "-")
|
||||
fragment: dict = {
|
||||
"image": self.default_image,
|
||||
"container_name": f"{decky_name}-{slug}",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if self._exec_cmd:
|
||||
fragment["command"] = self._exec_cmd.split()
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
return fragment
|
||||
|
||||
def dockerfile_context(self):
|
||||
return None
|
||||
@@ -12,11 +12,26 @@ Format:
|
||||
ip=192.168.1.82 # optional
|
||||
services=ssh,smb # optional; falls back to --randomize-services
|
||||
|
||||
[hostname-1.ssh] # optional per-service persona config
|
||||
kernel_version=5.15.0-76-generic
|
||||
users=root:toor,admin:admin123
|
||||
|
||||
[hostname-1.http]
|
||||
server_header=nginx/1.18.0
|
||||
fake_app=wordpress
|
||||
|
||||
[hostname-2]
|
||||
services=ssh
|
||||
|
||||
[hostname-3]
|
||||
ip=192.168.1.32
|
||||
|
||||
# Custom (bring-your-own) service definitions:
|
||||
[custom-myservice]
|
||||
binary=my-docker-image:latest
|
||||
exec=/usr/bin/myservice -p 8080
|
||||
ports=8080
|
||||
|
||||
"""
|
||||
|
||||
import configparser
|
||||
@@ -29,6 +44,16 @@ class DeckySpec:
|
||||
name: str
|
||||
ip: str | None = None
|
||||
services: list[str] | None = None
|
||||
service_config: dict[str, dict] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CustomServiceSpec:
|
||||
"""Spec for a user-defined (bring-your-own) service."""
|
||||
name: str # service slug, e.g. "myservice" (section is "custom-myservice")
|
||||
image: str # Docker image to use
|
||||
exec_cmd: str # command to run inside the container
|
||||
ports: list[int] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -38,6 +63,7 @@ class IniConfig:
|
||||
interface: str | None = None
|
||||
log_target: str | None = None
|
||||
deckies: list[DeckySpec] = field(default_factory=list)
|
||||
custom_services: list[CustomServiceSpec] = field(default_factory=list)
|
||||
|
||||
|
||||
def load_ini(path: str | Path) -> IniConfig:
|
||||
@@ -56,13 +82,40 @@ def load_ini(path: str | Path) -> IniConfig:
|
||||
cfg.interface = g.get("interface")
|
||||
cfg.log_target = g.get("log_target") or g.get("log-target")
|
||||
|
||||
# First pass: collect decky sections and custom service definitions
|
||||
for section in cp.sections():
|
||||
if section == "general":
|
||||
continue
|
||||
if "." in section:
|
||||
continue # subsections handled in second pass
|
||||
if section.startswith("custom-"):
|
||||
# Bring-your-own service definition
|
||||
s = cp[section]
|
||||
svc_name = section[len("custom-"):]
|
||||
image = s.get("binary", "")
|
||||
exec_cmd = s.get("exec", "")
|
||||
ports_raw = s.get("ports", "")
|
||||
ports = [int(p.strip()) for p in ports_raw.split(",") if p.strip().isdigit()]
|
||||
cfg.custom_services.append(
|
||||
CustomServiceSpec(name=svc_name, image=image, exec_cmd=exec_cmd, ports=ports)
|
||||
)
|
||||
continue
|
||||
s = cp[section]
|
||||
ip = s.get("ip")
|
||||
svc_raw = s.get("services")
|
||||
services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None
|
||||
cfg.deckies.append(DeckySpec(name=section, ip=ip, services=services))
|
||||
|
||||
# Second pass: collect per-service subsections [decky-name.service]
|
||||
decky_names = {d.name for d in cfg.deckies}
|
||||
decky_map = {d.name: d for d in cfg.deckies}
|
||||
for section in cp.sections():
|
||||
if "." not in section:
|
||||
continue
|
||||
decky_name, _, svc_name = section.partition(".")
|
||||
if decky_name not in decky_names:
|
||||
continue # orphaned subsection — ignore
|
||||
svc_cfg = {k: v for k, v in cp[section].items()}
|
||||
decky_map[decky_name].service_config[svc_name] = svc_cfg
|
||||
|
||||
return cfg
|
||||
|
||||
@@ -15,7 +15,12 @@ class BaseService(ABC):
|
||||
default_image: str # Docker image tag, or "build" if a Dockerfile is needed
|
||||
|
||||
@abstractmethod
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Return the docker-compose service dict for this service on a given decky.
|
||||
|
||||
@@ -26,6 +31,7 @@ class BaseService(ABC):
|
||||
Args:
|
||||
decky_name: unique identifier for the decky (e.g. "decky-01")
|
||||
log_target: "ip:port" string if log forwarding is enabled, else None
|
||||
service_cfg: optional per-service persona config from INI subsection
|
||||
"""
|
||||
|
||||
def dockerfile_context(self) -> Path | None:
|
||||
|
||||
@@ -12,7 +12,7 @@ class ConpotService(BaseService):
|
||||
ports = [502, 161, 80]
|
||||
default_image = "honeynet/conpot"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
return {
|
||||
"image": "honeynet/conpot",
|
||||
"container_name": f"{decky_name}-conpot",
|
||||
|
||||
@@ -9,12 +9,12 @@ class DockerAPIService(BaseService):
|
||||
ports = [2375, 2376]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-docker-api",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -10,13 +10,13 @@ class ElasticsearchService(BaseService):
|
||||
ports = [9200]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-elasticsearch",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {
|
||||
"HONEYPOT_NAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
},
|
||||
}
|
||||
if log_target:
|
||||
|
||||
@@ -9,13 +9,13 @@ class FTPService(BaseService):
|
||||
ports = [21]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-ftp",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {
|
||||
"HONEYPOT_NAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
},
|
||||
}
|
||||
if log_target:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from decnet.services.base import BaseService
|
||||
|
||||
@@ -9,17 +10,43 @@ class HTTPService(BaseService):
|
||||
ports = [80, 443]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
cfg = service_cfg or {}
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-http",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {
|
||||
"HONEYPOT_NAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
# Optional persona overrides — only injected when explicitly set
|
||||
if "server_header" in cfg:
|
||||
fragment["environment"]["SERVER_HEADER"] = cfg["server_header"]
|
||||
if "response_code" in cfg:
|
||||
fragment["environment"]["RESPONSE_CODE"] = str(cfg["response_code"])
|
||||
if "fake_app" in cfg:
|
||||
fragment["environment"]["FAKE_APP"] = cfg["fake_app"]
|
||||
if "extra_headers" in cfg:
|
||||
val = cfg["extra_headers"]
|
||||
fragment["environment"]["EXTRA_HEADERS"] = (
|
||||
json.dumps(val) if isinstance(val, dict) else val
|
||||
)
|
||||
if "custom_body" in cfg:
|
||||
fragment["environment"]["CUSTOM_BODY"] = cfg["custom_body"]
|
||||
if "files" in cfg:
|
||||
files_path = str(Path(cfg["files"]).resolve())
|
||||
fragment["environment"]["FILES_DIR"] = "/opt/html_files"
|
||||
fragment.setdefault("volumes", []).append(f"{files_path}:/opt/html_files:ro")
|
||||
|
||||
return fragment
|
||||
|
||||
def dockerfile_context(self) -> Path | None:
|
||||
|
||||
@@ -9,12 +9,12 @@ class IMAPService(BaseService):
|
||||
ports = [143, 993]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-imap",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,12 +9,12 @@ class KubernetesAPIService(BaseService):
|
||||
ports = [6443, 8080]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-k8s",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,13 +9,13 @@ class LDAPService(BaseService):
|
||||
ports = [389, 636]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-ldap",
|
||||
"restart": "unless-stopped",
|
||||
"cap_add": ["NET_BIND_SERVICE"],
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -16,12 +16,12 @@ class LLMNRService(BaseService):
|
||||
ports = [5355, 5353]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-llmnr",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,12 +9,12 @@ class MongoDBService(BaseService):
|
||||
ports = [27017]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-mongodb",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,12 +9,12 @@ class MQTTService(BaseService):
|
||||
ports = [1883]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-mqtt",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,12 +9,12 @@ class MSSQLService(BaseService):
|
||||
ports = [1433]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-mssql",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,15 +9,23 @@ class MySQLService(BaseService):
|
||||
ports = [3306]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
cfg = service_cfg or {}
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-mysql",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
if "version" in cfg:
|
||||
fragment["environment"]["MYSQL_VERSION"] = cfg["version"]
|
||||
return fragment
|
||||
|
||||
def dockerfile_context(self) -> Path | None:
|
||||
|
||||
@@ -9,12 +9,12 @@ class POP3Service(BaseService):
|
||||
ports = [110, 995]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-pop3",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,12 +9,12 @@ class PostgresService(BaseService):
|
||||
ports = [5432]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-postgres",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,13 +9,13 @@ class RDPService(BaseService):
|
||||
ports = [3389]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-rdp",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {
|
||||
"HONEYPOT_NAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
},
|
||||
}
|
||||
if log_target:
|
||||
|
||||
@@ -9,15 +9,25 @@ class RedisService(BaseService):
|
||||
ports = [6379]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
cfg = service_cfg or {}
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-redis",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
if "version" in cfg:
|
||||
fragment["environment"]["REDIS_VERSION"] = cfg["version"]
|
||||
if "os_string" in cfg:
|
||||
fragment["environment"]["REDIS_OS"] = cfg["os_string"]
|
||||
return fragment
|
||||
|
||||
def dockerfile_context(self) -> Path | None:
|
||||
|
||||
@@ -31,6 +31,12 @@ def _load_plugins() -> None:
|
||||
_loaded = True
|
||||
|
||||
|
||||
def register_custom_service(instance: BaseService) -> None:
|
||||
"""Register a dynamically created service (e.g. BYOS from INI)."""
|
||||
_load_plugins()
|
||||
_registry[instance.name] = instance
|
||||
|
||||
|
||||
def get_service(name: str) -> BaseService:
|
||||
_load_plugins()
|
||||
if name not in _registry:
|
||||
|
||||
@@ -9,12 +9,12 @@ class SIPService(BaseService):
|
||||
ports = [5060]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-sip",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,14 +9,14 @@ class SMBService(BaseService):
|
||||
ports = [445, 139]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-smb",
|
||||
"restart": "unless-stopped",
|
||||
"cap_add": ["NET_BIND_SERVICE"],
|
||||
"environment": {
|
||||
"HONEYPOT_NAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
},
|
||||
}
|
||||
if log_target:
|
||||
|
||||
@@ -10,18 +10,28 @@ class SMTPService(BaseService):
|
||||
ports = [25, 587]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
cfg = service_cfg or {}
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-smtp",
|
||||
"restart": "unless-stopped",
|
||||
"cap_add": ["NET_BIND_SERVICE"],
|
||||
"environment": {
|
||||
"HONEYPOT_NAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
if "banner" in cfg:
|
||||
fragment["environment"]["SMTP_BANNER"] = cfg["banner"]
|
||||
if "mta" in cfg:
|
||||
fragment["environment"]["SMTP_MTA"] = cfg["mta"]
|
||||
return fragment
|
||||
|
||||
def dockerfile_context(self) -> Path:
|
||||
|
||||
@@ -9,12 +9,12 @@ class SNMPService(BaseService):
|
||||
ports = [161]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-snmp",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
from pathlib import Path
|
||||
from decnet.services.base import BaseService
|
||||
|
||||
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "cowrie"
|
||||
|
||||
|
||||
class SSHService(BaseService):
|
||||
name = "ssh"
|
||||
ports = [22, 2222]
|
||||
default_image = "cowrie/cowrie"
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(
|
||||
self,
|
||||
decky_name: str,
|
||||
log_target: str | None = None,
|
||||
service_cfg: dict | None = None,
|
||||
) -> dict:
|
||||
cfg = service_cfg or {}
|
||||
env: dict = {
|
||||
# Override [honeypot] and [ssh] listen_endpoints to also bind port 22
|
||||
"COWRIE_HONEYPOT_HOSTNAME": decky_name,
|
||||
"NODE_NAME": decky_name,
|
||||
"COWRIE_HOSTNAME": decky_name,
|
||||
"COWRIE_HONEYPOT_LISTEN_ENDPOINTS": "tcp:22:interface=0.0.0.0 tcp:2222:interface=0.0.0.0",
|
||||
"COWRIE_SSH_LISTEN_ENDPOINTS": "tcp:22:interface=0.0.0.0 tcp:2222:interface=0.0.0.0",
|
||||
}
|
||||
@@ -18,13 +27,26 @@ class SSHService(BaseService):
|
||||
env["COWRIE_OUTPUT_TCP_ENABLED"] = "true"
|
||||
env["COWRIE_OUTPUT_TCP_HOST"] = host
|
||||
env["COWRIE_OUTPUT_TCP_PORT"] = port
|
||||
|
||||
# Optional persona overrides
|
||||
if "kernel_version" in cfg:
|
||||
env["COWRIE_HONEYPOT_KERNEL_VERSION"] = cfg["kernel_version"]
|
||||
if "kernel_build_string" in cfg:
|
||||
env["COWRIE_HONEYPOT_KERNEL_BUILD_STRING"] = cfg["kernel_build_string"]
|
||||
if "hardware_platform" in cfg:
|
||||
env["COWRIE_HONEYPOT_HARDWARE_PLATFORM"] = cfg["hardware_platform"]
|
||||
if "ssh_banner" in cfg:
|
||||
env["COWRIE_SSH_VERSION"] = cfg["ssh_banner"]
|
||||
if "users" in cfg:
|
||||
env["COWRIE_USERDB_ENTRIES"] = cfg["users"]
|
||||
|
||||
return {
|
||||
"image": "cowrie/cowrie",
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-ssh",
|
||||
"restart": "unless-stopped",
|
||||
"cap_add": ["NET_BIND_SERVICE"],
|
||||
"environment": env,
|
||||
}
|
||||
|
||||
def dockerfile_context(self):
|
||||
return None
|
||||
def dockerfile_context(self) -> Path:
|
||||
return TEMPLATES_DIR
|
||||
|
||||
@@ -6,7 +6,7 @@ class TelnetService(BaseService):
|
||||
ports = [23]
|
||||
default_image = "cowrie/cowrie"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
env: dict = {
|
||||
"COWRIE_HONEYPOT_HOSTNAME": decky_name,
|
||||
"COWRIE_TELNET_ENABLED": "true",
|
||||
|
||||
@@ -9,12 +9,12 @@ class TFTPService(BaseService):
|
||||
ports = [69]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-tftp",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -9,12 +9,12 @@ class VNCService(BaseService):
|
||||
ports = [5900]
|
||||
default_image = "build"
|
||||
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None) -> dict:
|
||||
def compose_fragment(self, decky_name: str, log_target: str | None = None, service_cfg: dict | None = None) -> dict:
|
||||
fragment: dict = {
|
||||
"build": {"context": str(TEMPLATES_DIR)},
|
||||
"container_name": f"{decky_name}-vnc",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {"HONEYPOT_NAME": decky_name},
|
||||
"environment": {"NODE_NAME": decky_name},
|
||||
}
|
||||
if log_target:
|
||||
fragment["environment"]["LOG_TARGET"] = log_target
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
[honeypot]
|
||||
hostname = {{ COWRIE_HOSTNAME | default('svr01') }}
|
||||
listen_endpoints = tcp:2222:interface=0.0.0.0
|
||||
kernel_version = {{ COWRIE_HONEYPOT_KERNEL_VERSION | default('5.15.0-76-generic') }}
|
||||
kernel_build_string = {{ COWRIE_HONEYPOT_KERNEL_BUILD_STRING | default('#83-Ubuntu SMP Thu Jun 15 19:16:32 UTC 2023') }}
|
||||
hardware_platform = {{ COWRIE_HONEYPOT_HARDWARE_PLATFORM | default('x86_64') }}
|
||||
|
||||
[ssh]
|
||||
enabled = true
|
||||
listen_endpoints = tcp:2222:interface=0.0.0.0
|
||||
version = {{ COWRIE_SSH_VERSION | default('SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5') }}
|
||||
|
||||
{% if COWRIE_LOG_HOST is defined and COWRIE_LOG_HOST %}
|
||||
[output_jsonlog]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Render Jinja2 template using the venv's python (has jinja2)
|
||||
# Render Jinja2 config template
|
||||
/home/cowrie/cowrie-env/bin/python3 - <<'EOF'
|
||||
import os
|
||||
from jinja2 import Template
|
||||
@@ -15,4 +15,19 @@ with open("/home/cowrie/cowrie-env/etc/cowrie.cfg", "w") as f:
|
||||
f.write(rendered)
|
||||
EOF
|
||||
|
||||
# Write userdb.txt if custom users were provided
|
||||
# Format: COWRIE_USERDB_ENTRIES=root:toor,admin:admin123
|
||||
if [ -n "${COWRIE_USERDB_ENTRIES}" ]; then
|
||||
USERDB="/home/cowrie/cowrie-env/etc/userdb.txt"
|
||||
: > "$USERDB"
|
||||
IFS=',' read -ra PAIRS <<< "${COWRIE_USERDB_ENTRIES}"
|
||||
for pair in "${PAIRS[@]}"; do
|
||||
user="${pair%%:*}"
|
||||
pass="${pair#*:}"
|
||||
uid=1000
|
||||
[ "$user" = "root" ] && uid=0
|
||||
echo "${user}:${uid}:${pass}" >> "$USERDB"
|
||||
done
|
||||
fi
|
||||
|
||||
exec authbind --deep /home/cowrie/cowrie-env/bin/twistd -n --pidfile= cowrie
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN pip3 install --no-cache-dir flask
|
||||
|
||||
COPY docker_api_honeypot.py /opt/docker_api_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/docker_api_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Docker API honeypot.
|
||||
Docker APIserver.
|
||||
Serves a fake Docker REST API on port 2375. Responds to common recon
|
||||
endpoints (/version, /info, /containers/json, /images/json) with plausible
|
||||
but fake data. Logs all requests as JSON.
|
||||
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
|
||||
|
||||
from flask import Flask, request
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "docker-host")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "docker-host")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -38,7 +38,7 @@ _INFO = {
|
||||
"MemoryLimit": True,
|
||||
"SwapLimit": True,
|
||||
"KernelMemory": False,
|
||||
"Name": HONEYPOT_NAME,
|
||||
"Name": NODE_NAME,
|
||||
"DockerRootDir": "/var/lib/docker",
|
||||
"HttpProxy": "",
|
||||
"HttpsProxy": "",
|
||||
@@ -73,7 +73,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "docker_api",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -127,5 +127,5 @@ def catch_all(path):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"Docker API honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"Docker API server starting as {NODE_NAME}")
|
||||
app.run(host="0.0.0.0", port=2375, debug=False)
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY elasticsearch_honeypot.py /opt/elasticsearch_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/elasticsearch_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Elasticsearch honeypot — presents a convincing ES 7.x HTTP API on port 9200.
|
||||
Elasticsearch server — presents a convincing ES 7.x HTTP API on port 9200.
|
||||
Logs all requests (especially recon probes like /_cat/, /_cluster/, /_nodes/)
|
||||
as JSON. Designed to attract automated scanners and credential stuffers.
|
||||
"""
|
||||
@@ -11,14 +11,14 @@ import socket
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "esserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "esserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
_CLUSTER_UUID = "xC3Pr9abTq2mNkOeLvXwYA"
|
||||
_NODE_UUID = "dJH7Lm2sRqWvPn0kFiEtBo"
|
||||
|
||||
_ROOT_RESPONSE = {
|
||||
"name": HONEYPOT_NAME,
|
||||
"name": NODE_NAME,
|
||||
"cluster_name": "elasticsearch",
|
||||
"cluster_uuid": _CLUSTER_UUID,
|
||||
"version": {
|
||||
@@ -51,7 +51,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "elasticsearch",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class ESHandler(BaseHTTPRequestHandler):
|
||||
if "_search" in path or "_bulk" in path:
|
||||
self._send_json(200, {"took": 1, "timed_out": False, "hits": {"total": {"value": 0}, "hits": []}})
|
||||
else:
|
||||
self._send_json(200, {"result": "created", "_id": "1", "_index": "honeypot"})
|
||||
self._send_json(200, {"result": "created", "_id": "1", "_index": "server"})
|
||||
|
||||
def do_PUT(self):
|
||||
src = self.client_address[0]
|
||||
@@ -133,6 +133,6 @@ class ESHandler(BaseHTTPRequestHandler):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"Elasticsearch honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"Elasticsearch server starting as {NODE_NAME}")
|
||||
server = HTTPServer(("0.0.0.0", 9200), ESHandler)
|
||||
server.serve_forever()
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN pip3 install --no-cache-dir twisted jinja2
|
||||
|
||||
COPY ftp_honeypot.py /opt/ftp_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/ftp_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FTP honeypot using Twisted's FTP server infrastructure.
|
||||
FTP server using Twisted's FTP server infrastructure.
|
||||
Accepts any credentials, logs all commands and file requests,
|
||||
forwards events as JSON to LOG_TARGET if set.
|
||||
"""
|
||||
@@ -15,7 +15,7 @@ from twisted.internet import defer, protocol, reactor
|
||||
from twisted.protocols.ftp import FTP, FTPFactory
|
||||
from twisted.python import log as twisted_log
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "ftpserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "ftpserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "ftp",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -42,22 +42,22 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
_forward(event)
|
||||
|
||||
|
||||
class HoneypotFTP(FTP):
|
||||
class ServerFTP(FTP):
|
||||
def connectionMade(self):
|
||||
peer = self.transport.getPeer()
|
||||
_log("connection", src_ip=peer.host, src_port=peer.port)
|
||||
super().connectionMade()
|
||||
|
||||
def ftp_USER(self, username):
|
||||
self._honeypot_user = username
|
||||
self._server_user = username
|
||||
_log("user", username=username)
|
||||
return super().ftp_USER(username)
|
||||
|
||||
def ftp_PASS(self, password):
|
||||
_log("auth_attempt", username=getattr(self, "_honeypot_user", "?"), password=password)
|
||||
# Accept everything — we're a honeypot
|
||||
_log("auth_attempt", username=getattr(self, "_server_user", "?"), password=password)
|
||||
# Accept everything — we're a server
|
||||
self.state = self.AUTHED
|
||||
self._user = getattr(self, "_honeypot_user", "anonymous")
|
||||
self._user = getattr(self, "_server_user", "anonymous")
|
||||
return defer.succeed((230, "Login successful."))
|
||||
|
||||
def ftp_RETR(self, path):
|
||||
@@ -71,12 +71,12 @@ class HoneypotFTP(FTP):
|
||||
super().connectionLost(reason)
|
||||
|
||||
|
||||
class HoneypotFTPFactory(FTPFactory):
|
||||
protocol = HoneypotFTP
|
||||
class ServerFTPFactory(FTPFactory):
|
||||
protocol = ServerFTP
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
twisted_log.startLogging(sys.stdout)
|
||||
_log("startup", msg=f"FTP honeypot starting as {HONEYPOT_NAME} on port 21")
|
||||
reactor.listenTCP(21, HoneypotFTPFactory())
|
||||
_log("startup", msg=f"FTP server starting as {NODE_NAME} on port 21")
|
||||
reactor.listenTCP(21, ServerFTPFactory())
|
||||
reactor.run()
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN pip3 install --no-cache-dir flask jinja2
|
||||
|
||||
COPY http_honeypot.py /opt/http_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/http_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP honeypot using Flask.
|
||||
Accepts all requests, logs every detail (method, path, headers, body),
|
||||
and responds with convincing but empty pages. Forwards events as JSON
|
||||
to LOG_TARGET if set.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Flask, request
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "webserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _forward(event: dict) -> None:
|
||||
if not LOG_TARGET:
|
||||
return
|
||||
try:
|
||||
host, port = LOG_TARGET.rsplit(":", 1)
|
||||
with socket.create_connection((host, int(port)), timeout=3) as s:
|
||||
s.sendall((json.dumps(event) + "\n").encode())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "http",
|
||||
"host": HONEYPOT_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
print(json.dumps(event), flush=True)
|
||||
_forward(event)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def log_request():
|
||||
_log(
|
||||
"request",
|
||||
method=request.method,
|
||||
path=request.path,
|
||||
remote_addr=request.remote_addr,
|
||||
headers=dict(request.headers),
|
||||
body=request.get_data(as_text=True)[:512],
|
||||
)
|
||||
|
||||
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
||||
def catch_all(path):
|
||||
return (
|
||||
"<html><body><h1>403 Forbidden</h1></body></html>",
|
||||
403,
|
||||
{"Server": "Apache/2.4.54 (Debian)", "Content-Type": "text/html"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"HTTP honeypot starting as {HONEYPOT_NAME}")
|
||||
app.run(host="0.0.0.0", port=80, debug=False)
|
||||
118
templates/http/server.py
Normal file
118
templates/http/server.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP service emulator using Flask.
|
||||
Accepts all requests, logs every detail (method, path, headers, body),
|
||||
and responds with configurable pages. Forwards events as JSON to LOG_TARGET if set.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "webserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
SERVER_HEADER = os.environ.get("SERVER_HEADER", "Apache/2.4.54 (Debian)")
|
||||
RESPONSE_CODE = int(os.environ.get("RESPONSE_CODE", "403"))
|
||||
FAKE_APP = os.environ.get("FAKE_APP", "")
|
||||
EXTRA_HEADERS = json.loads(os.environ.get("EXTRA_HEADERS", "{}"))
|
||||
CUSTOM_BODY = os.environ.get("CUSTOM_BODY", "")
|
||||
FILES_DIR = os.environ.get("FILES_DIR", "")
|
||||
|
||||
_FAKE_APP_BODIES: dict[str, str] = {
|
||||
"apache_default": (
|
||||
"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n"
|
||||
"<html><head><title>Apache2 Debian Default Page</title></head>\n"
|
||||
"<body><h1>Apache2 Debian Default Page</h1>\n"
|
||||
"<p>It works!</p></body></html>"
|
||||
),
|
||||
"nginx_default": (
|
||||
"<!DOCTYPE html><html><head><title>Welcome to nginx!</title></head>\n"
|
||||
"<body><h1>Welcome to nginx!</h1>\n"
|
||||
"<p>If you see this page, the nginx web server is successfully installed.</p>\n"
|
||||
"</body></html>"
|
||||
),
|
||||
"wordpress": (
|
||||
"<!DOCTYPE html><html><head><title>WordPress › Error</title></head>\n"
|
||||
"<body id=\"error-page\"><div class=\"wp-die-message\">\n"
|
||||
"<h1>Error establishing a database connection</h1></div></body></html>"
|
||||
),
|
||||
"phpmyadmin": (
|
||||
"<!DOCTYPE html><html><head><title>phpMyAdmin</title></head>\n"
|
||||
"<body><form method=\"post\" action=\"index.php\">\n"
|
||||
"<input type=\"text\" name=\"pma_username\" />\n"
|
||||
"<input type=\"password\" name=\"pma_password\" />\n"
|
||||
"<input type=\"submit\" value=\"Go\" /></form></body></html>"
|
||||
),
|
||||
"iis_default": (
|
||||
"<!DOCTYPE html><html><head><title>IIS Windows Server</title></head>\n"
|
||||
"<body><h1>IIS Windows Server</h1>\n"
|
||||
"<p>Welcome to Internet Information Services</p></body></html>"
|
||||
),
|
||||
}
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def _forward(event: dict) -> None:
|
||||
if not LOG_TARGET:
|
||||
return
|
||||
try:
|
||||
host, port = LOG_TARGET.rsplit(":", 1)
|
||||
with socket.create_connection((host, int(port)), timeout=3) as s:
|
||||
s.sendall((json.dumps(event) + "\n").encode())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "http",
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
print(json.dumps(event), flush=True)
|
||||
_forward(event)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def log_request():
|
||||
_log(
|
||||
"request",
|
||||
method=request.method,
|
||||
path=request.path,
|
||||
remote_addr=request.remote_addr,
|
||||
headers=dict(request.headers),
|
||||
body=request.get_data(as_text=True)[:512],
|
||||
)
|
||||
|
||||
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
||||
def catch_all(path):
|
||||
# Serve static files directory if configured
|
||||
if FILES_DIR and path:
|
||||
files_path = Path(FILES_DIR) / path
|
||||
if files_path.is_file():
|
||||
return send_from_directory(FILES_DIR, path)
|
||||
|
||||
# Select response body: custom > fake_app preset > default 403
|
||||
if CUSTOM_BODY:
|
||||
body = CUSTOM_BODY
|
||||
elif FAKE_APP and FAKE_APP in _FAKE_APP_BODIES:
|
||||
body = _FAKE_APP_BODIES[FAKE_APP]
|
||||
else:
|
||||
body = "<html><body><h1>403 Forbidden</h1></body></html>"
|
||||
|
||||
headers = {"Server": SERVER_HEADER, "Content-Type": "text/html", **EXTRA_HEADERS}
|
||||
return body, RESPONSE_CODE, headers
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"HTTP server starting as {NODE_NAME}")
|
||||
app.run(host="0.0.0.0", port=80, debug=False)
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY imap_honeypot.py /opt/imap_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/imap_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IMAP honeypot.
|
||||
IMAPserver.
|
||||
Presents an IMAP4rev1 banner, captures LOGIN credentials (plaintext and
|
||||
AUTHENTICATE), then returns a NO response. Logs all commands as JSON.
|
||||
"""
|
||||
@@ -11,9 +11,9 @@ import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mailserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
BANNER = f"* OK [{HONEYPOT_NAME}] IMAP4rev1 Service Ready\r\n"
|
||||
BANNER = f"* OK [{NODE_NAME}] IMAP4rev1 Service Ready\r\n"
|
||||
|
||||
|
||||
def _forward(event: dict) -> None:
|
||||
@@ -31,7 +31,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "imap",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class IMAPProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"IMAP honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"IMAP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(IMAPProtocol, "0.0.0.0", 143)
|
||||
async with server:
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Kubernetes API honeypot.
|
||||
Kubernetes APIserver.
|
||||
Serves a fake K8s REST API on port 6443 (HTTPS-ish, plain HTTP) and 8080.
|
||||
Responds to recon endpoints (/version, /api, /apis, /api/v1/namespaces,
|
||||
/api/v1/pods) with plausible but fake data. Logs all requests as JSON.
|
||||
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
|
||||
|
||||
from flask import Flask, request
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "k8s-master")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "k8s-master")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -33,7 +33,7 @@ _VERSION = {
|
||||
_API_VERSIONS = {
|
||||
"kind": "APIVersions",
|
||||
"versions": ["v1"],
|
||||
"serverAddressByClientCIDRs": [{"clientCIDR": "0.0.0.0/0", "serverAddress": f"{HONEYPOT_NAME}:6443"}],
|
||||
"serverAddressByClientCIDRs": [{"clientCIDR": "0.0.0.0/0", "serverAddress": f"{NODE_NAME}:6443"}],
|
||||
}
|
||||
|
||||
_NAMESPACES = {
|
||||
@@ -80,7 +80,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "k8s",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -138,5 +138,5 @@ def catch_all(path):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"Kubernetes API honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"Kubernetes API server starting as {NODE_NAME}")
|
||||
app.run(host="0.0.0.0", port=6443, debug=False)
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ldap_honeypot.py /opt/ldap_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/ldap_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LDAP honeypot.
|
||||
LDAPserver.
|
||||
Parses BER-encoded BindRequest messages, logs DN and password, returns an
|
||||
invalidCredentials error. Logs all interactions as JSON.
|
||||
"""
|
||||
@@ -11,7 +11,7 @@ import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "ldapserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "ldapserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "ldap",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -154,7 +154,7 @@ class LDAPProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"LDAP honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"LDAP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(LDAPProtocol, "0.0.0.0", 389)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY llmnr_honeypot.py /opt/llmnr_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/llmnr_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -13,7 +13,7 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "lan-host")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "lan-host")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "llmnr",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -104,7 +104,7 @@ class LLMNRProtocol(asyncio.DatagramProtocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"LLMNR/mDNS honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"LLMNR/mDNS server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# LLMNR: UDP 5355
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY mongodb_honeypot.py /opt/mongodb_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/mongodb_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MongoDB honeypot.
|
||||
MongoDBserver.
|
||||
Implements the MongoDB wire protocol OP_MSG/OP_QUERY handshake. Responds
|
||||
to isMaster/hello, listDatabases, and authenticate commands. Logs all
|
||||
received messages as JSON.
|
||||
@@ -13,7 +13,7 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mongodb")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mongodb")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
# Minimal BSON helpers
|
||||
@@ -64,7 +64,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "mongodb",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class MongoDBProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"MongoDB honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"MongoDB server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MongoDBProtocol, "0.0.0.0", 27017)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY mqtt_honeypot.py /opt/mqtt_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/mqtt_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MQTT honeypot (port 1883).
|
||||
MQTT server (port 1883).
|
||||
Parses MQTT CONNECT packets, extracts client_id, username, and password,
|
||||
then returns CONNACK with return code 5 (not authorized). Logs all
|
||||
interactions as JSON.
|
||||
@@ -13,7 +13,7 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mqtt-broker")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mqtt-broker")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
# CONNACK: packet type 0x20, remaining length 2, session_present=0, return_code=5
|
||||
@@ -35,7 +35,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "mqtt",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class MQTTProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"MQTT honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"MQTT server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MQTTProtocol, "0.0.0.0", 1883)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY mssql_honeypot.py /opt/mssql_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/mssql_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MSSQL (TDS) honeypot.
|
||||
MSSQL (TDS)server.
|
||||
Reads TDS pre-login and login7 packets, extracts username, responds with
|
||||
a login failed error. Logs auth attempts as JSON.
|
||||
"""
|
||||
@@ -12,7 +12,7 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "dbserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "dbserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
# Minimal TDS pre-login response
|
||||
@@ -54,7 +54,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "mssql",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class MSSQLProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"MSSQL honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"MSSQL server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MSSQLProtocol, "0.0.0.0", 1433)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY mysql_honeypot.py /opt/mysql_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/mysql_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MySQL honeypot.
|
||||
MySQLserver.
|
||||
Sends a realistic MySQL 5.7 server handshake, reads the client login
|
||||
packet, extracts username, then closes with Access Denied. Logs auth
|
||||
attempts as JSON.
|
||||
@@ -13,24 +13,25 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "dbserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "dbserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
_MYSQL_VER = os.environ.get("MYSQL_VERSION", "5.7.38-log")
|
||||
|
||||
# Minimal MySQL 5.7 server greeting (protocol v10)
|
||||
# Minimal MySQL server greeting (protocol v10) — version string is configurable
|
||||
_GREETING = (
|
||||
b"\x0a" # protocol version 10
|
||||
b"5.7.38-honeypot\x00" # server version + NUL
|
||||
b"\x01\x00\x00\x00" # connection id = 1
|
||||
b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1
|
||||
b"\x00" # filler
|
||||
b"\xff\xf7" # capability flags low
|
||||
b"\x21" # charset utf8
|
||||
b"\x02\x00" # status flags
|
||||
b"\xff\x81" # capability flags high
|
||||
b"\x15" # auth plugin data length
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserved (10 bytes)
|
||||
b"\x21\x4f\x7d\x25\x3e\x55\x4d\x7c\x67\x75\x5e\x31\x00" # auth part 2
|
||||
b"mysql_native_password\x00" # auth plugin name
|
||||
+ _MYSQL_VER.encode() + b"\x00" # server version + NUL
|
||||
+ b"\x01\x00\x00\x00" # connection id = 1
|
||||
+ b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1
|
||||
+ b"\x00" # filler
|
||||
+ b"\xff\xf7" # capability flags low
|
||||
+ b"\x21" # charset utf8
|
||||
+ b"\x02\x00" # status flags
|
||||
+ b"\xff\x81" # capability flags high
|
||||
+ b"\x15" # auth plugin data length
|
||||
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserved (10 bytes)
|
||||
+ b"\x21\x4f\x7d\x25\x3e\x55\x4d\x7c\x67\x75\x5e\x31\x00" # auth part 2
|
||||
+ b"mysql_native_password\x00" # auth plugin name
|
||||
)
|
||||
|
||||
|
||||
@@ -54,7 +55,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "mysql",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -110,7 +111,7 @@ class MySQLProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"MySQL honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"MySQL server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(MySQLProtocol, "0.0.0.0", 3306)
|
||||
async with server:
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
POP3 honeypot.
|
||||
POP3server.
|
||||
Presents a convincing POP3 banner, collects USER/PASS credentials, then
|
||||
stalls with a generic error. Logs every interaction as JSON and forwards
|
||||
to LOG_TARGET if set.
|
||||
@@ -12,9 +12,9 @@ import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mailserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
BANNER = f"+OK {HONEYPOT_NAME} POP3 server ready\r\n"
|
||||
BANNER = f"+OK {NODE_NAME} POP3 server ready\r\n"
|
||||
|
||||
|
||||
def _forward(event: dict) -> None:
|
||||
@@ -32,7 +32,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "pop3",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class POP3Protocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"POP3 honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"POP3 server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(POP3Protocol, "0.0.0.0", 110)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY postgres_honeypot.py /opt/postgres_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/postgres_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PostgreSQL honeypot.
|
||||
PostgreSQLserver.
|
||||
Reads the startup message, extracts username and database, responds with
|
||||
an AuthenticationMD5Password challenge, logs the hash sent back, then
|
||||
returns an error. Logs all interactions as JSON.
|
||||
@@ -13,7 +13,7 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "pgserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "pgserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
SALT = b"\xde\xad\xbe\xef"
|
||||
|
||||
@@ -40,7 +40,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "postgres",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -118,7 +118,7 @@ class PostgresProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"PostgreSQL honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"PostgreSQL server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(PostgresProtocol, "0.0.0.0", 5432)
|
||||
async with server:
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN pip3 install --no-cache-dir twisted jinja2
|
||||
|
||||
COPY rdp_honeypot.py /opt/rdp_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/rdp_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal RDP honeypot using Twisted.
|
||||
Minimal RDP server using Twisted.
|
||||
Listens on port 3389, logs connection attempts and any credentials sent
|
||||
in the initial RDP negotiation request. Forwards events as JSON to
|
||||
LOG_TARGET if set.
|
||||
@@ -15,7 +15,7 @@ from datetime import datetime, timezone
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.python import log as twisted_log
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "WORKSTATION")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "WORKSTATION")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "rdp",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -42,7 +42,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
_forward(event)
|
||||
|
||||
|
||||
class RDPHoneypotProtocol(protocol.Protocol):
|
||||
class RDPServerProtocol(protocol.Protocol):
|
||||
def connectionMade(self):
|
||||
peer = self.transport.getPeer()
|
||||
_log("connection", src_ip=peer.host, src_port=peer.port)
|
||||
@@ -61,12 +61,12 @@ class RDPHoneypotProtocol(protocol.Protocol):
|
||||
_log("disconnect", src_ip=peer.host, src_port=peer.port)
|
||||
|
||||
|
||||
class RDPHoneypotFactory(protocol.ServerFactory):
|
||||
protocol = RDPHoneypotProtocol
|
||||
class RDPServerFactory(protocol.ServerFactory):
|
||||
protocol = RDPServerProtocol
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
twisted_log.startLogging(sys.stdout)
|
||||
_log("startup", msg=f"RDP honeypot starting as {HONEYPOT_NAME} on port 3389")
|
||||
reactor.listenTCP(3389, RDPHoneypotFactory())
|
||||
_log("startup", msg=f"RDP server starting as {NODE_NAME} on port 3389")
|
||||
reactor.listenTCP(3389, RDPServerFactory())
|
||||
reactor.run()
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY redis_honeypot.py /opt/redis_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/redis_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Redis honeypot.
|
||||
Redisserver.
|
||||
Implements enough of the RESP protocol to respond to AUTH, INFO, CONFIG GET,
|
||||
KEYS, and arbitrary commands. Logs every command and argument as JSON.
|
||||
"""
|
||||
@@ -11,19 +11,22 @@ import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "cache-server")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "cache-server")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
_REDIS_VER = os.environ.get("REDIS_VERSION", "7.0.12")
|
||||
_REDIS_OS = os.environ.get("REDIS_OS", "Linux 5.15.0")
|
||||
|
||||
_INFO = f"""# Server
|
||||
redis_version:7.0.12
|
||||
redis_mode:standalone
|
||||
os:Linux 5.15.0
|
||||
arch_bits:64
|
||||
tcp_port:6379
|
||||
uptime_in_seconds:864000
|
||||
connected_clients:1
|
||||
# Keyspace
|
||||
""".encode()
|
||||
_INFO = (
|
||||
f"# Server\n"
|
||||
f"redis_version:{_REDIS_VER}\n"
|
||||
f"redis_mode:standalone\n"
|
||||
f"os:{_REDIS_OS}\n"
|
||||
f"arch_bits:64\n"
|
||||
f"tcp_port:6379\n"
|
||||
f"uptime_in_seconds:864000\n"
|
||||
f"connected_clients:1\n"
|
||||
f"# Keyspace\n"
|
||||
).encode()
|
||||
|
||||
|
||||
def _forward(event: dict) -> None:
|
||||
@@ -41,7 +44,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "redis",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -158,7 +161,7 @@ class RedisProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"Redis honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"Redis server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(RedisProtocol, "0.0.0.0", 6379)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY sip_honeypot.py /opt/sip_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/sip_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SIP honeypot (UDP + TCP port 5060).
|
||||
SIP server (UDP + TCP port 5060).
|
||||
Parses SIP REGISTER and INVITE messages, logs credentials from the
|
||||
Authorization header and call metadata, then responds with 401 Unauthorized.
|
||||
"""
|
||||
@@ -12,7 +12,7 @@ import re
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "pbx")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "pbx")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
_401 = (
|
||||
@@ -42,7 +42,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "sip",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -92,7 +92,7 @@ def _handle_message(data: bytes, src_addr) -> bytes | None:
|
||||
to=headers.get("to", ""),
|
||||
call_id=headers.get("call-id", ""),
|
||||
cseq=headers.get("cseq", ""),
|
||||
host=HONEYPOT_NAME,
|
||||
host=NODE_NAME,
|
||||
)
|
||||
return response.encode()
|
||||
return None
|
||||
@@ -134,7 +134,7 @@ class SIPTCPProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"SIP honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"SIP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
udp_transport, _ = await loop.create_datagram_endpoint(
|
||||
SIPUDPProtocol, local_addr=("0.0.0.0", 5060)
|
||||
@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
RUN pip3 install --no-cache-dir impacket jinja2
|
||||
|
||||
COPY smb_honeypot.py /opt/smb_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
mkdir -p /tmp/smb_share
|
||||
exec python3 /opt/smb_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal SMB honeypot using Impacket's SimpleSMBServer.
|
||||
Minimal SMB server using Impacket's SimpleSMBServer.
|
||||
Logs all connection attempts, optionally forwarding them as JSON to LOG_TARGET.
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,7 @@ from datetime import datetime, timezone
|
||||
|
||||
from impacket import smbserver
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "WORKSTATION")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "WORKSTATION")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "smb",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -39,7 +39,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"SMB honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"SMB server starting as {NODE_NAME}")
|
||||
os.makedirs("/tmp/smb_share", exist_ok=True)
|
||||
|
||||
server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=445)
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY smtp_honeypot.py /opt/smtp_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/smtp_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SMTP honeypot — emulates a realistic ESMTP server (Postfix-style).
|
||||
SMTP server — emulates a realistic ESMTP server (Postfix-style).
|
||||
Logs EHLO/AUTH/MAIL FROM/RCPT TO attempts as JSON, then denies auth.
|
||||
"""
|
||||
|
||||
@@ -10,8 +10,10 @@ import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "mailserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "mailserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
_SMTP_BANNER = os.environ.get("SMTP_BANNER", f"220 {NODE_NAME} ESMTP Postfix (Debian/GNU)")
|
||||
_SMTP_MTA = os.environ.get("SMTP_MTA", NODE_NAME)
|
||||
|
||||
|
||||
def _forward(event: dict) -> None:
|
||||
@@ -29,7 +31,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "smtp",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -47,7 +49,7 @@ class SMTPProtocol(asyncio.Protocol):
|
||||
self._transport = transport
|
||||
self._peer = transport.get_extra_info("peername", ("?", 0))
|
||||
_log("connect", src=self._peer[0], src_port=self._peer[1])
|
||||
transport.write(f"220 {HONEYPOT_NAME} ESMTP Postfix (Debian/GNU)\r\n".encode())
|
||||
transport.write(f"{_SMTP_BANNER}\r\n".encode())
|
||||
|
||||
def data_received(self, data):
|
||||
self._buf += data
|
||||
@@ -62,7 +64,7 @@ class SMTPProtocol(asyncio.Protocol):
|
||||
domain = line.split(None, 1)[1] if " " in line else ""
|
||||
_log("ehlo", src=self._peer[0], domain=domain)
|
||||
self._transport.write(
|
||||
f"250-{HONEYPOT_NAME}\r\n"
|
||||
f"250-{_SMTP_MTA}\r\n"
|
||||
f"250-PIPELINING\r\n"
|
||||
f"250-SIZE 10240000\r\n"
|
||||
f"250-VRFY\r\n"
|
||||
@@ -106,7 +108,7 @@ class SMTPProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"SMTP honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"SMTP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(SMTPProtocol, "0.0.0.0", 25)
|
||||
async with server:
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY snmp_honeypot.py /opt/snmp_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/snmp_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SNMP honeypot (UDP 161).
|
||||
SNMP server (UDP 161).
|
||||
Parses SNMPv1/v2c GetRequest PDUs, logs the community string and OID list,
|
||||
then responds with a GetResponse containing plausible system OID values.
|
||||
Logs all requests as JSON.
|
||||
@@ -13,16 +13,16 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "switch")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "switch")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
# OID value map — fake but plausible
|
||||
_OID_VALUES = {
|
||||
"1.3.6.1.2.1.1.1.0": f"Linux {HONEYPOT_NAME} 5.15.0-76-generic #83-Ubuntu SMP x86_64",
|
||||
"1.3.6.1.2.1.1.1.0": f"Linux {NODE_NAME} 5.15.0-76-generic #83-Ubuntu SMP x86_64",
|
||||
"1.3.6.1.2.1.1.2.0": "1.3.6.1.4.1.8072.3.2.10",
|
||||
"1.3.6.1.2.1.1.3.0": "12345678", # sysUpTime
|
||||
"1.3.6.1.2.1.1.4.0": "admin@localhost",
|
||||
"1.3.6.1.2.1.1.5.0": HONEYPOT_NAME,
|
||||
"1.3.6.1.2.1.1.5.0": NODE_NAME,
|
||||
"1.3.6.1.2.1.1.6.0": "Server Room",
|
||||
"1.3.6.1.2.1.1.7.0": "72",
|
||||
}
|
||||
@@ -43,7 +43,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "snmp",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -180,7 +180,7 @@ class SNMPProtocol(asyncio.DatagramProtocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"SNMP honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"SNMP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
transport, _ = await loop.create_datagram_endpoint(
|
||||
SNMPProtocol, local_addr=("0.0.0.0", 161)
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY tftp_honeypot.py /opt/tftp_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/tftp_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TFTP honeypot (UDP 69).
|
||||
TFTP server (UDP 69).
|
||||
Parses RRQ (read) and WRQ (write) requests, logs filename and transfer mode,
|
||||
then responds with an error packet. Logs all requests as JSON.
|
||||
"""
|
||||
@@ -12,7 +12,7 @@ import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "tftpserver")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "tftpserver")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
# TFTP opcodes
|
||||
@@ -40,7 +40,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "tftp",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class TFTPProtocol(asyncio.DatagramProtocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"TFTP honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"TFTP server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
transport, _ = await loop.create_datagram_endpoint(
|
||||
TFTPProtocol, local_addr=("0.0.0.0", 69)
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY vnc_honeypot.py /opt/vnc_honeypot.py
|
||||
COPY server.py /opt/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
exec python3 /opt/vnc_honeypot.py
|
||||
exec python3 /opt/server.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
VNC (RFB) honeypot.
|
||||
VNC (RFB)server.
|
||||
Performs the RFB 3.8 handshake, offers VNC authentication, captures the
|
||||
24-byte DES-encrypted challenge response, then rejects with "Authentication
|
||||
failed". Logs the raw response for offline cracking.
|
||||
@@ -12,7 +12,7 @@ import os
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
|
||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "desktop")
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "desktop")
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
|
||||
# RFB challenge — fixed so captured responses are reproducible
|
||||
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
|
||||
event = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"service": "vnc",
|
||||
"host": HONEYPOT_NAME,
|
||||
"host": NODE_NAME,
|
||||
"event": event_type,
|
||||
**kwargs,
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class VNCProtocol(asyncio.Protocol):
|
||||
|
||||
|
||||
async def main():
|
||||
_log("startup", msg=f"VNC honeypot starting as {HONEYPOT_NAME}")
|
||||
_log("startup", msg=f"VNC server starting as {NODE_NAME}")
|
||||
loop = asyncio.get_running_loop()
|
||||
server = await loop.create_server(VNCProtocol, "0.0.0.0", 5900)
|
||||
async with server:
|
||||
@@ -20,13 +20,13 @@ APT_COMPATIBLE = {
|
||||
}
|
||||
|
||||
BUILD_SERVICES = [
|
||||
"http", "rdp", "smb", "ftp", "smtp", "elasticsearch",
|
||||
"ssh", "http", "rdp", "smb", "ftp", "smtp", "elasticsearch",
|
||||
"pop3", "imap", "mysql", "mssql", "redis", "mongodb", "postgres",
|
||||
"ldap", "vnc", "docker_api", "k8s", "sip",
|
||||
"mqtt", "llmnr", "snmp", "tftp",
|
||||
]
|
||||
|
||||
UPSTREAM_SERVICES = ["ssh", "telnet", "conpot"]
|
||||
UPSTREAM_SERVICES = ["telnet", "conpot"]
|
||||
|
||||
|
||||
def _make_config(services, distro="debian", base_image=None, build_base=None):
|
||||
@@ -95,6 +95,86 @@ def test_upstream_service_has_no_build_section(svc):
|
||||
assert "image" in fragment
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# service_config propagation tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_service_config_http_server_header():
|
||||
"""service_config for http must inject SERVER_HEADER into compose env."""
|
||||
from decnet.config import DeckyConfig, DecnetConfig
|
||||
from decnet.distros import DISTROS
|
||||
profile = DISTROS["debian"]
|
||||
decky = DeckyConfig(
|
||||
name="decky-01", ip="10.0.0.10",
|
||||
services=["http"], distro="debian",
|
||||
base_image=profile.image, build_base=profile.build_base,
|
||||
hostname="test-host",
|
||||
service_config={"http": {"server_header": "nginx/1.18.0"}},
|
||||
)
|
||||
config = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=[decky],
|
||||
)
|
||||
compose = generate_compose(config)
|
||||
env = compose["services"]["decky-01-http"]["environment"]
|
||||
assert env.get("SERVER_HEADER") == "nginx/1.18.0"
|
||||
|
||||
|
||||
def test_service_config_ssh_kernel_version():
|
||||
"""service_config for ssh must inject COWRIE_HONEYPOT_KERNEL_VERSION."""
|
||||
from decnet.config import DeckyConfig, DecnetConfig
|
||||
from decnet.distros import DISTROS
|
||||
profile = DISTROS["debian"]
|
||||
decky = DeckyConfig(
|
||||
name="decky-01", ip="10.0.0.10",
|
||||
services=["ssh"], distro="debian",
|
||||
base_image=profile.image, build_base=profile.build_base,
|
||||
hostname="test-host",
|
||||
service_config={"ssh": {"kernel_version": "5.15.0-76-generic"}},
|
||||
)
|
||||
config = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=[decky],
|
||||
)
|
||||
compose = generate_compose(config)
|
||||
env = compose["services"]["decky-01-ssh"]["environment"]
|
||||
assert env.get("COWRIE_HONEYPOT_KERNEL_VERSION") == "5.15.0-76-generic"
|
||||
|
||||
|
||||
def test_service_config_for_one_service_does_not_affect_another():
|
||||
"""service_config for http must not bleed into ftp fragment."""
|
||||
from decnet.config import DeckyConfig, DecnetConfig
|
||||
from decnet.distros import DISTROS
|
||||
profile = DISTROS["debian"]
|
||||
decky = DeckyConfig(
|
||||
name="decky-01", ip="10.0.0.10",
|
||||
services=["http", "ftp"], distro="debian",
|
||||
base_image=profile.image, build_base=profile.build_base,
|
||||
hostname="test-host",
|
||||
service_config={"http": {"server_header": "nginx/1.18.0"}},
|
||||
)
|
||||
config = DecnetConfig(
|
||||
mode="unihost", interface="eth0",
|
||||
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||
deckies=[decky],
|
||||
)
|
||||
compose = generate_compose(config)
|
||||
ftp_env = compose["services"]["decky-01-ftp"]["environment"]
|
||||
assert "SERVER_HEADER" not in ftp_env
|
||||
|
||||
|
||||
def test_no_service_config_produces_no_extra_env():
|
||||
"""A decky with no service_config must not have new persona env vars."""
|
||||
config = _make_config(["http", "mysql"])
|
||||
compose = generate_compose(config)
|
||||
for svc in ("http", "mysql"):
|
||||
env = compose["services"][f"decky-01-{svc}"]["environment"]
|
||||
assert "SERVER_HEADER" not in env
|
||||
assert "MYSQL_VERSION" not in env
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base container uses distro image, not build_base
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user