Add per-service customization, stealth hardening, and BYOS support
- HTTP: configurable server_header, response_code, fake_app presets (apache/nginx/wordpress/phpmyadmin/iis), extra_headers, custom_body, static files directory mount - SSH/Cowrie: configurable kernel_version, hardware_platform, ssh_banner, and users/passwords via COWRIE_USERDB_ENTRIES; switched to build mode so cowrie.cfg.j2 persona fields and userdb.txt generation work - SMTP: configurable banner and MTA hostname - MySQL: configurable version string in protocol greeting - Redis: configurable redis_version and os string in INFO response - BYOS: [custom-*] INI sections define bring-your-own Docker services - Stealth: rename all *_honeypot.py → server.py; replace HONEYPOT_NAME env var with NODE_NAME across all 22+ service templates and plugins; strip "honeypot" from all in-container file content - Config: DeckyConfig.service_config dict; INI [decky-N.svc] subsections; composer passes service_cfg to compose_fragment - 350 tests passing (100%) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__plugin_context-mode_context-mode__ctx_batch_execute"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,3 +46,11 @@ DECNET is a honeypot/deception network framework. It deploys fake machines (call
|
|||||||
- The logging/aggregation network must be isolated from the decoy network.
|
- 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
41
decnet/custom_service.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Bring-your-own-service (BYOS) support.
|
||||||
|
|
||||||
|
CustomService wraps a user-defined service from an INI [custom-*] section.
|
||||||
|
It is instantiated dynamically and registered via register_custom_service(),
|
||||||
|
not through the auto-discovery mechanism in the registry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from decnet.services.base import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class CustomService(BaseService):
|
||||||
|
"""A user-defined service that runs an arbitrary Docker image."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, image: str, exec_cmd: str, ports: list[int] | None = None):
|
||||||
|
self.name = name
|
||||||
|
self.default_image = image
|
||||||
|
self.ports = ports or []
|
||||||
|
self._exec_cmd = exec_cmd
|
||||||
|
|
||||||
|
def compose_fragment(
|
||||||
|
self,
|
||||||
|
decky_name: str,
|
||||||
|
log_target: str | None = None,
|
||||||
|
service_cfg: dict | None = None,
|
||||||
|
) -> dict:
|
||||||
|
slug = self.name.replace("_", "-")
|
||||||
|
fragment: dict = {
|
||||||
|
"image": self.default_image,
|
||||||
|
"container_name": f"{decky_name}-{slug}",
|
||||||
|
"restart": "unless-stopped",
|
||||||
|
"environment": {"NODE_NAME": decky_name},
|
||||||
|
}
|
||||||
|
if self._exec_cmd:
|
||||||
|
fragment["command"] = self._exec_cmd.split()
|
||||||
|
if log_target:
|
||||||
|
fragment["environment"]["LOG_TARGET"] = log_target
|
||||||
|
return fragment
|
||||||
|
|
||||||
|
def dockerfile_context(self):
|
||||||
|
return None
|
||||||
@@ -12,11 +12,26 @@ Format:
|
|||||||
ip=192.168.1.82 # optional
|
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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/docker_api_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/elasticsearch_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/ftp_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/http_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
HTTP honeypot using Flask.
|
|
||||||
Accepts all requests, logs every detail (method, path, headers, body),
|
|
||||||
and responds with convincing but empty pages. Forwards events as JSON
|
|
||||||
to LOG_TARGET if set.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from flask import Flask, request
|
|
||||||
|
|
||||||
HONEYPOT_NAME = os.environ.get("HONEYPOT_NAME", "webserver")
|
|
||||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _forward(event: dict) -> None:
|
|
||||||
if not LOG_TARGET:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
host, port = LOG_TARGET.rsplit(":", 1)
|
|
||||||
with socket.create_connection((host, int(port)), timeout=3) as s:
|
|
||||||
s.sendall((json.dumps(event) + "\n").encode())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _log(event_type: str, **kwargs) -> None:
|
|
||||||
event = {
|
|
||||||
"ts": datetime.now(timezone.utc).isoformat(),
|
|
||||||
"service": "http",
|
|
||||||
"host": HONEYPOT_NAME,
|
|
||||||
"event": event_type,
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
print(json.dumps(event), flush=True)
|
|
||||||
_forward(event)
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def log_request():
|
|
||||||
_log(
|
|
||||||
"request",
|
|
||||||
method=request.method,
|
|
||||||
path=request.path,
|
|
||||||
remote_addr=request.remote_addr,
|
|
||||||
headers=dict(request.headers),
|
|
||||||
body=request.get_data(as_text=True)[:512],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", defaults={"path": ""})
|
|
||||||
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
|
||||||
def catch_all(path):
|
|
||||||
return (
|
|
||||||
"<html><body><h1>403 Forbidden</h1></body></html>",
|
|
||||||
403,
|
|
||||||
{"Server": "Apache/2.4.54 (Debian)", "Content-Type": "text/html"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
_log("startup", msg=f"HTTP honeypot starting as {HONEYPOT_NAME}")
|
|
||||||
app.run(host="0.0.0.0", port=80, debug=False)
|
|
||||||
118
templates/http/server.py
Normal file
118
templates/http/server.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
HTTP service emulator using Flask.
|
||||||
|
Accepts all requests, logs every detail (method, path, headers, body),
|
||||||
|
and responds with configurable pages. Forwards events as JSON to LOG_TARGET if set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask import Flask, request, send_from_directory
|
||||||
|
|
||||||
|
NODE_NAME = os.environ.get("NODE_NAME", "webserver")
|
||||||
|
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||||
|
SERVER_HEADER = os.environ.get("SERVER_HEADER", "Apache/2.4.54 (Debian)")
|
||||||
|
RESPONSE_CODE = int(os.environ.get("RESPONSE_CODE", "403"))
|
||||||
|
FAKE_APP = os.environ.get("FAKE_APP", "")
|
||||||
|
EXTRA_HEADERS = json.loads(os.environ.get("EXTRA_HEADERS", "{}"))
|
||||||
|
CUSTOM_BODY = os.environ.get("CUSTOM_BODY", "")
|
||||||
|
FILES_DIR = os.environ.get("FILES_DIR", "")
|
||||||
|
|
||||||
|
_FAKE_APP_BODIES: dict[str, str] = {
|
||||||
|
"apache_default": (
|
||||||
|
"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n"
|
||||||
|
"<html><head><title>Apache2 Debian Default Page</title></head>\n"
|
||||||
|
"<body><h1>Apache2 Debian Default Page</h1>\n"
|
||||||
|
"<p>It works!</p></body></html>"
|
||||||
|
),
|
||||||
|
"nginx_default": (
|
||||||
|
"<!DOCTYPE html><html><head><title>Welcome to nginx!</title></head>\n"
|
||||||
|
"<body><h1>Welcome to nginx!</h1>\n"
|
||||||
|
"<p>If you see this page, the nginx web server is successfully installed.</p>\n"
|
||||||
|
"</body></html>"
|
||||||
|
),
|
||||||
|
"wordpress": (
|
||||||
|
"<!DOCTYPE html><html><head><title>WordPress › Error</title></head>\n"
|
||||||
|
"<body id=\"error-page\"><div class=\"wp-die-message\">\n"
|
||||||
|
"<h1>Error establishing a database connection</h1></div></body></html>"
|
||||||
|
),
|
||||||
|
"phpmyadmin": (
|
||||||
|
"<!DOCTYPE html><html><head><title>phpMyAdmin</title></head>\n"
|
||||||
|
"<body><form method=\"post\" action=\"index.php\">\n"
|
||||||
|
"<input type=\"text\" name=\"pma_username\" />\n"
|
||||||
|
"<input type=\"password\" name=\"pma_password\" />\n"
|
||||||
|
"<input type=\"submit\" value=\"Go\" /></form></body></html>"
|
||||||
|
),
|
||||||
|
"iis_default": (
|
||||||
|
"<!DOCTYPE html><html><head><title>IIS Windows Server</title></head>\n"
|
||||||
|
"<body><h1>IIS Windows Server</h1>\n"
|
||||||
|
"<p>Welcome to Internet Information Services</p></body></html>"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _forward(event: dict) -> None:
|
||||||
|
if not LOG_TARGET:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
host, port = LOG_TARGET.rsplit(":", 1)
|
||||||
|
with socket.create_connection((host, int(port)), timeout=3) as s:
|
||||||
|
s.sendall((json.dumps(event) + "\n").encode())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _log(event_type: str, **kwargs) -> None:
|
||||||
|
event = {
|
||||||
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"service": "http",
|
||||||
|
"host": NODE_NAME,
|
||||||
|
"event": event_type,
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
print(json.dumps(event), flush=True)
|
||||||
|
_forward(event)
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def log_request():
|
||||||
|
_log(
|
||||||
|
"request",
|
||||||
|
method=request.method,
|
||||||
|
path=request.path,
|
||||||
|
remote_addr=request.remote_addr,
|
||||||
|
headers=dict(request.headers),
|
||||||
|
body=request.get_data(as_text=True)[:512],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", defaults={"path": ""})
|
||||||
|
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
||||||
|
def catch_all(path):
|
||||||
|
# Serve static files directory if configured
|
||||||
|
if FILES_DIR and path:
|
||||||
|
files_path = Path(FILES_DIR) / path
|
||||||
|
if files_path.is_file():
|
||||||
|
return send_from_directory(FILES_DIR, path)
|
||||||
|
|
||||||
|
# Select response body: custom > fake_app preset > default 403
|
||||||
|
if CUSTOM_BODY:
|
||||||
|
body = CUSTOM_BODY
|
||||||
|
elif FAKE_APP and FAKE_APP in _FAKE_APP_BODIES:
|
||||||
|
body = _FAKE_APP_BODIES[FAKE_APP]
|
||||||
|
else:
|
||||||
|
body = "<html><body><h1>403 Forbidden</h1></body></html>"
|
||||||
|
|
||||||
|
headers = {"Server": SERVER_HEADER, "Content-Type": "text/html", **EXTRA_HEADERS}
|
||||||
|
return body, RESPONSE_CODE, headers
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_log("startup", msg=f"HTTP server starting as {NODE_NAME}")
|
||||||
|
app.run(host="0.0.0.0", port=80, debug=False)
|
||||||
@@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
python3 \
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/imap_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/ldap_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/llmnr_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/mongodb_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/mqtt_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/mssql_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/mysql_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/postgres_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/rdp_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/redis_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/sip_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/smtp_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/snmp_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/tftp_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
exec python3 /opt/vnc_honeypot.py
|
exec python3 /opt/server.py
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
Reference in New Issue
Block a user