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:
2026-04-04 04:08:27 -03:00
parent 07c06e3c0a
commit cf1e00af28
102 changed files with 974 additions and 309 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"mcp__plugin_context-mode_context-mode__ctx_batch_execute"
]
}
}

View File

@@ -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. - The logging/aggregation network must be isolated from the decoy network.
- A publicly accessible real server acts as the bridge between the two networks. - 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. - 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.

View File

@@ -168,6 +168,7 @@ def _build_deckies_from_ini(
base_image=distro.image, base_image=distro.image,
build_base=distro.build_base, build_base=distro.build_base,
hostname=hostname, hostname=hostname,
service_config=spec.service_config,
)) ))
return deckies return deckies
@@ -217,6 +218,20 @@ def deploy(
f"[dim]Subnet:[/] {subnet_cidr} [dim]Gateway:[/] {effective_gateway} " f"[dim]Subnet:[/] {subnet_cidr} [dim]Gateway:[/] {effective_gateway} "
f"[dim]Host IP:[/] {host_ip}") 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 effective_log_target = log_target or ini.log_target
decky_configs = _build_deckies_from_ini( decky_configs = _build_deckies_from_ini(
ini, subnet_cidr, effective_gateway, host_ip, randomize_services ini, subnet_cidr, effective_gateway, host_ip, randomize_services

View File

@@ -46,7 +46,10 @@ def generate_compose(config: DecnetConfig) -> dict:
# --- Service containers: share base network namespace --- # --- Service containers: share base network namespace ---
for svc_name in decky.services: for svc_name in decky.services:
svc = get_service(svc_name) 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 # Inject the per-decky base image into build services so containers
# vary by distro and don't all fingerprint as debian:bookworm-slim. # vary by distro and don't all fingerprint as debian:bookworm-slim.

View File

@@ -26,6 +26,7 @@ class DeckyConfig(BaseModel):
base_image: str # Docker image for the base/IP-holder container base_image: str # Docker image for the base/IP-holder container
build_base: str = "debian:bookworm-slim" # apt-compatible image for service Dockerfiles build_base: str = "debian:bookworm-slim" # apt-compatible image for service Dockerfiles
hostname: str hostname: str
service_config: dict[str, dict] = {} # optional per-service persona config
@field_validator("services") @field_validator("services")
@classmethod @classmethod

41
decnet/custom_service.py Normal file
View 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

View File

@@ -12,11 +12,26 @@ Format:
ip=192.168.1.82 # optional ip=192.168.1.82 # optional
services=ssh,smb # optional; falls back to --randomize-services 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] [hostname-2]
services=ssh services=ssh
[hostname-3] [hostname-3]
ip=192.168.1.32 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 import configparser
@@ -29,6 +44,16 @@ class DeckySpec:
name: str name: str
ip: str | None = None ip: str | None = None
services: list[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 @dataclass
@@ -38,6 +63,7 @@ class IniConfig:
interface: str | None = None interface: str | None = None
log_target: str | None = None log_target: str | None = None
deckies: list[DeckySpec] = field(default_factory=list) deckies: list[DeckySpec] = field(default_factory=list)
custom_services: list[CustomServiceSpec] = field(default_factory=list)
def load_ini(path: str | Path) -> IniConfig: def load_ini(path: str | Path) -> IniConfig:
@@ -56,13 +82,40 @@ def load_ini(path: str | Path) -> IniConfig:
cfg.interface = g.get("interface") cfg.interface = g.get("interface")
cfg.log_target = g.get("log_target") or g.get("log-target") 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(): for section in cp.sections():
if section == "general": if section == "general":
continue 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] s = cp[section]
ip = s.get("ip") ip = s.get("ip")
svc_raw = s.get("services") svc_raw = s.get("services")
services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None services = [sv.strip() for sv in svc_raw.split(",")] if svc_raw else None
cfg.deckies.append(DeckySpec(name=section, ip=ip, services=services)) 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 return cfg

View File

@@ -15,7 +15,12 @@ class BaseService(ABC):
default_image: str # Docker image tag, or "build" if a Dockerfile is needed default_image: str # Docker image tag, or "build" if a Dockerfile is needed
@abstractmethod @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. Return the docker-compose service dict for this service on a given decky.
@@ -26,6 +31,7 @@ class BaseService(ABC):
Args: Args:
decky_name: unique identifier for the decky (e.g. "decky-01") decky_name: unique identifier for the decky (e.g. "decky-01")
log_target: "ip:port" string if log forwarding is enabled, else None 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: def dockerfile_context(self) -> Path | None:

View File

@@ -12,7 +12,7 @@ class ConpotService(BaseService):
ports = [502, 161, 80] ports = [502, 161, 80]
default_image = "honeynet/conpot" 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 { return {
"image": "honeynet/conpot", "image": "honeynet/conpot",
"container_name": f"{decky_name}-conpot", "container_name": f"{decky_name}-conpot",

View File

@@ -9,12 +9,12 @@ class DockerAPIService(BaseService):
ports = [2375, 2376] ports = [2375, 2376]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-docker-api", "container_name": f"{decky_name}-docker-api",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -10,13 +10,13 @@ class ElasticsearchService(BaseService):
ports = [9200] ports = [9200]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-elasticsearch", "container_name": f"{decky_name}-elasticsearch",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": { "environment": {
"HONEYPOT_NAME": decky_name, "NODE_NAME": decky_name,
}, },
} }
if log_target: if log_target:

View File

@@ -9,13 +9,13 @@ class FTPService(BaseService):
ports = [21] ports = [21]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ftp", "container_name": f"{decky_name}-ftp",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": { "environment": {
"HONEYPOT_NAME": decky_name, "NODE_NAME": decky_name,
}, },
} }
if log_target: if log_target:

View File

@@ -1,3 +1,4 @@
import json
from pathlib import Path from pathlib import Path
from decnet.services.base import BaseService from decnet.services.base import BaseService
@@ -9,17 +10,43 @@ class HTTPService(BaseService):
ports = [80, 443] ports = [80, 443]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-http", "container_name": f"{decky_name}-http",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": { "environment": {
"HONEYPOT_NAME": decky_name, "NODE_NAME": decky_name,
}, },
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = 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 return fragment
def dockerfile_context(self) -> Path | None: def dockerfile_context(self) -> Path | None:

View File

@@ -9,12 +9,12 @@ class IMAPService(BaseService):
ports = [143, 993] ports = [143, 993]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-imap", "container_name": f"{decky_name}-imap",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class KubernetesAPIService(BaseService):
ports = [6443, 8080] ports = [6443, 8080]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-k8s", "container_name": f"{decky_name}-k8s",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,13 +9,13 @@ class LDAPService(BaseService):
ports = [389, 636] ports = [389, 636]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ldap", "container_name": f"{decky_name}-ldap",
"restart": "unless-stopped", "restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"], "cap_add": ["NET_BIND_SERVICE"],
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -16,12 +16,12 @@ class LLMNRService(BaseService):
ports = [5355, 5353] ports = [5355, 5353]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-llmnr", "container_name": f"{decky_name}-llmnr",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class MongoDBService(BaseService):
ports = [27017] ports = [27017]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mongodb", "container_name": f"{decky_name}-mongodb",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class MQTTService(BaseService):
ports = [1883] ports = [1883]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mqtt", "container_name": f"{decky_name}-mqtt",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class MSSQLService(BaseService):
ports = [1433] ports = [1433]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mssql", "container_name": f"{decky_name}-mssql",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,15 +9,23 @@ class MySQLService(BaseService):
ports = [3306] ports = [3306]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-mysql", "container_name": f"{decky_name}-mysql",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target
if "version" in cfg:
fragment["environment"]["MYSQL_VERSION"] = cfg["version"]
return fragment return fragment
def dockerfile_context(self) -> Path | None: def dockerfile_context(self) -> Path | None:

View File

@@ -9,12 +9,12 @@ class POP3Service(BaseService):
ports = [110, 995] ports = [110, 995]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-pop3", "container_name": f"{decky_name}-pop3",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class PostgresService(BaseService):
ports = [5432] ports = [5432]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-postgres", "container_name": f"{decky_name}-postgres",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,13 +9,13 @@ class RDPService(BaseService):
ports = [3389] ports = [3389]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-rdp", "container_name": f"{decky_name}-rdp",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": { "environment": {
"HONEYPOT_NAME": decky_name, "NODE_NAME": decky_name,
}, },
} }
if log_target: if log_target:

View File

@@ -9,15 +9,25 @@ class RedisService(BaseService):
ports = [6379] ports = [6379]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-redis", "container_name": f"{decky_name}-redis",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = 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 return fragment
def dockerfile_context(self) -> Path | None: def dockerfile_context(self) -> Path | None:

View File

@@ -31,6 +31,12 @@ def _load_plugins() -> None:
_loaded = True _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: def get_service(name: str) -> BaseService:
_load_plugins() _load_plugins()
if name not in _registry: if name not in _registry:

View File

@@ -9,12 +9,12 @@ class SIPService(BaseService):
ports = [5060] ports = [5060]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-sip", "container_name": f"{decky_name}-sip",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,14 +9,14 @@ class SMBService(BaseService):
ports = [445, 139] ports = [445, 139]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-smb", "container_name": f"{decky_name}-smb",
"restart": "unless-stopped", "restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"], "cap_add": ["NET_BIND_SERVICE"],
"environment": { "environment": {
"HONEYPOT_NAME": decky_name, "NODE_NAME": decky_name,
}, },
} }
if log_target: if log_target:

View File

@@ -10,18 +10,28 @@ class SMTPService(BaseService):
ports = [25, 587] ports = [25, 587]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-smtp", "container_name": f"{decky_name}-smtp",
"restart": "unless-stopped", "restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"], "cap_add": ["NET_BIND_SERVICE"],
"environment": { "environment": {
"HONEYPOT_NAME": decky_name, "NODE_NAME": decky_name,
}, },
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = 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 return fragment
def dockerfile_context(self) -> Path: def dockerfile_context(self) -> Path:

View File

@@ -9,12 +9,12 @@ class SNMPService(BaseService):
ports = [161] ports = [161]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-snmp", "container_name": f"{decky_name}-snmp",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -1,15 +1,24 @@
from pathlib import Path
from decnet.services.base import BaseService from decnet.services.base import BaseService
TEMPLATES_DIR = Path(__file__).parent.parent.parent / "templates" / "cowrie"
class SSHService(BaseService): class SSHService(BaseService):
name = "ssh" name = "ssh"
ports = [22, 2222] 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 = { env: dict = {
# Override [honeypot] and [ssh] listen_endpoints to also bind port 22 "NODE_NAME": decky_name,
"COWRIE_HONEYPOT_HOSTNAME": 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_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", "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_ENABLED"] = "true"
env["COWRIE_OUTPUT_TCP_HOST"] = host env["COWRIE_OUTPUT_TCP_HOST"] = host
env["COWRIE_OUTPUT_TCP_PORT"] = port 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 { return {
"image": "cowrie/cowrie", "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-ssh", "container_name": f"{decky_name}-ssh",
"restart": "unless-stopped", "restart": "unless-stopped",
"cap_add": ["NET_BIND_SERVICE"], "cap_add": ["NET_BIND_SERVICE"],
"environment": env, "environment": env,
} }
def dockerfile_context(self): def dockerfile_context(self) -> Path:
return None return TEMPLATES_DIR

View File

@@ -6,7 +6,7 @@ class TelnetService(BaseService):
ports = [23] ports = [23]
default_image = "cowrie/cowrie" 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 = { env: dict = {
"COWRIE_HONEYPOT_HOSTNAME": decky_name, "COWRIE_HONEYPOT_HOSTNAME": decky_name,
"COWRIE_TELNET_ENABLED": "true", "COWRIE_TELNET_ENABLED": "true",

View File

@@ -9,12 +9,12 @@ class TFTPService(BaseService):
ports = [69] ports = [69]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-tftp", "container_name": f"{decky_name}-tftp",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -9,12 +9,12 @@ class VNCService(BaseService):
ports = [5900] ports = [5900]
default_image = "build" 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 = { fragment: dict = {
"build": {"context": str(TEMPLATES_DIR)}, "build": {"context": str(TEMPLATES_DIR)},
"container_name": f"{decky_name}-vnc", "container_name": f"{decky_name}-vnc",
"restart": "unless-stopped", "restart": "unless-stopped",
"environment": {"HONEYPOT_NAME": decky_name}, "environment": {"NODE_NAME": decky_name},
} }
if log_target: if log_target:
fragment["environment"]["LOG_TARGET"] = log_target fragment["environment"]["LOG_TARGET"] = log_target

View File

@@ -1,10 +1,14 @@
[honeypot] [honeypot]
hostname = {{ COWRIE_HOSTNAME | default('svr01') }} hostname = {{ COWRIE_HOSTNAME | default('svr01') }}
listen_endpoints = tcp:2222:interface=0.0.0.0 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] [ssh]
enabled = true enabled = true
listen_endpoints = tcp:2222:interface=0.0.0.0 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 %} {% if COWRIE_LOG_HOST is defined and COWRIE_LOG_HOST %}
[output_jsonlog] [output_jsonlog]

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Render Jinja2 template using the venv's python (has jinja2) # Render Jinja2 config template
/home/cowrie/cowrie-env/bin/python3 - <<'EOF' /home/cowrie/cowrie-env/bin/python3 - <<'EOF'
import os import os
from jinja2 import Template from jinja2 import Template
@@ -15,4 +15,19 @@ with open("/home/cowrie/cowrie-env/etc/cowrie.cfg", "w") as f:
f.write(rendered) f.write(rendered)
EOF 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 exec authbind --deep /home/cowrie/cowrie-env/bin/twistd -n --pidfile= cowrie

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1 ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir flask 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/docker_api_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Docker API honeypot. Docker APIserver.
Serves a fake Docker REST API on port 2375. Responds to common recon Serves a fake Docker REST API on port 2375. Responds to common recon
endpoints (/version, /info, /containers/json, /images/json) with plausible endpoints (/version, /info, /containers/json, /images/json) with plausible
but fake data. Logs all requests as JSON. but fake data. Logs all requests as JSON.
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
from flask import Flask, request 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
app = Flask(__name__) app = Flask(__name__)
@@ -38,7 +38,7 @@ _INFO = {
"MemoryLimit": True, "MemoryLimit": True,
"SwapLimit": True, "SwapLimit": True,
"KernelMemory": False, "KernelMemory": False,
"Name": HONEYPOT_NAME, "Name": NODE_NAME,
"DockerRootDir": "/var/lib/docker", "DockerRootDir": "/var/lib/docker",
"HttpProxy": "", "HttpProxy": "",
"HttpsProxy": "", "HttpsProxy": "",
@@ -73,7 +73,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "docker_api", "service": "docker_api",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -127,5 +127,5 @@ def catch_all(path):
if __name__ == "__main__": 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) app.run(host="0.0.0.0", port=2375, debug=False)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/elasticsearch_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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/) Logs all requests (especially recon probes like /_cat/, /_cluster/, /_nodes/)
as JSON. Designed to attract automated scanners and credential stuffers. as JSON. Designed to attract automated scanners and credential stuffers.
""" """
@@ -11,14 +11,14 @@ import socket
from datetime import datetime, timezone from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
_CLUSTER_UUID = "xC3Pr9abTq2mNkOeLvXwYA" _CLUSTER_UUID = "xC3Pr9abTq2mNkOeLvXwYA"
_NODE_UUID = "dJH7Lm2sRqWvPn0kFiEtBo" _NODE_UUID = "dJH7Lm2sRqWvPn0kFiEtBo"
_ROOT_RESPONSE = { _ROOT_RESPONSE = {
"name": HONEYPOT_NAME, "name": NODE_NAME,
"cluster_name": "elasticsearch", "cluster_name": "elasticsearch",
"cluster_uuid": _CLUSTER_UUID, "cluster_uuid": _CLUSTER_UUID,
"version": { "version": {
@@ -51,7 +51,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "elasticsearch", "service": "elasticsearch",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -110,7 +110,7 @@ class ESHandler(BaseHTTPRequestHandler):
if "_search" in path or "_bulk" in path: if "_search" in path or "_bulk" in path:
self._send_json(200, {"took": 1, "timed_out": False, "hits": {"total": {"value": 0}, "hits": []}}) self._send_json(200, {"took": 1, "timed_out": False, "hits": {"total": {"value": 0}, "hits": []}})
else: 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): def do_PUT(self):
src = self.client_address[0] src = self.client_address[0]
@@ -133,6 +133,6 @@ class ESHandler(BaseHTTPRequestHandler):
if __name__ == "__main__": 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 = HTTPServer(("0.0.0.0", 9200), ESHandler)
server.serve_forever() server.serve_forever()

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1 ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir twisted jinja2 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/ftp_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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, Accepts any credentials, logs all commands and file requests,
forwards events as JSON to LOG_TARGET if set. 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.protocols.ftp import FTP, FTPFactory
from twisted.python import log as twisted_log 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "ftp", "service": "ftp",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -42,22 +42,22 @@ def _log(event_type: str, **kwargs) -> None:
_forward(event) _forward(event)
class HoneypotFTP(FTP): class ServerFTP(FTP):
def connectionMade(self): def connectionMade(self):
peer = self.transport.getPeer() peer = self.transport.getPeer()
_log("connection", src_ip=peer.host, src_port=peer.port) _log("connection", src_ip=peer.host, src_port=peer.port)
super().connectionMade() super().connectionMade()
def ftp_USER(self, username): def ftp_USER(self, username):
self._honeypot_user = username self._server_user = username
_log("user", username=username) _log("user", username=username)
return super().ftp_USER(username) return super().ftp_USER(username)
def ftp_PASS(self, password): def ftp_PASS(self, password):
_log("auth_attempt", username=getattr(self, "_honeypot_user", "?"), password=password) _log("auth_attempt", username=getattr(self, "_server_user", "?"), password=password)
# Accept everything — we're a honeypot # Accept everything — we're a server
self.state = self.AUTHED self.state = self.AUTHED
self._user = getattr(self, "_honeypot_user", "anonymous") self._user = getattr(self, "_server_user", "anonymous")
return defer.succeed((230, "Login successful.")) return defer.succeed((230, "Login successful."))
def ftp_RETR(self, path): def ftp_RETR(self, path):
@@ -71,12 +71,12 @@ class HoneypotFTP(FTP):
super().connectionLost(reason) super().connectionLost(reason)
class HoneypotFTPFactory(FTPFactory): class ServerFTPFactory(FTPFactory):
protocol = HoneypotFTP protocol = ServerFTP
if __name__ == "__main__": if __name__ == "__main__":
twisted_log.startLogging(sys.stdout) twisted_log.startLogging(sys.stdout)
_log("startup", msg=f"FTP honeypot starting as {HONEYPOT_NAME} on port 21") _log("startup", msg=f"FTP server starting as {NODE_NAME} on port 21")
reactor.listenTCP(21, HoneypotFTPFactory()) reactor.listenTCP(21, ServerFTPFactory())
reactor.run() reactor.run()

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1 ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir flask jinja2 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/http_honeypot.py exec python3 /opt/server.py

View File

@@ -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
View 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 &rsaquo; 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)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/imap_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
IMAP honeypot. IMAPserver.
Presents an IMAP4rev1 banner, captures LOGIN credentials (plaintext and Presents an IMAP4rev1 banner, captures LOGIN credentials (plaintext and
AUTHENTICATE), then returns a NO response. Logs all commands as JSON. AUTHENTICATE), then returns a NO response. Logs all commands as JSON.
""" """
@@ -11,9 +11,9 @@ import os
import socket import socket
from datetime import datetime, timezone 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", "") 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: def _forward(event: dict) -> None:
@@ -31,7 +31,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "imap", "service": "imap",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -87,7 +87,7 @@ class IMAPProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(IMAPProtocol, "0.0.0.0", 143) server = await loop.create_server(IMAPProtocol, "0.0.0.0", 143)
async with server: async with server:

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Kubernetes API honeypot. Kubernetes APIserver.
Serves a fake K8s REST API on port 6443 (HTTPS-ish, plain HTTP) and 8080. 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, Responds to recon endpoints (/version, /api, /apis, /api/v1/namespaces,
/api/v1/pods) with plausible but fake data. Logs all requests as JSON. /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 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
app = Flask(__name__) app = Flask(__name__)
@@ -33,7 +33,7 @@ _VERSION = {
_API_VERSIONS = { _API_VERSIONS = {
"kind": "APIVersions", "kind": "APIVersions",
"versions": ["v1"], "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 = { _NAMESPACES = {
@@ -80,7 +80,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "k8s", "service": "k8s",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -138,5 +138,5 @@ def catch_all(path):
if __name__ == "__main__": 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) app.run(host="0.0.0.0", port=6443, debug=False)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/ldap_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
LDAP honeypot. LDAPserver.
Parses BER-encoded BindRequest messages, logs DN and password, returns an Parses BER-encoded BindRequest messages, logs DN and password, returns an
invalidCredentials error. Logs all interactions as JSON. invalidCredentials error. Logs all interactions as JSON.
""" """
@@ -11,7 +11,7 @@ import os
import socket import socket
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -30,7 +30,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "ldap", "service": "ldap",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -154,7 +154,7 @@ class LDAPProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(LDAPProtocol, "0.0.0.0", 389) server = await loop.create_server(LDAPProtocol, "0.0.0.0", 389)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/llmnr_honeypot.py exec python3 /opt/server.py

View File

@@ -13,7 +13,7 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -32,7 +32,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "llmnr", "service": "llmnr",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -104,7 +104,7 @@ class LLMNRProtocol(asyncio.DatagramProtocol):
async def main(): 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() loop = asyncio.get_running_loop()
# LLMNR: UDP 5355 # LLMNR: UDP 5355

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/mongodb_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
MongoDB honeypot. MongoDBserver.
Implements the MongoDB wire protocol OP_MSG/OP_QUERY handshake. Responds Implements the MongoDB wire protocol OP_MSG/OP_QUERY handshake. Responds
to isMaster/hello, listDatabases, and authenticate commands. Logs all to isMaster/hello, listDatabases, and authenticate commands. Logs all
received messages as JSON. received messages as JSON.
@@ -13,7 +13,7 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
# Minimal BSON helpers # Minimal BSON helpers
@@ -64,7 +64,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "mongodb", "service": "mongodb",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -115,7 +115,7 @@ class MongoDBProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(MongoDBProtocol, "0.0.0.0", 27017) server = await loop.create_server(MongoDBProtocol, "0.0.0.0", 27017)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/mqtt_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
MQTT honeypot (port 1883). MQTT server (port 1883).
Parses MQTT CONNECT packets, extracts client_id, username, and password, Parses MQTT CONNECT packets, extracts client_id, username, and password,
then returns CONNACK with return code 5 (not authorized). Logs all then returns CONNACK with return code 5 (not authorized). Logs all
interactions as JSON. interactions as JSON.
@@ -13,7 +13,7 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
# CONNACK: packet type 0x20, remaining length 2, session_present=0, return_code=5 # 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 = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "mqtt", "service": "mqtt",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -137,7 +137,7 @@ class MQTTProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(MQTTProtocol, "0.0.0.0", 1883) server = await loop.create_server(MQTTProtocol, "0.0.0.0", 1883)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/mssql_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
MSSQL (TDS) honeypot. MSSQL (TDS)server.
Reads TDS pre-login and login7 packets, extracts username, responds with Reads TDS pre-login and login7 packets, extracts username, responds with
a login failed error. Logs auth attempts as JSON. a login failed error. Logs auth attempts as JSON.
""" """
@@ -12,7 +12,7 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
# Minimal TDS pre-login response # Minimal TDS pre-login response
@@ -54,7 +54,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "mssql", "service": "mssql",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -137,7 +137,7 @@ class MSSQLProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(MSSQLProtocol, "0.0.0.0", 1433) server = await loop.create_server(MSSQLProtocol, "0.0.0.0", 1433)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/mysql_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
MySQL honeypot. MySQLserver.
Sends a realistic MySQL 5.7 server handshake, reads the client login Sends a realistic MySQL 5.7 server handshake, reads the client login
packet, extracts username, then closes with Access Denied. Logs auth packet, extracts username, then closes with Access Denied. Logs auth
attempts as JSON. attempts as JSON.
@@ -13,24 +13,25 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") 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 = ( _GREETING = (
b"\x0a" # protocol version 10 b"\x0a" # protocol version 10
b"5.7.38-honeypot\x00" # server version + NUL + _MYSQL_VER.encode() + b"\x00" # server version + NUL
b"\x01\x00\x00\x00" # connection id = 1 + b"\x01\x00\x00\x00" # connection id = 1
b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1 + b"\x70\x76\x21\x6d\x61\x67\x69\x63" # auth-plugin-data part 1
b"\x00" # filler + b"\x00" # filler
b"\xff\xf7" # capability flags low + b"\xff\xf7" # capability flags low
b"\x21" # charset utf8 + b"\x21" # charset utf8
b"\x02\x00" # status flags + b"\x02\x00" # status flags
b"\xff\x81" # capability flags high + b"\xff\x81" # capability flags high
b"\x15" # auth plugin data length + b"\x15" # auth plugin data length
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # reserved (10 bytes) + 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"\x21\x4f\x7d\x25\x3e\x55\x4d\x7c\x67\x75\x5e\x31\x00" # auth part 2
b"mysql_native_password\x00" # auth plugin name + b"mysql_native_password\x00" # auth plugin name
) )
@@ -54,7 +55,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "mysql", "service": "mysql",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -110,7 +111,7 @@ class MySQLProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(MySQLProtocol, "0.0.0.0", 3306) server = await loop.create_server(MySQLProtocol, "0.0.0.0", 3306)
async with server: async with server:

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
POP3 honeypot. POP3server.
Presents a convincing POP3 banner, collects USER/PASS credentials, then Presents a convincing POP3 banner, collects USER/PASS credentials, then
stalls with a generic error. Logs every interaction as JSON and forwards stalls with a generic error. Logs every interaction as JSON and forwards
to LOG_TARGET if set. to LOG_TARGET if set.
@@ -12,9 +12,9 @@ import os
import socket import socket
from datetime import datetime, timezone 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", "") 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: def _forward(event: dict) -> None:
@@ -32,7 +32,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "pop3", "service": "pop3",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -83,7 +83,7 @@ class POP3Protocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(POP3Protocol, "0.0.0.0", 110) server = await loop.create_server(POP3Protocol, "0.0.0.0", 110)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/postgres_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
PostgreSQL honeypot. PostgreSQLserver.
Reads the startup message, extracts username and database, responds with Reads the startup message, extracts username and database, responds with
an AuthenticationMD5Password challenge, logs the hash sent back, then an AuthenticationMD5Password challenge, logs the hash sent back, then
returns an error. Logs all interactions as JSON. returns an error. Logs all interactions as JSON.
@@ -13,7 +13,7 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
SALT = b"\xde\xad\xbe\xef" SALT = b"\xde\xad\xbe\xef"
@@ -40,7 +40,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "postgres", "service": "postgres",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -118,7 +118,7 @@ class PostgresProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(PostgresProtocol, "0.0.0.0", 5432) server = await loop.create_server(PostgresProtocol, "0.0.0.0", 5432)
async with server: async with server:

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1 ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir twisted jinja2 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/rdp_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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 Listens on port 3389, logs connection attempts and any credentials sent
in the initial RDP negotiation request. Forwards events as JSON to in the initial RDP negotiation request. Forwards events as JSON to
LOG_TARGET if set. LOG_TARGET if set.
@@ -15,7 +15,7 @@ from datetime import datetime, timezone
from twisted.internet import protocol, reactor from twisted.internet import protocol, reactor
from twisted.python import log as twisted_log 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "rdp", "service": "rdp",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -42,7 +42,7 @@ def _log(event_type: str, **kwargs) -> None:
_forward(event) _forward(event)
class RDPHoneypotProtocol(protocol.Protocol): class RDPServerProtocol(protocol.Protocol):
def connectionMade(self): def connectionMade(self):
peer = self.transport.getPeer() peer = self.transport.getPeer()
_log("connection", src_ip=peer.host, src_port=peer.port) _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) _log("disconnect", src_ip=peer.host, src_port=peer.port)
class RDPHoneypotFactory(protocol.ServerFactory): class RDPServerFactory(protocol.ServerFactory):
protocol = RDPHoneypotProtocol protocol = RDPServerProtocol
if __name__ == "__main__": if __name__ == "__main__":
twisted_log.startLogging(sys.stdout) twisted_log.startLogging(sys.stdout)
_log("startup", msg=f"RDP honeypot starting as {HONEYPOT_NAME} on port 3389") _log("startup", msg=f"RDP server starting as {NODE_NAME} on port 3389")
reactor.listenTCP(3389, RDPHoneypotFactory()) reactor.listenTCP(3389, RDPServerFactory())
reactor.run() reactor.run()

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/redis_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Redis honeypot. Redisserver.
Implements enough of the RESP protocol to respond to AUTH, INFO, CONFIG GET, Implements enough of the RESP protocol to respond to AUTH, INFO, CONFIG GET,
KEYS, and arbitrary commands. Logs every command and argument as JSON. KEYS, and arbitrary commands. Logs every command and argument as JSON.
""" """
@@ -11,19 +11,22 @@ import os
import socket import socket
from datetime import datetime, timezone 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", "") 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 _INFO = (
redis_version:7.0.12 f"# Server\n"
redis_mode:standalone f"redis_version:{_REDIS_VER}\n"
os:Linux 5.15.0 f"redis_mode:standalone\n"
arch_bits:64 f"os:{_REDIS_OS}\n"
tcp_port:6379 f"arch_bits:64\n"
uptime_in_seconds:864000 f"tcp_port:6379\n"
connected_clients:1 f"uptime_in_seconds:864000\n"
# Keyspace f"connected_clients:1\n"
""".encode() f"# Keyspace\n"
).encode()
def _forward(event: dict) -> None: def _forward(event: dict) -> None:
@@ -41,7 +44,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "redis", "service": "redis",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -158,7 +161,7 @@ class RedisProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(RedisProtocol, "0.0.0.0", 6379) server = await loop.create_server(RedisProtocol, "0.0.0.0", 6379)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/sip_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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 Parses SIP REGISTER and INVITE messages, logs credentials from the
Authorization header and call metadata, then responds with 401 Unauthorized. Authorization header and call metadata, then responds with 401 Unauthorized.
""" """
@@ -12,7 +12,7 @@ import re
import socket import socket
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
_401 = ( _401 = (
@@ -42,7 +42,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "sip", "service": "sip",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -92,7 +92,7 @@ def _handle_message(data: bytes, src_addr) -> bytes | None:
to=headers.get("to", ""), to=headers.get("to", ""),
call_id=headers.get("call-id", ""), call_id=headers.get("call-id", ""),
cseq=headers.get("cseq", ""), cseq=headers.get("cseq", ""),
host=HONEYPOT_NAME, host=NODE_NAME,
) )
return response.encode() return response.encode()
return None return None
@@ -134,7 +134,7 @@ class SIPTCPProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
udp_transport, _ = await loop.create_datagram_endpoint( udp_transport, _ = await loop.create_datagram_endpoint(
SIPUDPProtocol, local_addr=("0.0.0.0", 5060) SIPUDPProtocol, local_addr=("0.0.0.0", 5060)

View File

@@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ENV PIP_BREAK_SYSTEM_PACKAGES=1 ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN pip3 install --no-cache-dir impacket jinja2 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
set -e set -e
mkdir -p /tmp/smb_share mkdir -p /tmp/smb_share
exec python3 /opt/smb_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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. 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 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
@@ -30,7 +30,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "smb", "service": "smb",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -39,7 +39,7 @@ def _log(event_type: str, **kwargs) -> None:
if __name__ == "__main__": 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) os.makedirs("/tmp/smb_share", exist_ok=True)
server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=445) server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=445)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/smtp_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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. Logs EHLO/AUTH/MAIL FROM/RCPT TO attempts as JSON, then denies auth.
""" """
@@ -10,8 +10,10 @@ import os
import socket import socket
from datetime import datetime, timezone 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", "") 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: def _forward(event: dict) -> None:
@@ -29,7 +31,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "smtp", "service": "smtp",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -47,7 +49,7 @@ class SMTPProtocol(asyncio.Protocol):
self._transport = transport self._transport = transport
self._peer = transport.get_extra_info("peername", ("?", 0)) self._peer = transport.get_extra_info("peername", ("?", 0))
_log("connect", src=self._peer[0], src_port=self._peer[1]) _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): def data_received(self, data):
self._buf += data self._buf += data
@@ -62,7 +64,7 @@ class SMTPProtocol(asyncio.Protocol):
domain = line.split(None, 1)[1] if " " in line else "" domain = line.split(None, 1)[1] if " " in line else ""
_log("ehlo", src=self._peer[0], domain=domain) _log("ehlo", src=self._peer[0], domain=domain)
self._transport.write( self._transport.write(
f"250-{HONEYPOT_NAME}\r\n" f"250-{_SMTP_MTA}\r\n"
f"250-PIPELINING\r\n" f"250-PIPELINING\r\n"
f"250-SIZE 10240000\r\n" f"250-SIZE 10240000\r\n"
f"250-VRFY\r\n" f"250-VRFY\r\n"
@@ -106,7 +108,7 @@ class SMTPProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(SMTPProtocol, "0.0.0.0", 25) server = await loop.create_server(SMTPProtocol, "0.0.0.0", 25)
async with server: async with server:

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/snmp_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
SNMP honeypot (UDP 161). SNMP server (UDP 161).
Parses SNMPv1/v2c GetRequest PDUs, logs the community string and OID list, Parses SNMPv1/v2c GetRequest PDUs, logs the community string and OID list,
then responds with a GetResponse containing plausible system OID values. then responds with a GetResponse containing plausible system OID values.
Logs all requests as JSON. Logs all requests as JSON.
@@ -13,16 +13,16 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
# OID value map — fake but plausible # OID value map — fake but plausible
_OID_VALUES = { _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.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.3.0": "12345678", # sysUpTime
"1.3.6.1.2.1.1.4.0": "admin@localhost", "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.6.0": "Server Room",
"1.3.6.1.2.1.1.7.0": "72", "1.3.6.1.2.1.1.7.0": "72",
} }
@@ -43,7 +43,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "snmp", "service": "snmp",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -180,7 +180,7 @@ class SNMPProtocol(asyncio.DatagramProtocol):
async def main(): 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() loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint( transport, _ = await loop.create_datagram_endpoint(
SNMPProtocol, local_addr=("0.0.0.0", 161) SNMPProtocol, local_addr=("0.0.0.0", 161)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/tftp_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
TFTP honeypot (UDP 69). TFTP server (UDP 69).
Parses RRQ (read) and WRQ (write) requests, logs filename and transfer mode, Parses RRQ (read) and WRQ (write) requests, logs filename and transfer mode,
then responds with an error packet. Logs all requests as JSON. then responds with an error packet. Logs all requests as JSON.
""" """
@@ -12,7 +12,7 @@ import socket
import struct import struct
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
# TFTP opcodes # TFTP opcodes
@@ -40,7 +40,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "tftp", "service": "tftp",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -81,7 +81,7 @@ class TFTPProtocol(asyncio.DatagramProtocol):
async def main(): 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() loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint( transport, _ = await loop.create_datagram_endpoint(
TFTPProtocol, local_addr=("0.0.0.0", 69) TFTPProtocol, local_addr=("0.0.0.0", 69)

View File

@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \ python3 \
&& rm -rf /var/lib/apt/lists/* && 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 COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
set -e set -e
exec python3 /opt/vnc_honeypot.py exec python3 /opt/server.py

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
VNC (RFB) honeypot. VNC (RFB)server.
Performs the RFB 3.8 handshake, offers VNC authentication, captures the Performs the RFB 3.8 handshake, offers VNC authentication, captures the
24-byte DES-encrypted challenge response, then rejects with "Authentication 24-byte DES-encrypted challenge response, then rejects with "Authentication
failed". Logs the raw response for offline cracking. failed". Logs the raw response for offline cracking.
@@ -12,7 +12,7 @@ import os
import socket import socket
from datetime import datetime, timezone 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", "") LOG_TARGET = os.environ.get("LOG_TARGET", "")
# RFB challenge — fixed so captured responses are reproducible # RFB challenge — fixed so captured responses are reproducible
@@ -34,7 +34,7 @@ def _log(event_type: str, **kwargs) -> None:
event = { event = {
"ts": datetime.now(timezone.utc).isoformat(), "ts": datetime.now(timezone.utc).isoformat(),
"service": "vnc", "service": "vnc",
"host": HONEYPOT_NAME, "host": NODE_NAME,
"event": event_type, "event": event_type,
**kwargs, **kwargs,
} }
@@ -100,7 +100,7 @@ class VNCProtocol(asyncio.Protocol):
async def main(): 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() loop = asyncio.get_running_loop()
server = await loop.create_server(VNCProtocol, "0.0.0.0", 5900) server = await loop.create_server(VNCProtocol, "0.0.0.0", 5900)
async with server: async with server:

View File

@@ -20,13 +20,13 @@ APT_COMPATIBLE = {
} }
BUILD_SERVICES = [ BUILD_SERVICES = [
"http", "rdp", "smb", "ftp", "smtp", "elasticsearch", "ssh", "http", "rdp", "smb", "ftp", "smtp", "elasticsearch",
"pop3", "imap", "mysql", "mssql", "redis", "mongodb", "postgres", "pop3", "imap", "mysql", "mssql", "redis", "mongodb", "postgres",
"ldap", "vnc", "docker_api", "k8s", "sip", "ldap", "vnc", "docker_api", "k8s", "sip",
"mqtt", "llmnr", "snmp", "tftp", "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): 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 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 # Base container uses distro image, not build_base
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

Some files were not shown because too many files have changed in this diff Show More