From aefe3c7dacba3cc50c94fe38f04ca95fa738e648 Mon Sep 17 00:00:00 2001 From: t3chn0m4g3 Date: Thu, 28 May 2026 14:01:11 +0200 Subject: [PATCH] Add support for Cowrie personas with patching and startup scripts - Introduced `patch_cowrie_persona_support.py` to modify Cowrie's source files for persona support, including updates to protocol handling, SSH command responses, netstat output, and service status. - Created `start-cowrie-persona.py` to manage persona activation, including loading persona configurations, selecting a persona based on environment variables, and starting Cowrie with the selected persona. - Ensured proper error handling and validation for persona files and configurations. --- docker/_tests/README.md | 2 + docker/_tests/tests/cowrie.sh | 178 ++- docker/cowrie/Dockerfile | 8 +- docker/cowrie/dist/generate_cowrie_fs.py | 1226 ++++++++++++++--- .../dist/patch_cowrie_persona_support.py | 114 ++ docker/cowrie/dist/start-cowrie-persona.py | 79 ++ 6 files changed, 1390 insertions(+), 217 deletions(-) create mode 100755 docker/cowrie/dist/patch_cowrie_persona_support.py create mode 100755 docker/cowrie/dist/start-cowrie-persona.py diff --git a/docker/_tests/README.md b/docker/_tests/README.md index ecec05ab..c947eeea 100644 --- a/docker/_tests/README.md +++ b/docker/_tests/README.md @@ -34,6 +34,8 @@ Individual tests can also be run directly: ./docker/_tests/tests/conpot.sh --guardian-ast-port 11001 --ipmi-port 1623 ./docker/_tests/tests/cowrie.sh ./docker/_tests/tests/cowrie.sh --ssh-port 2222 --telnet-port 2323 +./docker/_tests/tests/cowrie.sh --persona debian-bookworm-vuln +./docker/_tests/tests/cowrie.sh --persona openwrt-1806 ``` ## Conventions diff --git a/docker/_tests/tests/cowrie.sh b/docker/_tests/tests/cowrie.sh index ed5ac6fd..67892c7a 100755 --- a/docker/_tests/tests/cowrie.sh +++ b/docker/_tests/tests/cowrie.sh @@ -11,6 +11,7 @@ DEFAULT_IMAGE="ghcr.io/telekom-security/cowrie:24.04.1" IMAGE="" SSH_PORT="" TELNET_PORT="" +PERSONA="" LOG_DIR="" TTY_LOG_DIR="" DL_DIR="" @@ -29,6 +30,7 @@ Options: --image IMAGE Image to test. Defaults to docker/cowrie/docker-compose.yml. --ssh-port PORT Host TCP port for SSH. Default: dynamic free port. --telnet-port PORT Host TCP port for Telnet. Default: dynamic free port. + --persona ID Force a generated Cowrie persona. Default: random. --timeout SEC Timeout for startup, protocol, and log checks. Default: 30. --bind-ip IP Host IP to bind. Default: 127.0.0.1. --keep-artifacts Keep temporary compose file and logs for debugging. @@ -66,6 +68,15 @@ parse_args() { TELNET_PORT="${1#*=}" shift ;; + --persona) + [[ $# -ge 2 ]] || test_die "--persona requires an argument" + PERSONA="$2" + shift 2 + ;; + --persona=*) + PERSONA="${1#*=}" + shift + ;; --timeout) [[ $# -ge 2 ]] || test_die "--timeout requires an argument" TEST_TIMEOUT="$2" @@ -107,6 +118,9 @@ validate_args() { if [[ -n "${TELNET_PORT}" ]]; then test_validate_port "${TELNET_PORT}" fi + if [[ -n "${PERSONA}" && ! "${PERSONA}" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then + test_die "--persona must contain only lowercase letters, numbers, and dashes" + fi } prepare_cowrie_harness() { @@ -136,6 +150,8 @@ services: ports: - "${TEST_BIND_IP}:${SSH_PORT}:22" - "${TEST_BIND_IP}:${TELNET_PORT}:23" + environment: + COWRIE_PERSONA: "${PERSONA}" volumes: - "${DL_DIR}:/home/cowrie/cowrie/dl" - "${KEYS_DIR}:/home/cowrie/cowrie/etc" @@ -415,12 +431,16 @@ PY assert_custom_filesystem() { docker exec -i "${TEST_CONTAINER_NAME}" python3 - <<'PY' +import configparser +import json from pathlib import Path import sys root = Path("/home/cowrie/cowrie") -pickle_path = root / "src" / "cowrie" / "data" / "fs.pickle" -honeyfs = root / "honeyfs" +personas_root = root / "personas" +metadata_path = personas_root / "personas.json" +selected_path = Path("/tmp/cowrie/persona") +runtime_config_path = Path("/tmp/cowrie/runtime/cowrie.cfg") offenders = [] @@ -432,50 +452,128 @@ def read_bytes(path): sys.exit(1) -if not pickle_path.is_file(): - print(f"Missing Cowrie filesystem pickle: {pickle_path}", file=sys.stderr) - sys.exit(1) -if not honeyfs.is_dir(): - print(f"Missing Cowrie honeyfs directory: {honeyfs}", file=sys.stderr) +def fail(message): + print(message, file=sys.stderr) sys.exit(1) -if b"phil" in read_bytes(pickle_path).lower(): - offenders.append(str(pickle_path)) -for item in honeyfs.rglob("*"): - if "phil" in item.name.lower(): - offenders.append(str(item)) - continue - if item.is_file() and b"phil" in read_bytes(item).lower(): - offenders.append(str(item)) +def check_forbidden(path): + if "phil" in path.name.lower(): + offenders.append(str(path)) + if path.is_file(): + data = read_bytes(path).lower() + forbidden = ( + b"phil", + b"ubuntu 22.04", + b"2.6.26-2-686", + b"2.6.26-19lenny", + b"com/ubuntu/upstart", + b"dannf@debian.org", + ) + if any(marker in data for marker in forbidden): + offenders.append(str(path)) + + +if not metadata_path.is_file(): + fail(f"Missing Cowrie persona metadata: {metadata_path}") +if not selected_path.is_file(): + fail(f"Missing selected Cowrie persona file: {selected_path}") +if not runtime_config_path.is_file(): + fail(f"Missing runtime Cowrie config: {runtime_config_path}") + +personas = json.loads(metadata_path.read_text(encoding="utf-8")) +if len(personas) != 10: + fail(f"Expected 10 Cowrie personas, found {len(personas)}") + +ids = {persona["id"]: persona for persona in personas} +selected = selected_path.read_text(encoding="utf-8").strip() +if selected not in ids: + fail(f"Selected persona is not in personas.json: {selected}") + +config = configparser.ConfigParser() +config.read(runtime_config_path) +selected_dir = personas_root / selected +expected_filesystem = selected_dir / "fs.pickle" +expected_honeyfs = selected_dir / "honeyfs" +expected_processes = selected_dir / "cmdoutput.json" +expected_txtcmds = selected_dir / "txtcmds" + +if config.get("shell", "filesystem", fallback="") != str(expected_filesystem): + fail("Runtime config does not point to the selected fs.pickle") +if config.get("honeypot", "contents_path", fallback="") != str(expected_honeyfs): + fail("Runtime config does not point to the selected honeyfs") +if config.get("shell", "processes", fallback="") != str(expected_processes): + fail("Runtime config does not point to the selected cmdoutput.json") +if config.get("honeypot", "txtcmds_path", fallback="") != str(expected_txtcmds): + fail("Runtime config does not point to the selected txtcmds") +if config.get("ssh", "version", fallback="") != ids[selected]["ssh_banner"]: + fail("Runtime config SSH banner does not match selected persona metadata") + +for persona_id, persona in ids.items(): + persona_dir = personas_root / persona_id + pickle_path = persona_dir / "fs.pickle" + honeyfs = persona_dir / "honeyfs" + config_path = persona_dir / "cowrie.cfg" + cmdoutput_path = persona_dir / "cmdoutput.json" + txtcmds_path = persona_dir / "txtcmds" + + if not pickle_path.is_file(): + fail(f"Missing Cowrie persona pickle: {pickle_path}") + if not honeyfs.is_dir(): + fail(f"Missing Cowrie persona honeyfs: {honeyfs}") + if not config_path.is_file(): + fail(f"Missing Cowrie persona config: {config_path}") + if not cmdoutput_path.is_file(): + fail(f"Missing Cowrie persona cmdoutput.json: {cmdoutput_path}") + if not txtcmds_path.is_dir(): + fail(f"Missing Cowrie persona txtcmds: {txtcmds_path}") + if pickle_path.stat().st_size < 1000000: + fail(f"{persona_id} fs.pickle is unexpectedly small: {pickle_path.stat().st_size} bytes") + + pickle_bytes = read_bytes(pickle_path) + if persona["user"].encode("utf-8") not in pickle_bytes: + fail(f"{persona_id} fs.pickle does not contain persona user {persona['user']}") + if persona["hostname"].encode("utf-8") not in pickle_bytes: + fail(f"{persona_id} fs.pickle does not contain persona hostname {persona['hostname']}") + if persona["ssh_banner"] not in config_path.read_text(encoding="utf-8"): + fail(f"{persona_id} config does not contain persona SSH banner") + cmdoutput = json.loads(cmdoutput_path.read_text(encoding="utf-8")) + if not cmdoutput.get("command", {}).get("ps"): + fail(f"{persona_id} cmdoutput.json has no ps process list") + for command_path in ( + "bin/df", + "bin/dmesg", + "bin/mount", + "bin/ulimit", + "usr/bin/lscpu", + "usr/bin/nproc", + "usr/bin/top", + ): + if not (txtcmds_path / command_path).is_file(): + fail(f"{persona_id} txtcmds is missing {command_path}") + + passwd = read_bytes(honeyfs / "etc" / "passwd") + hostname = read_bytes(honeyfs / "etc" / "hostname") + os_release = read_bytes(honeyfs / "etc" / "os-release") + if persona["user"].encode("utf-8") not in passwd: + fail(f"{persona_id} honeyfs /etc/passwd does not contain persona user") + if persona["hostname"].encode("utf-8") not in hostname: + fail(f"{persona_id} honeyfs /etc/hostname does not contain persona hostname") + if not os_release.strip(): + fail(f"{persona_id} honeyfs /etc/os-release is empty") + + check_forbidden(pickle_path) + check_forbidden(config_path) + check_forbidden(cmdoutput_path) + for item in honeyfs.rglob("*"): + check_forbidden(item) + for item in txtcmds_path.rglob("*"): + check_forbidden(item) if offenders: - print("Cowrie filesystem still contains 'phil': " + ", ".join(offenders), file=sys.stderr) - sys.exit(1) + fail("Cowrie persona filesystem contains forbidden markers: " + ", ".join(offenders)) -pickle_bytes = read_bytes(pickle_path) -pickle_size = pickle_path.stat().st_size -passwd = read_bytes(honeyfs / "etc" / "passwd") -hostname = read_bytes(honeyfs / "etc" / "hostname") -os_release = read_bytes(honeyfs / "etc" / "os-release") - -if pickle_size < 1000000: - print(f"fs.pickle is unexpectedly small: {pickle_size} bytes", file=sys.stderr) - sys.exit(1) - -checks = [ - (b"ubuntu", pickle_bytes, "fs.pickle does not contain ubuntu"), - (b"ubuntu", passwd, "honeyfs /etc/passwd does not contain ubuntu"), - (b"srv01", hostname, "honeyfs /etc/hostname does not contain srv01"), - (b"Ubuntu 22.04", os_release, "honeyfs /etc/os-release does not describe Ubuntu 22.04"), -] - -for needle, haystack, message in checks: - if needle not in haystack: - print(message, file=sys.stderr) - sys.exit(1) - -print("Cowrie filesystem profile validated: ubuntu@srv01 without phil") +print(f"Cowrie persona pool validated: {selected} selected, {len(personas)} personas, no phil") PY } diff --git a/docker/cowrie/Dockerfile b/docker/cowrie/Dockerfile index 645d7512..aa232d3d 100644 --- a/docker/cowrie/Dockerfile +++ b/docker/cowrie/Dockerfile @@ -48,17 +48,19 @@ RUN apk --no-cache -U upgrade && \ pip3 install --break-system-packages --upgrade --no-cache-dir pip && \ pip3 install --break-system-packages --no-cache-dir -r requirements.txt && \ pip3 install --break-system-packages --no-cache-dir -e . && \ + python3 /root/dist/patch_cowrie_persona_support.py --cowrie-root /home/cowrie/cowrie && \ python3 /root/dist/generate_cowrie_fs.py --cowrie-root /home/cowrie/cowrie && \ + install -m 0755 /root/dist/start-cowrie-persona.py /usr/local/bin/start-cowrie-persona && \ # # Setup configs setcap cap_net_bind_service=+ep $(readlink -f $(type -P python3)) && \ - cp /root/dist/cowrie.cfg /home/cowrie/cowrie/cowrie.cfg && \ + cp /home/cowrie/cowrie/personas/debian-bookworm-vuln/cowrie.cfg /home/cowrie/cowrie/cowrie.cfg && \ chown cowrie:cowrie -R /home/cowrie/* /usr/lib/$(readlink -f $(type -P python3) | cut -f4 -d"/")/site-packages/twisted/plugins && \ # # Start Cowrie once to prevent dropin.cache errors upon container start caused by read-only filesystem su - cowrie -c "export PYTHONPATH=/home/cowrie/cowrie:/home/cowrie/cowrie/src && \ cd /home/cowrie/cowrie && \ - /usr/bin/twistd --uid=2000 --gid=2000 -y cowrie.tac --pidfile cowrie.pid cowrie &" && \ + /usr/bin/twistd --uid=2000 --gid=2000 --pidfile cowrie.pid cowrie &" && \ sleep 10 && \ rm -rf /home/cowrie/cowrie/etc && \ # @@ -82,4 +84,4 @@ RUN apk --no-cache -U upgrade && \ ENV PYTHONPATH=/home/cowrie/cowrie:/home/cowrie/cowrie/src WORKDIR /home/cowrie/cowrie USER cowrie:cowrie -CMD ["/usr/bin/twistd", "--nodaemon", "-y", "cowrie.tac", "--pidfile", "/tmp/cowrie/cowrie.pid", "cowrie"] +CMD ["/usr/local/bin/start-cowrie-persona"] diff --git a/docker/cowrie/dist/generate_cowrie_fs.py b/docker/cowrie/dist/generate_cowrie_fs.py index 734bfdc8..73323153 100644 --- a/docker/cowrie/dist/generate_cowrie_fs.py +++ b/docker/cowrie/dist/generate_cowrie_fs.py @@ -1,19 +1,39 @@ #!/usr/bin/env python3 import argparse +import copy +import datetime +import json import os import pickle +import random import shutil import stat import time from pathlib import Path -HOSTNAME = "srv01" -LOGIN_USER = "ubuntu" -LOGIN_UID = 1000 -LOGIN_GID = 1000 MIN_PICKLE_SIZE = 1_000_000 +PERSONAS_DIRNAME = "personas" +DEFAULT_BOOTSTRAP_PERSONA = "debian-bookworm-vuln" +FORBIDDEN_MARKERS = [ + "phil", + "Ubuntu 22.04", + "2.6.26-2-686", + "2.6.26-19lenny", + "com/ubuntu/upstart", + "dannf@debian.org", +] +TXTCMD_PATHS = [ + "bin/df", + "bin/dmesg", + "bin/mount", + "bin/ulimit", + "usr/bin/lscpu", + "usr/bin/nproc", + "usr/bin/top", +] +RANDOM = random.SystemRandom() A_NAME = 0 A_TYPE = 1 @@ -30,8 +50,47 @@ T_LINK = 0 T_DIR = 1 T_FILE = 2 -TEXT_FILES = { - "etc/passwd": """root:x:0:0:root:/root:/bin/bash + +COMMON_REMOVE_PATHS = [ + "home/phil", + "home/ubuntu", + "etc/lsb-release", + "etc/redhat-release", + "etc/fedora-release", + "etc/openwrt_release", + "etc/openwrt_version", + "etc.defaults/VERSION", + "etc/config/uLinux.conf", + "etc/default_config/uLinux.conf", + "etc_ro/version", + "etc_ro/product.ini", + "etc/ubnt/version", + "etc/zyxel/model", + "firmware/mnt/info/fwversion", +] + + +COMMON_DIRS = [ + "bin", + "dev", + "etc", + "home", + "lib", + "proc", + "root", + "sbin", + "tmp", + "usr", + "usr/bin", + "usr/sbin", + "var", + "var/log", +] + + +def server_passwd(user, uid, gid, gecos=None): + gecos = gecos or user + return f"""root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin @@ -47,19 +106,21 @@ www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin -gnats:x:41:41:Gnats Bug-Reporting System:/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin systemd-resolve:x:997:997:systemd Resolver:/:/usr/sbin/nologin messagebus:x:100:102::/nonexistent:/usr/sbin/nologin sshd:x:101:65534::/run/sshd:/usr/sbin/nologin -ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash -""", - "etc/group": """root:x:0: +{user}:x:{uid}:{gid}:{gecos}:/home/{user}:/bin/bash +""" + + +def server_group(user, gid): + return f"""root:x:0: daemon:x:1: bin:x:2: sys:x:3: -adm:x:4:syslog,ubuntu +adm:x:4:syslog,{user} tty:x:5: disk:x:6: lp:x:7: @@ -69,27 +130,20 @@ uucp:x:10: man:x:12: proxy:x:13: kmem:x:15: -dialout:x:20:ubuntu -fax:x:21: -voice:x:22: -cdrom:x:24:ubuntu -floppy:x:25:ubuntu -tape:x:26: -sudo:x:27:ubuntu -audio:x:29:ubuntu -dip:x:30:ubuntu +dialout:x:20:{user} +cdrom:x:24:{user} +sudo:x:27:{user} +audio:x:29:{user} +dip:x:30:{user} www-data:x:33: backup:x:34: operator:x:37: list:x:38: irc:x:39: -src:x:40: -gnats:x:41: shadow:x:42: utmp:x:43: -video:x:44:ubuntu -sasl:x:45: -plugdev:x:46:ubuntu +video:x:44:{user} +plugdev:x:46:{user} staff:x:50: games:x:60: users:x:100: @@ -98,9 +152,12 @@ systemd-network:x:998: systemd-resolve:x:997: messagebus:x:102: ssh:x:103: -ubuntu:x:1000: -""", - "etc/shadow": """root:*:19276:0:99999:7::: +{user}:x:{gid}: +""" + + +def server_shadow(user): + return f"""root:*:19276:0:99999:7::: daemon:*:19276:0:99999:7::: bin:*:19276:0:99999:7::: sys:*:19276:0:99999:7::: @@ -116,58 +173,70 @@ www-data:*:19276:0:99999:7::: backup:*:19276:0:99999:7::: list:*:19276:0:99999:7::: irc:*:19276:0:99999:7::: -gnats:*:19276:0:99999:7::: nobody:*:19276:0:99999:7::: systemd-network:*:19276:0:99999:7::: systemd-resolve:*:19276:0:99999:7::: messagebus:*:19276:0:99999:7::: sshd:*:19276:0:99999:7::: -ubuntu:*:19276:0:99999:7::: -""", - "etc/hostname": f"{HOSTNAME}\n", - "etc/hosts": f"""127.0.0.1 localhost -127.0.1.1 {HOSTNAME} +{user}:*:19276:0:99999:7::: +""" -# The following lines are desirable for IPv6 capable hosts -::1 ip6-localhost ip6-loopback -fe00::0 ip6-localnet -ff00::0 ip6-mcastprefix + +def embedded_passwd(user, uid, gid, shell="/bin/sh", gecos=None): + gecos = gecos or user + return f"""root:x:0:0:root:/root:/bin/sh +daemon:x:1:1:daemon:/var:/bin/false +bin:x:2:2:bin:/bin:/bin/false +nobody:x:65534:65534:nobody:/var:/bin/false +sshd:x:22:22:sshd:/var/empty:/bin/false +{user}:x:{uid}:{gid}:{gecos}:/home/{user}:{shell} +""" + + +def embedded_group(user, gid): + return f"""root:x:0: +daemon:x:1: +bin:x:2: +adm:x:4:{user} +tty:x:5: +disk:x:6: +wheel:x:10:{user} +audio:x:29: +users:x:100: +nobody:x:65534: +sshd:x:22: +{user}:x:{gid}: +""" + + +def embedded_shadow(user): + return f"""root:$1$root$A3qH7I7U2vW0nLJ9rKaNl/:18900:0:99999:7::: +daemon:*:18900:0:99999:7::: +bin:*:18900:0:99999:7::: +nobody:*:18900:0:99999:7::: +sshd:*:18900:0:99999:7::: +{user}:*:18900:0:99999:7::: +""" + + +def hosts(hostname): + return f"""127.0.0.1 localhost +127.0.1.1 {hostname} + +::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters -""", - "etc/os-release": """PRETTY_NAME="Ubuntu 22.04.4 LTS" -NAME="Ubuntu" -VERSION_ID="22.04" -VERSION="22.04.4 LTS (Jammy Jellyfish)" -VERSION_CODENAME=jammy -ID=ubuntu -ID_LIKE=debian -HOME_URL="https://www.ubuntu.com/" -SUPPORT_URL="https://help.ubuntu.com/" -BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" -PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" -UBUNTU_CODENAME=jammy -""", - "etc/issue": "Ubuntu 22.04.4 LTS \\n \\l\n", - "etc/issue.net": "Ubuntu 22.04.4 LTS\n", - "etc/motd": """Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-23-generic x86_64) +""" - * Documentation: https://help.ubuntu.com - * Management: https://landscape.canonical.com - * Support: https://ubuntu.com/advantage -0 updates can be applied immediately. -""", - "proc/version": "Linux version 5.15.0-23-generic (buildd@lcy02-amd64-058) (gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #25~22.04-Ubuntu SMP\n", -} - -HOME_FILES = { - ".bash_logout": """# ~/.bash_logout: executed by bash(1) when login shell exits. +def home_files(user): + return { + f"home/{user}/.bash_logout": """# ~/.bash_logout: executed by bash(1) when login shell exits. if [ "$SHLVL" = 1 ]; then [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q fi """, - ".bashrc": """# ~/.bashrc: executed by bash(1) for non-login shells. + f"home/{user}/.bashrc": """# ~/.bashrc: executed by bash(1) for non-login shells. case $- in *i*) ;; *) return;; @@ -177,30 +246,634 @@ HISTCONTROL=ignoreboth shopt -s histappend HISTSIZE=1000 HISTFILESIZE=2000 - -if [ -f /etc/bash_completion ]; then - . /etc/bash_completion -fi """, - ".profile": """# ~/.profile: executed by the command interpreter for login shells. + f"home/{user}/.profile": """# ~/.profile: executed by the command interpreter for login shells. if [ "$BASH" ]; then - if [ -f ~/.bashrc ]; then - . ~/.bashrc - fi + [ -f ~/.bashrc ] && . ~/.bashrc fi - -mesg n 2> /dev/null || true """, - ".sudo_as_admin_successful": "", -} + } + + +def random_ps_start(): + today = datetime.date.today() + day = today - datetime.timedelta(days=RANDOM.randint(3, 240)) + return day.strftime("%b%d") + + +def ps_entry(user, pid, command, cpu=0.0, mem=0.1, vsz=0, rss=0, stat="S", tty="?", start=None): + return { + "USER": user, + "PID": pid, + "CPU": cpu, + "MEM": mem, + "VSZ": vsz, + "RSS": rss, + "TTY": tty, + "STAT": stat, + "START": start or "Apr17", + "TIME": 0.0, + "COMMAND": command, + } + + +def server_processes(persona): + family = persona["family"] + user = persona["user"] + start = persona["process_start"] + if family == "fedora": + return [ + ps_entry("root", 1, "/usr/lib/systemd/systemd --switched-root --system --deserialize 31", mem=0.6, vsz=177248, rss=8120, stat="Ss", start=start), + ps_entry("root", 428, "/usr/lib/systemd/systemd-journald", mem=0.3, vsz=45328, rss=5120, stat="Ss", start=start), + ps_entry("root", 462, "/usr/lib/systemd/systemd-udevd", mem=0.2, vsz=28576, rss=3776, stat="Ss", start=start), + ps_entry("dbus", 713, "/usr/bin/dbus-broker-launch --scope system --audit", mem=0.2, vsz=12840, rss=2560, stat="Ss", start=start), + ps_entry("root", 899, "/usr/sbin/sshd -D", mem=0.2, vsz=19352, rss=4376, stat="Ss", start=start), + ps_entry("root", 936, "/usr/sbin/crond -n", mem=0.1, vsz=7312, rss=1712, stat="Ss", start=start), + ps_entry(user, 1512, "-bash", mem=0.1, vsz=9456, rss=3240, stat="Ss", tty="pts/0", start=start), + ] + if family == "rhel": + return [ + ps_entry("root", 1, "/usr/lib/systemd/systemd --switched-root --system --deserialize 30", mem=0.5, vsz=178108, rss=8260, stat="Ss", start=start), + ps_entry("root", 433, "/usr/lib/systemd/systemd-journald", mem=0.3, vsz=45452, rss=5384, stat="Ss", start=start), + ps_entry("root", 469, "/usr/lib/systemd/systemd-udevd", mem=0.2, vsz=28964, rss=3900, stat="Ss", start=start), + ps_entry("dbus", 688, "/usr/bin/dbus-broker-launch --scope system --audit", mem=0.1, vsz=12844, rss=2636, stat="Ss", start=start), + ps_entry("root", 835, "/usr/sbin/chronyd -F 2", mem=0.2, vsz=29184, rss=3508, stat="S", start=start), + ps_entry("root", 991, "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups", mem=0.2, vsz=21432, rss=5160, stat="Ss", start=start), + ps_entry(user, 1770, "-bash", mem=0.1, vsz=9632, rss=3428, stat="Ss", tty="pts/0", start=start), + ] + return [ + ps_entry("root", 1, "/lib/systemd/systemd --system --deserialize=31", mem=0.6, vsz=166200, rss=7904, stat="Ss", start=start), + ps_entry("root", 312, "/lib/systemd/systemd-journald", mem=0.3, vsz=40376, rss=5028, stat="Ss", start=start), + ps_entry("root", 346, "/lib/systemd/systemd-udevd", mem=0.2, vsz=28772, rss=3512, stat="Ss", start=start), + ps_entry("systemd+", 571, "/lib/systemd/systemd-networkd", mem=0.2, vsz=24124, rss=4072, stat="Ss", start=start), + ps_entry("message+", 696, "/usr/bin/dbus-daemon --system --address=systemd:", mem=0.1, vsz=9192, rss=2900, stat="Ss", start=start), + ps_entry("root", 834, "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups", mem=0.2, vsz=15824, rss=5044, stat="Ss", start=start), + ps_entry("root", 865, "/usr/sbin/cron -f", mem=0.1, vsz=6812, rss=2180, stat="Ss", start=start), + ps_entry(user, 1421, "-bash", mem=0.1, vsz=8672, rss=3312, stat="Ss", tty="pts/0", start=start), + ] + + +def embedded_processes(persona): + family = persona["family"] + user = persona["user"] + hostname = persona["hostname"] + start = persona["process_start"] + if family == "openwrt": + return [ + ps_entry("root", 1, "/sbin/procd", mem=0.4, vsz=1540, rss=780, stat="S", start=start), + ps_entry("root", 452, "/sbin/ubusd", mem=0.2, vsz=1240, rss=620, stat="S", start=start), + ps_entry("root", 701, "/sbin/netifd", mem=0.3, vsz=1760, rss=900, stat="S", start=start), + ps_entry("root", 918, "/usr/sbin/dropbear -F -P /var/run/dropbear.1.pid", mem=0.2, vsz=1440, rss=720, stat="S", start=start), + ps_entry("root", 965, "/usr/sbin/uhttpd -f -h /www -r OpenWrt", mem=0.3, vsz=1708, rss=864, stat="S", start=start), + ps_entry("root", 1120, "ash", mem=0.1, vsz=1224, rss=612, stat="S", tty="pts/0", start=start), + ] + if family == "qnap": + return [ + ps_entry("admin", 1, "init", mem=0.2, vsz=3028, rss=1040, stat="S", start=start), + ps_entry("admin", 520, "/sbin/daemon_mgr", mem=0.3, vsz=8104, rss=2236, stat="S", start=start), + ps_entry("admin", 833, "/sbin/qLogEngined", mem=0.5, vsz=18572, rss=5232, stat="S", start=start), + ps_entry("admin", 1002, "/usr/sbin/sshd -f /etc/config/ssh/sshd_config -D", mem=0.2, vsz=12300, rss=3120, stat="S", start=start), + ps_entry("admin", 1288, "/sbin/thttpd -p 8080 -d /home/httpd", mem=0.2, vsz=7624, rss=2024, stat="S", start=start), + ps_entry("admin", 1435, "sh", mem=0.1, vsz=2184, rss=1088, stat="S", tty="pts/0", start=start), + ] + if family == "synology": + return [ + ps_entry("root", 1, "/sbin/init", mem=0.3, vsz=11840, rss=2424, stat="Ss", start=start), + ps_entry("root", 412, "/usr/syno/bin/synoservice --bootup", mem=0.4, vsz=22048, rss=6024, stat="S", start=start), + ps_entry("root", 668, "/usr/syno/sbin/synocgid", mem=0.2, vsz=14492, rss=3096, stat="S", start=start), + ps_entry("root", 900, "/usr/sbin/sshd -D", mem=0.2, vsz=12420, rss=3820, stat="Ss", start=start), + ps_entry("root", 1004, "/usr/syno/sbin/synoscheduler", mem=0.2, vsz=16884, rss=3596, stat="S", start=start), + ps_entry("admin", 1477, "sh", mem=0.1, vsz=2328, rss=1092, stat="S", tty="pts/0", start=start), + ] + if family == "ubiquiti": + return [ + ps_entry("root", 1, "/sbin/init", mem=0.2, vsz=2936, rss=1044, stat="Ss", start=start), + ps_entry("root", 321, "/sbin/ubnt-util", mem=0.2, vsz=3640, rss=1280, stat="S", start=start), + ps_entry("root", 554, "/usr/sbin/ubnt-daemon", mem=0.3, vsz=6820, rss=2300, stat="S", start=start), + ps_entry("root", 812, "/usr/sbin/sshd -D", mem=0.2, vsz=11384, rss=2784, stat="Ss", start=start), + ps_entry("root", 940, "/opt/vyatta/sbin/vyatta-router", mem=0.3, vsz=9488, rss=2956, stat="S", start=start), + ps_entry(user, 1295, "-vbash", mem=0.1, vsz=3916, rss=1420, stat="S", tty="pts/0", start=start), + ] + return [ + ps_entry("root", 1, "init", mem=0.2, vsz=1512, rss=688, stat="S", start=start), + ps_entry("root", 216, "/sbin/syslogd -n", mem=0.1, vsz=1128, rss=516, stat="S", start=start), + ps_entry("root", 239, "/sbin/klogd -n", mem=0.1, vsz=1116, rss=508, stat="S", start=start), + ps_entry("root", 481, "/usr/sbin/dropbear -F -p 22", mem=0.2, vsz=1368, rss=672, stat="S", start=start), + ps_entry("root", 522, f"/usr/sbin/httpd -h /www -n {hostname}", mem=0.2, vsz=1548, rss=760, stat="S", start=start), + ps_entry(user, 911, "sh", mem=0.1, vsz=1096, rss=548, stat="S", tty="pts/0", start=start), + ] + + +def cmdoutput_for_persona(persona): + family = persona["family"] + processes = embedded_processes(persona) if family in { + "iot-router", + "iot-nas", + "openwrt", + "qnap", + "synology", + "ubiquiti", + } else server_processes(persona) + return {"command": {"ps": processes}} + + +def txtcmds_for_persona(persona): + family = persona["family"] + arch = persona["hardware_platform"] + kernel = persona["kernel_version"] + build = persona["kernel_build_string"] + hostname = persona["hostname"] + cpu_count = "1" if arch in ("mips", "armv7l") else "2" + + if family in {"iot-router", "openwrt", "ubiquiti"}: + df = "Filesystem 1K-blocks Used Available Use% Mounted on\nrootfs 8192 3584 4608 44% /\n/dev/root 8192 8192 0 100% /rom\ntmpfs 65536 324 65212 1% /tmp\n" + mount = "rootfs on / type rootfs (rw)\n/dev/root on /rom type squashfs (ro,relatime)\nproc on /proc type proc (rw,nosuid,nodev,noexec,noatime)\ntmpfs on /tmp type tmpfs (rw,nosuid,nodev,noatime)\n" + lscpu = f"Architecture: {arch}\nByte Order: Little Endian\nCPU(s): {cpu_count}\nModel name: MIPS 24Kc V7.4\nBogoMIPS: 385.84\n" + top = "Mem: 28704K used, 36832K free, 0K shrd, 1024K buff, 8212K cached\nCPU: 1% usr 2% sys 0% nic 96% idle 0% io 0% irq 1% sirq\n PID USER STATUS VSZ %VSZ %CPU COMMAND\n 1 root S 1512 2% 0% init\n 481 root S 1368 2% 0% dropbear\n" + elif family in {"iot-nas", "qnap", "synology"}: + df = "Filesystem 1K-blocks Used Available Use% Mounted on\n/dev/md9 521684 165312 356372 32% /\ntmpfs 1024000 2048 1021952 1% /tmp\n/dev/mapper/cachedev1 389120000 185344000 203776000 48% /share/CACHEDEV1_DATA\n" + mount = "/dev/md9 on / type ext4 (rw,relatime,data=ordered)\nproc on /proc type proc (rw,nosuid,nodev,noexec,relatime)\ntmpfs on /tmp type tmpfs (rw,nosuid,nodev,relatime)\n/dev/mapper/cachedev1 on /share/CACHEDEV1_DATA type ext4 (rw,relatime,data=ordered)\n" + lscpu = f"Architecture: {arch}\nCPU(s): {cpu_count}\nByte Order: Little Endian\nModel name: embedded storage processor\n" + top = "top - 12:00:01 up 14 days, 2:18, 1 user, load average: 0.08, 0.05, 0.01\nTasks: 102 total, 1 running, 101 sleeping, 0 stopped, 0 zombie\n%Cpu(s): 1.2 us, 0.8 sy, 0.0 ni, 97.6 id, 0.2 wa\n" + else: + df = "Filesystem 1K-blocks Used Available Use% Mounted on\nudev 496124 0 496124 0% /dev\ntmpfs 101784 744 101040 1% /run\n/dev/sda1 20509264 4882112 14562204 26% /\ntmpfs 508904 0 508904 0% /dev/shm\n" + mount = "/dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro)\nproc on /proc type proc (rw,nosuid,nodev,noexec,relatime)\nsysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)\ntmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,relatime,size=101784k)\n" + lscpu = f"Architecture: {arch}\nCPU op-mode(s): 32-bit, 64-bit\nByte Order: Little Endian\nCPU(s): {cpu_count}\nModel name: Common KVM processor\nBogoMIPS: 4800.00\n" + top = "top - 12:00:01 up 6 days, 3:14, 1 user, load average: 0.03, 0.04, 0.01\nTasks: 87 total, 1 running, 86 sleeping, 0 stopped, 0 zombie\n%Cpu(s): 0.4 us, 0.3 sy, 0.0 ni, 99.0 id, 0.1 wa\n" + + dmesg = ( + f"[ 0.000000] Linux version {kernel} ({hostname}) {build}\n" + "[ 0.000000] Command line: console=ttyS0 root=/dev/sda1 ro quiet\n" + f"[ 0.000000] CPU: {arch} processor initialized\n" + "[ 0.120000] VFS: Mounted root filesystem readonly\n" + "[ 1.420000] random: crng init done\n" + ) + ulimit = """core file size (blocks, -c) 0 +data seg size (kbytes, -d) unlimited +scheduling priority (-e) 0 +file size (blocks, -f) unlimited +open files (-n) 1024 +stack size (kbytes, -s) 8192 +cpu time (seconds, -t) unlimited +max user processes (-u) 1024 +virtual memory (kbytes, -v) unlimited +""" + return { + "bin/df": df, + "bin/dmesg": dmesg, + "bin/mount": mount, + "bin/ulimit": ulimit, + "usr/bin/lscpu": lscpu, + "usr/bin/nproc": f"{cpu_count}\n", + "usr/bin/top": top, + } + + +def profile( + persona_id, + family, + hostname, + user, + uid, + gid, + arch, + kernel_version, + kernel_build_string, + ssh_banner, + shell_ssh_version, + hardware_platform, + operating_system, + passwd, + group, + shadow, + files, + vulnerability, + remove_paths=None, + shell="/bin/sh", +): + base_files = { + "etc/passwd": passwd, + "etc/group": group, + "etc/shadow": shadow, + "etc/hostname": f"{hostname}\n", + "etc/hosts": hosts(hostname), + "proc/version": f"Linux version {kernel_version} ({hostname}) {kernel_build_string}\n", + "proc/mounts": "rootfs / rootfs rw 0 0\nproc /proc proc rw,nosuid,nodev,noexec,relatime 0 0\n", + "proc/cpuinfo": f"processor\t: 0\nmodel name\t: {hardware_platform}\nBogoMIPS\t: 100.00\n", + "proc/meminfo": "MemTotal: 262144 kB\nMemFree: 32768 kB\n", + } + base_files.update(files) + home_base = "root" if user == "root" else f"home/{user}" + if shell.endswith("bash") and user != "root": + base_files.update(home_files(user)) + else: + base_files[f"{home_base}/.profile"] = "export PATH=/bin:/sbin:/usr/bin:/usr/sbin\n" + + return { + "id": persona_id, + "family": family, + "hostname": hostname, + "user": user, + "uid": uid, + "gid": gid, + "arch": arch, + "kernel_version": kernel_version, + "kernel_build_string": kernel_build_string, + "ssh_banner": ssh_banner, + "shell_ssh_version": shell_ssh_version, + "hardware_platform": hardware_platform, + "operating_system": operating_system, + "files": base_files, + "remove_paths": COMMON_REMOVE_PATHS + (remove_paths or []), + "vulnerability": vulnerability, + "shell": shell, + "process_start": random_ps_start(), + } + + +PERSONAS = [ + profile( + "debian-bookworm-vuln", + "debian", + "db12-web01", + "debian", + 1000, + 1000, + "linux-x64-lsb", + "6.1.0-18-amd64", + "#1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1", + "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", + "OpenSSH_9.2p1, OpenSSL 3.0.11 19 Sep 2023", + "x86_64", + "GNU/Linux", + server_passwd("debian", 1000, 1000, "Debian"), + server_group("debian", 1000), + server_shadow("debian"), + { + "etc/os-release": """PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" +NAME="Debian GNU/Linux" +VERSION_ID="12" +VERSION="12 (bookworm)" +VERSION_CODENAME=bookworm +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +""", + "etc/debian_version": "12.5\n", + "etc/issue": "Debian GNU/Linux 12 \\n \\l\n", + "etc/issue.net": "Debian GNU/Linux 12\n", + "etc/motd": "Linux db12-web01 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 x86_64\n", + }, + "regreSSHion-era OpenSSH 9.2p1 fingerprint", + shell="/bin/bash", + ), + profile( + "fedora-36-vuln", + "fedora", + "fedora-edge", + "fedora", + 1000, + 1000, + "linux-x64-lsb", + "5.17.5-300.fc36.x86_64", + "#1 SMP PREEMPT Thu Apr 28 15:57:21 UTC 2022", + "SSH-2.0-OpenSSH_8.8", + "OpenSSH_8.8p1, OpenSSL 3.0.2 15 Mar 2022", + "x86_64", + "GNU/Linux", + server_passwd("fedora", 1000, 1000, "Fedora"), + server_group("fedora", 1000), + server_shadow("fedora"), + { + "etc/os-release": """NAME="Fedora Linux" +VERSION="36 (Server Edition)" +ID=fedora +VERSION_ID=36 +PLATFORM_ID="platform:f36" +PRETTY_NAME="Fedora Linux 36 (Server Edition)" +ANSI_COLOR="0;38;2;60;110;180" +HOME_URL="https://fedoraproject.org/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +VARIANT="Server Edition" +VARIANT_ID=server +""", + "etc/fedora-release": "Fedora release 36 (Thirty Six)\n", + "etc/issue": "Fedora release 36 (Thirty Six) \\n \\l\n", + "etc/issue.net": "Fedora release 36 (Thirty Six)\n", + "etc/motd": "Fedora Linux 36 (Server Edition)\n", + }, + "EOL Fedora 36 OpenSSH 8.8p1 fingerprint", + shell="/bin/bash", + ), + profile( + "rhel-9-vuln", + "rhel", + "rhel9-app01", + "cloud-user", + 1000, + 1000, + "linux-x64-lsb", + "5.14.0-362.8.1.el9_3.x86_64", + "#1 SMP PREEMPT_DYNAMIC Red Hat 5.14.0-362.8.1.el9_3", + "SSH-2.0-OpenSSH_8.7", + "OpenSSH_8.7p1, OpenSSL 3.0.7 1 Nov 2022", + "x86_64", + "GNU/Linux", + server_passwd("cloud-user", 1000, 1000, "Cloud User"), + server_group("cloud-user", 1000), + server_shadow("cloud-user"), + { + "etc/os-release": """NAME="Red Hat Enterprise Linux" +VERSION="9.3 (Plow)" +ID="rhel" +ID_LIKE="fedora" +VERSION_ID="9.3" +PLATFORM_ID="platform:el9" +PRETTY_NAME="Red Hat Enterprise Linux 9.3 (Plow)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos" +HOME_URL="https://www.redhat.com/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +""", + "etc/redhat-release": "Red Hat Enterprise Linux release 9.3 (Plow)\n", + "etc/issue": "\\S\nKernel \\r on an \\m\n", + "etc/issue.net": "\\S\nKernel \\r on an \\m\n", + "etc/motd": "Red Hat Enterprise Linux 9.3 (Plow)\n", + }, + "RHEL 9 OpenSSH 8.7p1 fingerprint", + shell="/bin/bash", + ), + profile( + "dlink-dir859", + "iot-router", + "DIR-859", + "admin", + 1000, + 1000, + "linux-mips-lsb", + "2.6.30.9", + "#1 Thu Oct 18 17:14:26 CST 2018", + "SSH-2.0-dropbear_2012.55", + "Dropbear sshd 2012.55", + "mips", + "Linux", + embedded_passwd("admin", 1000, 1000, "/bin/sh", "admin"), + embedded_group("admin", 1000), + embedded_shadow("admin"), + { + "etc/os-release": """NAME="D-Link Embedded Linux" +ID=dlink +PRETTY_NAME="D-Link DIR-859" +VERSION_ID="1.06" +""", + "etc/issue": "D-Link DIR-859 login: \\l\n", + "etc/issue.net": "D-Link DIR-859\n", + "etc/motd": "D-Link DIR-859\n", + "etc_ro/version": "Firmware External Version: V1.06B01\nFirmware Internal Version: V1.06\n", + "etc_ro/product.ini": "model=DIR-859\nvendor=D-Link\n", + "bin/busybox": "BusyBox v1.19.4 (2018-10-18 17:14:26 CST) multi-call binary.\n", + }, + "D-Link DIR-859 CVE-2019-17621-style router fingerprint", + ), + profile( + "tplink-wr841n", + "iot-router", + "TL-WR841N", + "admin", + 1000, + 1000, + "linux-mips-lsb", + "3.18.23", + "#1 Mon Jan 9 15:21:00 CST 2023", + "SSH-2.0-dropbear_2015.67", + "Dropbear sshd 2015.67", + "mips", + "Linux", + embedded_passwd("admin", 1000, 1000, "/bin/sh", "admin"), + embedded_group("admin", 1000), + embedded_shadow("admin"), + { + "etc/os-release": """NAME="TP-Link Embedded Linux" +ID=tplink +PRETTY_NAME="TP-Link TL-WR841N" +VERSION_ID="0.9.1" +""", + "etc/issue": "TP-Link Wireless Router \\n \\l\n", + "etc/issue.net": "TP-Link Wireless Router\n", + "etc/motd": "TL-WR841N\n", + "etc/product-info": "product_name=TL-WR841N\nproduct_ver=14.0\n", + "etc/config/system": "config system\n\toption hostname 'TL-WR841N'\n\toption timezone 'UTC'\n", + "bin/busybox": "BusyBox v1.25.1 (2023-01-09 15:21:00 CST) multi-call binary.\n", + }, + "TP-Link TL-WR841N CVE-2023-33538-style router fingerprint", + ), + profile( + "zyxel-nas326", + "iot-nas", + "NAS326", + "admin", + 1000, + 1000, + "linux-arm-lsb", + "3.2.54", + "#1 SMP Tue Jan 14 02:32:09 CST 2020", + "SSH-2.0-dropbear_2014.63", + "Dropbear sshd 2014.63", + "armv7l", + "Linux", + embedded_passwd("admin", 1000, 1000, "/bin/sh", "admin"), + embedded_group("admin", 1000), + embedded_shadow("admin"), + { + "etc/os-release": """NAME="Zyxel NAS" +ID=zyxel +PRETTY_NAME="Zyxel NAS326" +VERSION_ID="5.21" +""", + "etc/issue": "Welcome to Zyxel NAS326 \\n \\l\n", + "etc/issue.net": "Zyxel NAS326\n", + "etc/motd": "Zyxel NAS326\n", + "etc/zyxel/model": "NAS326\n", + "firmware/mnt/info/fwversion": "V5.21(AAZF.7)\n", + "bin/busybox": "BusyBox v1.19.4 (2020-01-14 02:32:09 CST) multi-call binary.\n", + }, + "Zyxel NAS326 CVE-2020-9054-style NAS fingerprint", + ), + profile( + "openwrt-1806", + "openwrt", + "OpenWrt", + "root", + 0, + 0, + "linux-mips-lsb", + "4.14.95", + "#0 Mon Jan 28 08:54:02 2019", + "SSH-2.0-dropbear_2017.75", + "Dropbear sshd 2017.75", + "mips", + "Linux", + """root:x:0:0:root:/root:/bin/ash +daemon:x:1:1:daemon:/var:/bin/false +ftp:x:55:55:ftp:/home/ftp:/bin/false +nobody:x:65534:65534:nobody:/var:/bin/false +""", + """root:x:0: +daemon:x:1: +adm:x:4: +users:x:100: +nobody:x:65534: +""", + "root:$1$root$A3qH7I7U2vW0nLJ9rKaNl/:17950:0:99999:7:::\n", + { + "etc/os-release": """NAME="OpenWrt" +VERSION="18.06.2" +ID="openwrt" +ID_LIKE="lede openwrt" +PRETTY_NAME="OpenWrt 18.06.2" +VERSION_ID="18.06.2" +HOME_URL="https://openwrt.org/" +BUG_URL="https://bugs.openwrt.org/" +SUPPORT_URL="https://forum.openwrt.org/" +BUILD_ID="r7676-cddd7b4c77" +LEDE_BOARD="ramips/mt7620" +LEDE_ARCH="mipsel_24kc" +LEDE_TAINTS="no-all" +""", + "etc/openwrt_release": """DISTRIB_ID='OpenWrt' +DISTRIB_RELEASE='18.06.2' +DISTRIB_REVISION='r7676-cddd7b4c77' +DISTRIB_TARGET='ramips/mt7620' +DISTRIB_ARCH='mipsel_24kc' +DISTRIB_DESCRIPTION='OpenWrt 18.06.2 r7676-cddd7b4c77' +DISTRIB_TAINTS='no-all' +""", + "etc/openwrt_version": "r7676-cddd7b4c77\n", + "etc/issue": "OpenWrt 18.06.2, r7676-cddd7b4c77 \\n \\l\n", + "etc/issue.net": "OpenWrt 18.06.2\n", + "etc/banner": " _______ ________ __\n | |.-----.-----.-----.| | | |.----.| |_\n | - || _ | -__| || | | || _|| _|\n |_______|| __|_____|__|__||________||__| |____|\n |__| W I R E L E S S F R E E D O M\n", + "bin/busybox": "BusyBox v1.28.4 () multi-call binary.\n", + }, + "OpenWrt 18.06 Dropbear router fingerprint", + remove_paths=["home/root"], + ), + profile( + "qnap-qts", + "qnap", + "NAS4BAY", + "admin", + 1000, + 1000, + "linux-x64-lsb", + "5.10.60-qnap", + "#1 SMP Tue Sep 12 01:23:45 CST 2023", + "SSH-2.0-OpenSSH_8.4", + "OpenSSH_8.4p1, OpenSSL 1.1.1t 7 Feb 2023", + "x86_64", + "GNU/Linux", + embedded_passwd("admin", 1000, 1000, "/bin/sh", "admin"), + embedded_group("admin", 1000), + embedded_shadow("admin"), + { + "etc/os-release": """NAME="QTS" +ID=qnap +PRETTY_NAME="QNAP QTS 5.1.0" +VERSION_ID="5.1.0" +""", + "etc/issue": "Welcome to QNAP Systems, Inc. \\n \\l\n", + "etc/issue.net": "QNAP Systems, Inc.\n", + "etc/motd": "QNAP NAS\n", + "etc/version": "QTS 5.1.0\n", + "etc/config/uLinux.conf": "[System]\nModel = TS-451+\nVersion = 5.1.0\nBuild Number = 20230912\n", + "etc/default_config/uLinux.conf": "[System]\nModel = TS-451+\n", + }, + "QNAP QTS NAS fingerprint", + ), + profile( + "synology-dsm", + "synology", + "DiskStation", + "admin", + 1000, + 1000, + "linux-x64-lsb", + "4.4.302+", + "#64570 SMP Fri Jul 21 00:00:00 CST 2023", + "SSH-2.0-OpenSSH_8.2", + "OpenSSH_8.2p1, OpenSSL 1.1.1t 7 Feb 2023", + "x86_64", + "GNU/Linux", + embedded_passwd("admin", 1000, 1000, "/bin/sh", "admin"), + embedded_group("admin", 1000), + embedded_shadow("admin"), + { + "etc/os-release": """NAME="Synology DSM" +ID=synology +PRETTY_NAME="Synology DSM 7.1" +VERSION_ID="7.1" +""", + "etc/issue": "Synology DiskStation \\n \\l\n", + "etc/issue.net": "Synology DiskStation\n", + "etc/motd": "Synology DiskStation\n", + "etc.defaults/VERSION": """majorversion="7" +minorversion="1" +productversion="7.1.1" +buildphase="GM" +buildnumber="42962" +smallfixnumber="6" +builddate="2023/06/05" +""", + "etc/VERSION": """majorversion="7" +minorversion="1" +productversion="7.1.1" +buildnumber="42962" +""", + }, + "Synology DSM 7.1 NAS fingerprint", + ), + profile( + "ubiquiti-edgerouter-x", + "ubiquiti", + "ubnt", + "ubnt", + 1000, + 1000, + "linux-mips-lsb", + "4.14.54-UBNT", + "#1 SMP Thu May 25 12:12:35 UTC 2023", + "SSH-2.0-OpenSSH_7.4", + "OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017", + "mips", + "GNU/Linux", + embedded_passwd("ubnt", 1000, 1000, "/bin/vbash", "Ubiquiti"), + embedded_group("ubnt", 1000), + embedded_shadow("ubnt"), + { + "etc/os-release": """PRETTY_NAME="EdgeOS" +NAME="EdgeOS" +ID=edgeos +VERSION_ID="2.0.9" +HOME_URL="https://www.ui.com/" +""", + "etc/issue": "EdgeOS \\n \\l\n", + "etc/issue.net": "EdgeOS\n", + "etc/motd": "Welcome to EdgeOS\n", + "etc/version": "v2.0.9-hotfix.7\n", + "etc/ubnt/version": "EdgeRouter X v2.0.9-hotfix.7\n", + "opt/vyatta/etc/config.boot": "system {\n host-name ubnt\n login {\n user ubnt {\n level admin\n }\n }\n}\n", + }, + "Ubiquiti EdgeRouter CVE-2023-2377-style EdgeOS fingerprint", + ), +] def parse_args(): parser = argparse.ArgumentParser( - description="Derive a custom Cowrie fs.pickle and honeyfs overlay from Cowrie defaults." + description="Generate Cowrie persona fs.pickle, honeyfs, configs, and metadata." ) parser.add_argument("--cowrie-root", required=True, type=Path) - parser.add_argument("--work-dir", default=Path("/tmp/cowrie-custom-fs"), type=Path) + parser.add_argument("--work-dir", default=Path("/tmp/cowrie-personas"), type=Path) return parser.parse_args() @@ -218,15 +891,6 @@ def node_children(node): return node_get(node, A_CONTENTS, []) -def walk(node, path=""): - name = node_get(node, A_NAME, "") - current = "/" if name == "/" else f"{path.rstrip('/')}/{name}" - yield current, node - if node_get(node, A_TYPE) == T_DIR: - for child in node_children(node): - yield from walk(child, current) - - def find_node(root, relative_path): if relative_path in ("", "."): return root @@ -257,6 +921,10 @@ def ensure_dir(root, relative_path, uid=0, gid=0, mode=0o755): match = [part, T_DIR, uid, gid, 4096, stat.S_IFDIR | mode, now, [], None, None] node_children(current).append(match) current = match + node_set(current, A_TYPE, T_DIR) + node_set(current, A_UID, uid) + node_set(current, A_GID, gid) + node_set(current, A_MODE, stat.S_IFDIR | mode) return current @@ -280,12 +948,28 @@ def ensure_file(root, relative_path, content, uid=0, gid=0, mode=0o644): node_set(node, A_MODE, stat.S_IFREG | mode) if not node_get(node, A_CTIME): node_set(node, A_CTIME, now) - node_set(node, A_CONTENTS, []) + node_set(node, A_CONTENTS, content) node_set(node, A_TARGET, None) node_set(node, A_REALFILE, None) return node +def remove_node(root, relative_path): + parts = [part for part in Path(relative_path).parts if part not in ("", "/")] + if not parts: + return + parent = root + for part in parts[:-1]: + parent = next( + (child for child in node_children(parent) if node_get(child, A_NAME) == part), + None, + ) + if parent is None: + return + children = node_children(parent) + children[:] = [child for child in children if node_get(child, A_NAME) != parts[-1]] + + def load_pickle(path): with path.open("rb") as handle: try: @@ -295,125 +979,319 @@ def load_pickle(path): return pickle.load(handle, encoding="utf-8") -def rename_default_home(root): - phil = find_node(root, "home/phil") - if phil is not None: - node_set(phil, A_NAME, LOGIN_USER) - node_set(phil, A_UID, LOGIN_UID) - node_set(phil, A_GID, LOGIN_GID) - node_set(phil, A_MODE, stat.S_IFDIR | 0o755) - for child in node_children(phil): - node_set(child, A_UID, LOGIN_UID) - node_set(child, A_GID, LOGIN_GID) - - -def update_pickle_metadata(root): - rename_default_home(root) - ensure_dir(root, "home/ubuntu", LOGIN_UID, LOGIN_GID, 0o755) - ensure_dir(root, "etc", 0, 0, 0o755) - ensure_dir(root, "proc", 0, 0, 0o555) - - for relative_path, text in TEXT_FILES.items(): - mode = 0o644 - if relative_path == "etc/shadow": - mode = 0o640 - ensure_file(root, relative_path, text.encode("utf-8"), 0, 0, mode) - - for name, text in HOME_FILES.items(): - ensure_file( - root, - f"home/ubuntu/{name}", - text.encode("utf-8"), - LOGIN_UID, - LOGIN_GID, - 0o644, - ) - - def write_text_file(path, text, mode=0o644): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") os.chmod(path, mode) -def copy_and_patch_honeyfs(source, target): - if target.exists(): - shutil.rmtree(target) - if source.is_dir(): - shutil.copytree(source, target, copy_function=shutil.copy2) - else: - target.mkdir(parents=True) +def apply_persona_to_tree(tree, persona): + for remove_path in persona["remove_paths"]: + remove_node(tree, remove_path) - for relative_path, text in TEXT_FILES.items(): + for relative_path in COMMON_DIRS: + mode = 0o755 + if relative_path == "proc": + mode = 0o555 + if relative_path == "tmp": + mode = 0o1777 + ensure_dir(tree, relative_path, 0, 0, mode) + + user = persona["user"] + uid = persona["uid"] + gid = persona["gid"] + if user == "root": + ensure_dir(tree, "root", 0, 0, 0o700) + else: + ensure_dir(tree, f"home/{user}", uid, gid, 0o755) + + for relative_path, text in persona["files"].items(): mode = 0o644 if relative_path == "etc/shadow": mode = 0o640 + if relative_path.startswith("bin/"): + mode = 0o755 + ensure_file(tree, relative_path, text.encode("utf-8"), 0, 0, mode) + + for relative_path in TXTCMD_PATHS: + ensure_file(tree, relative_path, b"", 0, 0, 0o755) + + +def write_honeyfs(target, persona): + if target.exists(): + shutil.rmtree(target) + target.mkdir(parents=True) + + user = persona["user"] + uid = persona["uid"] + gid = persona["gid"] + for relative_path in COMMON_DIRS: + path = target / relative_path + path.mkdir(parents=True, exist_ok=True) + os.chmod(path, 0o1777 if relative_path == "tmp" else 0o755) + + home_path = target / ("root" if user == "root" else f"home/{user}") + home_path.mkdir(parents=True, exist_ok=True) + os.chmod(home_path, 0o700 if user == "root" else 0o755) + + for relative_path, text in persona["files"].items(): + mode = 0o644 + if relative_path == "etc/shadow": + mode = 0o640 + if relative_path.startswith("bin/"): + mode = 0o755 write_text_file(target / relative_path, text, mode) - home = target / "home" / LOGIN_USER - home.mkdir(parents=True, exist_ok=True) - os.chmod(home, 0o755) - for name, text in HOME_FILES.items(): - write_text_file(home / name, text, 0o644) + try: + os.chown(home_path, uid, gid) + except OSError: + pass -def validate_no_phil(pickle_path, honeyfs): +def write_txtcmds(target, persona): + if target.exists(): + shutil.rmtree(target) + for relative_path, text in txtcmds_for_persona(persona).items(): + write_text_file(target / relative_path, text, 0o644) + + +def write_cmdoutput(path, persona): + write_text_file( + path, + json.dumps(cmdoutput_for_persona(persona), indent=2, sort_keys=True) + "\n", + 0o644, + ) + + +def render_cowrie_cfg(persona, cowrie_root, persona_dir): + return f"""[honeypot] +hostname = {persona["hostname"]} +log_path = {cowrie_root}/log +logtype = plain +download_path = {cowrie_root}/dl +share_path = {cowrie_root}/src/cowrie/data/share/cowrie +state_path = /tmp/cowrie/data +etc_path = {cowrie_root}/etc +contents_path = {persona_dir}/honeyfs +txtcmds_path = {persona_dir}/txtcmds +ttylog = true +ttylog_path = {cowrie_root}/log/tty +interactive_timeout = 180 +authentication_timeout = 120 +backend = shell +timezone = UTC +auth_class = AuthRandom +auth_class_parameters = 2, 5, 10 +data_path = {cowrie_root}/src/cowrie/data + +[shell] +filesystem = {persona_dir}/fs.pickle +processes = {persona_dir}/cmdoutput.json +arch = {persona["arch"]} +kernel_version = {persona["kernel_version"]} +kernel_build_string = {persona["kernel_build_string"]} +hardware_platform = {persona["hardware_platform"]} +operating_system = {persona["operating_system"]} +ssh_version = {persona["shell_ssh_version"]} + +[ssh] +enabled = true +rsa_public_key = {cowrie_root}/etc/ssh_host_rsa_key.pub +rsa_private_key = {cowrie_root}/etc/ssh_host_rsa_key +dsa_public_key = {cowrie_root}/etc/ssh_host_dsa_key.pub +dsa_private_key = {cowrie_root}/etc/ssh_host_dsa_key +ecdsa_public_key = {cowrie_root}/etc/ssh_host_ecdsa_key.pub +ecdsa_private_key = {cowrie_root}/etc/ssh_host_ecdsa_key +ed25519_public_key = {cowrie_root}/etc/ssh_host_ed25519_key.pub +ed25519_private_key = {cowrie_root}/etc/ssh_host_ed25519_key +public_key_auth = ssh-rsa,ssh-dss,ecdsa-sha2-nistp256,ssh-ed25519 +version = {persona["ssh_banner"]} +ciphers = aes128-ctr,aes192-ctr,aes256-ctr,aes256-cbc,aes192-cbc,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc +macs = hmac-sha2-512,hmac-sha2-384,hmac-sha2-56,hmac-sha1,hmac-md5 +compression = zlib@openssh.com,zlib,none +listen_endpoints = tcp:22:interface=0.0.0.0 +sftp_enabled = true +forwarding = false +forward_redirect = false +forward_tunnel = false +auth_none_enabled = false +auth_keyboard_interactive_enabled = true +auth_publickey_allow_any = true + +[telnet] +enabled = true +listen_endpoints = tcp:23:interface=0.0.0.0 +reported_port = 23 +cve_2026_24061_vulnerable = true + +[output_jsonlog] +enabled = true +logfile = {cowrie_root}/log/cowrie.json +epoch_timestamp = false + +[output_textlog] +enabled = false +logfile = {cowrie_root}/log/cowrie-textlog.log +format = text + +[output_crashreporter] +enabled = false +debug = false +""" + + +def validate_no_marker(path, marker, offenders): + marker_bytes = marker.lower().encode("utf-8") + if path.is_file() and marker_bytes in path.read_bytes().lower(): + offenders.append(str(path)) + + +def validate_persona(persona, persona_dir, runtime_persona_dir): + pickle_path = persona_dir / "fs.pickle" + honeyfs = persona_dir / "honeyfs" + config_path = persona_dir / "cowrie.cfg" + cmdoutput_path = persona_dir / "cmdoutput.json" + txtcmds = persona_dir / "txtcmds" offenders = [] - if b"phil" in pickle_path.read_bytes().lower(): - offenders.append(str(pickle_path)) - for item in honeyfs.rglob("*"): - if "phil" in item.name.lower(): - offenders.append(str(item)) - continue - if item.is_file() and b"phil" in item.read_bytes().lower(): - offenders.append(str(item)) - - if offenders: - raise RuntimeError("Generated Cowrie filesystem still contains 'phil': " + ", ".join(offenders)) - - -def validate_expected_markers(pickle_path, honeyfs): - pickle_bytes = pickle_path.read_bytes() if pickle_path.stat().st_size < MIN_PICKLE_SIZE: raise RuntimeError( - f"Generated pickle is unexpectedly small: {pickle_path.stat().st_size} bytes" + f"{persona['id']} generated pickle is unexpectedly small: {pickle_path.stat().st_size} bytes" ) - if LOGIN_USER.encode("ascii") not in pickle_bytes: - raise RuntimeError(f"Generated pickle does not contain {LOGIN_USER}") - if HOSTNAME.encode("ascii") not in (honeyfs / "etc" / "hostname").read_bytes(): - raise RuntimeError(f"Generated honeyfs does not contain {HOSTNAME}") - if not (honeyfs / "home" / LOGIN_USER).is_dir(): - raise RuntimeError(f"Generated honeyfs does not contain /home/{LOGIN_USER}") + + for marker in FORBIDDEN_MARKERS: + validate_no_marker(pickle_path, marker, offenders) + + for root in (honeyfs, txtcmds): + for item in root.rglob("*"): + if "phil" in item.name.lower(): + offenders.append(str(item)) + continue + if item.is_file(): + for marker in FORBIDDEN_MARKERS: + validate_no_marker(item, marker, offenders) + + for marker in FORBIDDEN_MARKERS: + validate_no_marker(config_path, marker, offenders) + validate_no_marker(cmdoutput_path, marker, offenders) + + if offenders: + raise RuntimeError( + f"{persona['id']} generated persona contains forbidden markers: " + + ", ".join(offenders) + ) + + pickle_bytes = pickle_path.read_bytes() + required = [ + persona["user"].encode("utf-8"), + persona["hostname"].encode("utf-8"), + ] + for marker in required: + if marker not in pickle_bytes: + raise RuntimeError(f"{persona['id']} pickle does not contain {marker!r}") + + if persona["ssh_banner"] not in config_path.read_text(encoding="utf-8"): + raise RuntimeError(f"{persona['id']} config does not contain SSH banner") + config_text = config_path.read_text(encoding="utf-8") + if f"{runtime_persona_dir}/cmdoutput.json" not in config_text: + raise RuntimeError(f"{persona['id']} config does not point to cmdoutput.json") + if f"{runtime_persona_dir}/txtcmds" not in config_text: + raise RuntimeError(f"{persona['id']} config does not point to txtcmds") + if not cmdoutput_path.is_file(): + raise RuntimeError(f"{persona['id']} has no cmdoutput.json") + if not txtcmds.is_dir(): + raise RuntimeError(f"{persona['id']} has no txtcmds directory") + if not json.loads(cmdoutput_path.read_text(encoding="utf-8"))["command"]["ps"]: + raise RuntimeError(f"{persona['id']} has empty process output") + process_list = json.loads(cmdoutput_path.read_text(encoding="utf-8"))["command"]["ps"] + expected_start = persona["process_start"] + if any(process.get("START") != expected_start for process in process_list): + raise RuntimeError(f"{persona['id']} has inconsistent process start dates") + for relative_path in TXTCMD_PATHS: + if not (txtcmds / relative_path).is_file(): + raise RuntimeError(f"{persona['id']} has no txtcmds/{relative_path}") + if not (honeyfs / "etc" / "hostname").is_file(): + raise RuntimeError(f"{persona['id']} honeyfs has no /etc/hostname") + + +def build_persona(default_tree, cowrie_root, personas_root, persona): + persona_dir = personas_root / persona["id"] + runtime_persona_dir = cowrie_root / PERSONAS_DIRNAME / persona["id"] + if persona_dir.exists(): + shutil.rmtree(persona_dir) + persona_dir.mkdir(parents=True) + + tree = copy.deepcopy(default_tree) + apply_persona_to_tree(tree, persona) + + with (persona_dir / "fs.pickle").open("wb") as handle: + pickle.dump(tree, handle) + + write_honeyfs(persona_dir / "honeyfs", persona) + write_cmdoutput(persona_dir / "cmdoutput.json", persona) + write_txtcmds(persona_dir / "txtcmds", persona) + write_text_file( + persona_dir / "cowrie.cfg", + render_cowrie_cfg(persona, cowrie_root, runtime_persona_dir), + 0o644, + ) + validate_persona(persona, persona_dir, runtime_persona_dir) + + +def write_metadata(personas_root): + metadata = [] + for persona in PERSONAS: + metadata.append( + { + "id": persona["id"], + "family": persona["family"], + "hostname": persona["hostname"], + "user": persona["user"], + "arch": persona["arch"], + "ssh_banner": persona["ssh_banner"], + "vulnerability": persona["vulnerability"], + } + ) + write_text_file( + personas_root / "personas.json", + json.dumps(metadata, indent=2, sort_keys=True) + "\n", + 0o644, + ) def main(): args = parse_args() cowrie_root = args.cowrie_root.resolve() work_dir = args.work_dir.resolve() - pickle_path = cowrie_root / "src" / "cowrie" / "data" / "fs.pickle" - source_honeyfs = cowrie_root / "honeyfs" - generated_pickle_path = work_dir / "fs.pickle" - generated_honeyfs = work_dir / "honeyfs" + default_pickle_path = cowrie_root / "src" / "cowrie" / "data" / "fs.pickle" + personas_root = cowrie_root / PERSONAS_DIRNAME if work_dir.exists(): shutil.rmtree(work_dir) work_dir.mkdir(parents=True) - tree = load_pickle(pickle_path) - update_pickle_metadata(tree) - with generated_pickle_path.open("wb") as handle: - pickle.dump(tree, handle) + default_tree = load_pickle(default_pickle_path) + generated_root = work_dir / PERSONAS_DIRNAME + generated_root.mkdir() - copy_and_patch_honeyfs(source_honeyfs, generated_honeyfs) + ids = [persona["id"] for persona in PERSONAS] + if len(ids) != 10 or len(set(ids)) != len(ids): + raise RuntimeError("Expected exactly 10 unique Cowrie personas") - validate_no_phil(generated_pickle_path, generated_honeyfs) - validate_expected_markers(generated_pickle_path, generated_honeyfs) + for persona in PERSONAS: + build_persona(default_tree, cowrie_root, generated_root, persona) + write_metadata(generated_root) - shutil.move(generated_pickle_path, pickle_path) + if personas_root.exists(): + shutil.rmtree(personas_root) + shutil.copytree(generated_root, personas_root, copy_function=shutil.copy2) + + bootstrap = personas_root / DEFAULT_BOOTSTRAP_PERSONA + shutil.copy2(bootstrap / "fs.pickle", default_pickle_path) + source_honeyfs = cowrie_root / "honeyfs" if source_honeyfs.exists(): shutil.rmtree(source_honeyfs) - shutil.copytree(generated_honeyfs, source_honeyfs, copy_function=shutil.copy2) + shutil.copytree(bootstrap / "honeyfs", source_honeyfs, copy_function=shutil.copy2) if __name__ == "__main__": diff --git a/docker/cowrie/dist/patch_cowrie_persona_support.py b/docker/cowrie/dist/patch_cowrie_persona_support.py new file mode 100755 index 00000000..505a010a --- /dev/null +++ b/docker/cowrie/dist/patch_cowrie_persona_support.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +import argparse +from pathlib import Path + + +def replace_once(path, old, new): + text = path.read_text(encoding="utf-8") + if old not in text: + raise RuntimeError(f"Could not find expected text in {path}") + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def replace_all(path, old, new): + text = path.read_text(encoding="utf-8") + if old not in text: + raise RuntimeError(f"Could not find expected text in {path}") + path.write_text(text.replace(old, new), encoding="utf-8") + + +def patch_protocol(cowrie_root): + path = cowrie_root / "src" / "cowrie" / "shell" / "protocol.py" + replace_once(path, "import traceback\n", "import traceback\nfrom pathlib import Path\n") + replace_once( + path, + """ try: + binary_data = read_data_bytes("txtcmds", *path.lstrip("/").split("/")) + return self.txtcmd(binary_data) + except FileNotFoundError: + pass +""", + """ txtcmds_path = CowrieConfig.get("honeypot", "txtcmds_path", fallback="") + if txtcmds_path: + operator_cmd = Path(txtcmds_path) / path.lstrip("/") + if operator_cmd.is_file(): + return self.txtcmd(operator_cmd.read_bytes()) + + try: + binary_data = read_data_bytes("txtcmds", *path.lstrip("/").split("/")) + return self.txtcmd(binary_data) + except FileNotFoundError: + pass +""", + ) + + +def patch_ssh(cowrie_root): + path = cowrie_root / "src" / "cowrie" / "commands" / "ssh.py" + replace_once( + path, + """ self.write( + f"Linux {self.protocol.hostname} 2.6.26-2-686 #1 SMP Wed Nov 4 20:45:37 \\ + UTC 2009 i686\\n" + ) +""", + """ kernel_version = CowrieConfig.get("shell", "kernel_version", fallback="5.10.0") + kernel_build = CowrieConfig.get("shell", "kernel_build_string", fallback="#1 SMP") + hardware = CowrieConfig.get("shell", "hardware_platform", fallback="x86_64") + operating_system = CowrieConfig.get("shell", "operating_system", fallback="GNU/Linux") + self.write( + f"Linux {self.protocol.hostname} {kernel_version} {kernel_build} " + f"{hardware} {operating_system}\\n" + ) +""", + ) + + +def patch_netstat(cowrie_root): + path = cowrie_root / "src" / "cowrie" / "commands" / "netstat.py" + replace_all(path, "@/com/ubuntu/upstart", "/run/systemd/private") + replace_once( + path, + "Fred Baumgarten, Alan Cox, Bernd Eckenfels, Phil Blundell, Tuan Hoang and others\\n", + "Fred Baumgarten, Alan Cox, Bernd Eckenfels, Tuan Hoang and others\\n", + ) + + +def patch_service(cowrie_root): + path = cowrie_root / "src" / "cowrie" / "commands" / "service.py" + text = path.read_text(encoding="utf-8") + start = text.index(" output = (\n") + end = text.index(" )\n for line in output:", start) + len(" )\n") + replacement = """ output = ( + "[ + ] cron", + "[ + ] dbus", + "[ + ] networking", + "[ + ] ssh", + "[ + ] rsyslog", + "[ + ] udev", + "[ + ] systemd-journald", + "[ + ] systemd-networkd", + "[ - ] bluetooth", + "[ - ] cups", + "[ - ] nfs-server", + "[ - ] postfix", + "[ - ] rpcbind", + ) +""" + path.write_text(text[:start] + replacement + text[end:], encoding="utf-8") + + +def main(): + parser = argparse.ArgumentParser(description="Patch Cowrie v3 persona command support.") + parser.add_argument("--cowrie-root", required=True, type=Path) + args = parser.parse_args() + cowrie_root = args.cowrie_root.resolve() + patch_protocol(cowrie_root) + patch_ssh(cowrie_root) + patch_netstat(cowrie_root) + patch_service(cowrie_root) + + +if __name__ == "__main__": + main() diff --git a/docker/cowrie/dist/start-cowrie-persona.py b/docker/cowrie/dist/start-cowrie-persona.py new file mode 100755 index 00000000..718c4023 --- /dev/null +++ b/docker/cowrie/dist/start-cowrie-persona.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +import json +import os +import random +import shutil +import sys +from pathlib import Path + + +COWRIE_ROOT = Path("/home/cowrie/cowrie") +PERSONAS_ROOT = COWRIE_ROOT / "personas" +RUNTIME_ROOT = Path("/tmp/cowrie/runtime") +SELECTED_PERSONA_FILE = Path("/tmp/cowrie/persona") + + +def load_personas(): + metadata_path = PERSONAS_ROOT / "personas.json" + with metadata_path.open(encoding="utf-8") as handle: + personas = json.load(handle) + if not isinstance(personas, list) or not personas: + raise RuntimeError(f"No personas found in {metadata_path}") + return personas + + +def choose_persona(personas): + requested = os.environ.get("COWRIE_PERSONA", "").strip() + ids = [persona["id"] for persona in personas] + if requested and requested != "random": + if requested not in ids: + raise RuntimeError( + f"Unknown COWRIE_PERSONA={requested!r}; expected one of: {', '.join(ids)}" + ) + return requested + return random.choice(ids) + + +def activate_persona(persona_id): + persona_dir = PERSONAS_ROOT / persona_id + config_path = persona_dir / "cowrie.cfg" + if not config_path.is_file(): + raise RuntimeError(f"Persona config not found: {config_path}") + if not (persona_dir / "fs.pickle").is_file(): + raise RuntimeError(f"Persona fs.pickle not found: {persona_dir / 'fs.pickle'}") + if not (persona_dir / "honeyfs").is_dir(): + raise RuntimeError(f"Persona honeyfs not found: {persona_dir / 'honeyfs'}") + + RUNTIME_ROOT.mkdir(parents=True, exist_ok=True) + shutil.copy2(config_path, RUNTIME_ROOT / "cowrie.cfg") + SELECTED_PERSONA_FILE.write_text(persona_id + "\n", encoding="utf-8") + + +def main(): + try: + personas = load_personas() + persona_id = choose_persona(personas) + activate_persona(persona_id) + except Exception as exc: + print(f"Could not activate Cowrie persona: {exc}", file=sys.stderr) + return 1 + + print(f"Starting Cowrie persona: {persona_id}", flush=True) + os.environ.setdefault("PYTHONPATH", f"{COWRIE_ROOT}:{COWRIE_ROOT / 'src'}") + os.chdir(RUNTIME_ROOT) + os.execv( + "/usr/bin/twistd", + [ + "/usr/bin/twistd", + "--nodaemon", + "--pidfile", + "/tmp/cowrie/cowrie.pid", + "cowrie", + ], + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())