Enhance Cowrie persona support by adding package manager handling and skip command filtering in protocol.py

This commit is contained in:
t3chn0m4g3 2026-05-28 14:49:17 +02:00
parent aefe3c7dac
commit e1d6d376dc
3 changed files with 351 additions and 3 deletions

View file

@ -433,6 +433,7 @@ assert_custom_filesystem() {
docker exec -i "${TEST_CONTAINER_NAME}" python3 - <<'PY'
import configparser
import json
import pickle
from pathlib import Path
import sys
@ -441,7 +442,40 @@ 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")
protocol_path = root / "src" / "cowrie" / "shell" / "protocol.py"
offenders = []
expected_package_managers = {
"debian-bookworm-vuln": {"apt", "apt-get", "dpkg"},
"fedora-36-vuln": {"dnf", "rpm", "yum"},
"rhel-9-vuln": {"dnf", "rpm", "yum"},
"dlink-dir859": set(),
"tplink-wr841n": set(),
"zyxel-nas326": set(),
"openwrt-1806": {"opkg"},
"qnap-qts": {"qpkg_cli"},
"synology-dsm": {"synopkg"},
"ubiquiti-edgerouter-x": {"apt-get", "dpkg"},
}
package_manager_paths = (
"bin/apt",
"bin/apt-get",
"bin/dnf",
"bin/opkg",
"bin/rpm",
"bin/yum",
"sbin/opkg",
"sbin/qpkg_cli",
"usr/bin/apt",
"usr/bin/apt-get",
"usr/bin/dnf",
"usr/bin/dpkg",
"usr/bin/opkg",
"usr/bin/rpm",
"usr/bin/yum",
"usr/sbin/opkg",
"usr/sbin/qpkg_cli",
"usr/syno/bin/synopkg",
)
def read_bytes(path):
@ -474,12 +508,29 @@ def check_forbidden(path):
offenders.append(str(path))
def node_children(node):
return node[7] if len(node) > 7 and isinstance(node[7], list) else []
def find_node(root_node, relative_path):
current = root_node
for part in Path(relative_path).parts:
if part in ("", "/"):
continue
current = next((child for child in node_children(current) if child[0] == part), None)
if current is None:
return None
return current
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}")
if "skip_python_commands" not in protocol_path.read_text(encoding="utf-8"):
fail("Cowrie protocol.py does not contain persona command filtering patch")
personas = json.loads(metadata_path.read_text(encoding="utf-8"))
if len(personas) != 10:
@ -508,6 +559,8 @@ 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")
if "apt" not in config.get("shell", "skip_python_commands", fallback=""):
fail("Runtime config does not disable generic package-manager Python commands")
for persona_id, persona in ids.items():
persona_dir = personas_root / persona_id
@ -531,6 +584,8 @@ for persona_id, persona in ids.items():
fail(f"{persona_id} fs.pickle is unexpectedly small: {pickle_path.stat().st_size} bytes")
pickle_bytes = read_bytes(pickle_path)
with pickle_path.open("rb") as handle:
pickle_tree = pickle.load(handle)
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:
@ -540,6 +595,12 @@ for persona_id, persona in ids.items():
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")
config_text = config_path.read_text(encoding="utf-8")
if "skip_python_commands =" not in config_text:
fail(f"{persona_id} config does not define skip_python_commands")
package_managers = set(persona.get("package_managers", []))
if package_managers != expected_package_managers[persona_id]:
fail(f"{persona_id} package managers do not match persona: {sorted(package_managers)}")
for command_path in (
"bin/df",
"bin/dmesg",
@ -551,6 +612,15 @@ for persona_id, persona in ids.items():
):
if not (txtcmds_path / command_path).is_file():
fail(f"{persona_id} txtcmds is missing {command_path}")
for command_path in package_manager_paths:
manager_name = Path(command_path).name
should_exist = manager_name in package_managers
exists_in_pickle = find_node(pickle_tree, command_path) is not None
exists_in_txtcmds = (txtcmds_path / command_path).is_file()
if should_exist and exists_in_txtcmds and not exists_in_pickle:
fail(f"{persona_id} package manager is missing from fs.pickle: {command_path}")
if not should_exist and (exists_in_pickle or exists_in_txtcmds):
fail(f"{persona_id} has mismatched package manager command: {command_path}")
passwd = read_bytes(honeyfs / "etc" / "passwd")
hostname = read_bytes(honeyfs / "etc" / "hostname")

View file

@ -34,6 +34,70 @@ TXTCMD_PATHS = [
"usr/bin/top",
]
RANDOM = random.SystemRandom()
PACKAGE_MANAGER_PATHS = [
"bin/apt",
"bin/apt-get",
"bin/dnf",
"bin/opkg",
"bin/rpm",
"bin/yum",
"sbin/opkg",
"sbin/qpkg_cli",
"usr/bin/apt",
"usr/bin/apt-get",
"usr/bin/dnf",
"usr/bin/dpkg",
"usr/bin/opkg",
"usr/bin/rpm",
"usr/bin/yum",
"usr/sbin/opkg",
"usr/sbin/qpkg_cli",
"usr/syno/bin/synopkg",
]
HIGH_VARIANCE_COMMAND_PATHS = [
"bin/busybox",
"usr/bin/gcc",
"usr/bin/git",
"usr/bin/lspci",
"usr/bin/perl",
"usr/bin/python",
"usr/bin/python3",
"usr/bin/sudo",
"usr/sbin/service",
]
COMMON_SKIP_PYTHON_COMMANDS = [
"/bin/apt",
"/bin/apt-get",
"/bin/busybox",
"/bin/yum",
"/usr/bin/apt",
"/usr/bin/apt-get",
"/usr/bin/busybox",
"/usr/bin/gcc",
"/usr/bin/lspci",
"/usr/bin/sudo",
"/usr/bin/yum",
"/usr/sbin/service",
"apt",
"apt-get",
"busybox",
"gcc",
"lspci",
"sudo",
"yum",
"service",
]
EMBEDDED_SKIP_PYTHON_COMMANDS = [
"adduser",
"finger",
"git",
"groups",
"perl",
"python",
"python3",
"scp",
"systemctl",
]
A_NAME = 0
A_TYPE = 1
@ -261,6 +325,144 @@ def random_ps_start():
return day.strftime("%b%d")
def package_manager_txtcmds(persona):
family = persona["family"]
if family == "debian":
apt_version = "apt 2.6.1 (amd64)\n"
apt_help = """apt 2.6.1 (amd64)
Usage: apt [options] command
apt is a commandline package manager and provides commands for
searching and managing as well as querying information about packages.
Most used commands:
list - list packages based on package names
search - search in package descriptions
show - show package details
update - update list of available packages
install - install packages
remove - remove packages
upgrade - upgrade the system by installing/upgrading packages
"""
dpkg_version = "Debian 'dpkg' package management program version 1.21.22 (amd64).\n"
return {
"usr/bin/apt": apt_help,
"usr/bin/apt-get": apt_version,
"usr/bin/dpkg": dpkg_version,
}
if family == "fedora":
dnf = """dnf 4.14.0
usage: dnf [options] COMMAND
List of Main Commands:
install install a package or packages on your system
remove remove a package or packages from your system
upgrade upgrade a package or packages on your system
search search package details for the given string
repolist display the configured software repositories
"""
rpm = "RPM version 4.17.0\n"
return {
"usr/bin/dnf": dnf,
"usr/bin/yum": dnf,
"usr/bin/rpm": rpm,
}
if family == "rhel":
dnf = """dnf 4.14.0
usage: dnf [options] COMMAND
List of Main Commands:
install install a package or packages on your system
remove remove a package or packages from your system
upgrade upgrade a package or packages on your system
repolist display the configured software repositories
module interact with modular content
"""
rpm = "RPM version 4.16.1.3\n"
return {
"usr/bin/dnf": dnf,
"usr/bin/yum": dnf,
"usr/bin/rpm": rpm,
}
if family == "openwrt":
opkg = """opkg must have one sub-command argument
usage: opkg [options...] sub-command [arguments...]
Package Manipulation:
update Update list of available packages
install <pkg> Install package(s)
remove <pkg> Remove package(s)
list-installed List installed packages
"""
return {
"bin/opkg": opkg,
}
if family == "ubiquiti":
apt = """Reading package lists... Done
Building dependency tree... Done
E: Unable to locate package
"""
dpkg = "Debian 'dpkg' package management program version 1.19.8 (mips).\n"
return {
"usr/bin/apt-get": apt,
"usr/bin/dpkg": dpkg,
}
if family == "qnap":
qpkg = """Usage: qpkg_cli [options]
--list list installed packages
--status NAME show package status
--help show this help
"""
return {
"sbin/qpkg_cli": qpkg,
}
if family == "synology":
synopkg = """Copyright (c) 2003-2023 Synology Inc. All rights reserved.
Usage: synopkg <command> [package]
list
status <package>
start <package>
stop <package>
"""
return {
"usr/syno/bin/synopkg": synopkg,
}
return {}
def command_inventory_txtcmds(persona):
family = persona["family"]
txtcmds = {}
if family in {"debian", "fedora", "rhel"}:
txtcmds.update(
{
"usr/bin/lspci": "00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC\n00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA\n00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE\n00:02.0 VGA compatible controller: Device 1234:1111\n",
"usr/bin/sudo": "sudo: a password is required\n",
"usr/sbin/service": "Usage: service < option > | --status-all | [ service_name [ command | --full-restart ] ]\n",
}
)
elif family in {"qnap", "synology"}:
txtcmds["usr/bin/lspci"] = (
"00:00.0 Host bridge: Intel Corporation Atom Processor C3000 Host Bridge\n"
"00:14.0 USB controller: Intel Corporation Atom Processor C3000 USB 3.0 xHCI Controller\n"
"00:17.0 SATA controller: Intel Corporation Atom Processor C3000 SATA Controller\n"
)
if family in {"iot-router", "iot-nas", "openwrt", "qnap", "synology", "ubiquiti"}:
if "bin/busybox" in persona["files"]:
txtcmds["bin/busybox"] = persona["files"]["bin/busybox"]
return txtcmds
def skip_python_commands_for_family(family):
commands = list(COMMON_SKIP_PYTHON_COMMANDS)
if family in {"iot-router", "iot-nas", "openwrt", "qnap", "synology", "ubiquiti"}:
commands.extend(EMBEDDED_SKIP_PYTHON_COMMANDS)
return sorted(set(commands))
def ps_entry(user, pid, command, cpu=0.0, mem=0.1, vsz=0, rss=0, stat="S", tty="?", start=None):
return {
"USER": user,
@ -418,7 +620,7 @@ cpu time (seconds, -t) unlimited
max user processes (-u) 1024
virtual memory (kbytes, -v) unlimited
"""
return {
txtcmds = {
"bin/df": df,
"bin/dmesg": dmesg,
"bin/mount": mount,
@ -427,6 +629,9 @@ virtual memory (kbytes, -v) unlimited
"usr/bin/nproc": f"{cpu_count}\n",
"usr/bin/top": top,
}
txtcmds.update(package_manager_txtcmds(persona))
txtcmds.update(command_inventory_txtcmds(persona))
return txtcmds
def profile(
@ -488,6 +693,7 @@ def profile(
"vulnerability": vulnerability,
"shell": shell,
"process_start": random_ps_start(),
"skip_python_commands": skip_python_commands_for_family(family),
}
@ -988,6 +1194,8 @@ def write_text_file(path, text, mode=0o644):
def apply_persona_to_tree(tree, persona):
for remove_path in persona["remove_paths"]:
remove_node(tree, remove_path)
for remove_path in PACKAGE_MANAGER_PATHS + HIGH_VARIANCE_COMMAND_PATHS:
remove_node(tree, remove_path)
for relative_path in COMMON_DIRS:
mode = 0o755
@ -1013,7 +1221,7 @@ def apply_persona_to_tree(tree, persona):
mode = 0o755
ensure_file(tree, relative_path, text.encode("utf-8"), 0, 0, mode)
for relative_path in TXTCMD_PATHS:
for relative_path in txtcmds_for_persona(persona):
ensure_file(tree, relative_path, b"", 0, 0, 0o755)
@ -1093,6 +1301,7 @@ kernel_build_string = {persona["kernel_build_string"]}
hardware_platform = {persona["hardware_platform"]}
operating_system = {persona["operating_system"]}
ssh_version = {persona["shell_ssh_version"]}
skip_python_commands = {",".join(persona["skip_python_commands"])}
[ssh]
enabled = true
@ -1182,6 +1391,7 @@ def validate_persona(persona, persona_dir, runtime_persona_dir):
)
pickle_bytes = pickle_path.read_bytes()
pickle_tree = load_pickle(pickle_path)
required = [
persona["user"].encode("utf-8"),
persona["hostname"].encode("utf-8"),
@ -1207,9 +1417,21 @@ def validate_persona(persona, persona_dir, runtime_persona_dir):
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:
expected_txtcmds = txtcmds_for_persona(persona)
for relative_path in expected_txtcmds:
if not (txtcmds / relative_path).is_file():
raise RuntimeError(f"{persona['id']} has no txtcmds/{relative_path}")
for relative_path in PACKAGE_MANAGER_PATHS:
has_command = relative_path in expected_txtcmds
in_pickle = find_node(pickle_tree, relative_path) is not None
in_honeyfs = (honeyfs / relative_path).exists()
if has_command and not in_pickle:
raise RuntimeError(f"{persona['id']} package manager missing from pickle: {relative_path}")
if not has_command and (in_pickle or in_honeyfs):
raise RuntimeError(f"{persona['id']} has mismatched package manager path: {relative_path}")
config_text = config_path.read_text(encoding="utf-8")
if "skip_python_commands =" not in config_text:
raise RuntimeError(f"{persona['id']} config does not define skip_python_commands")
if not (honeyfs / "etc" / "hostname").is_file():
raise RuntimeError(f"{persona['id']} honeyfs has no /etc/hostname")
@ -1249,6 +1471,9 @@ def write_metadata(personas_root):
"user": persona["user"],
"arch": persona["arch"],
"ssh_banner": persona["ssh_banner"],
"package_managers": sorted(
Path(path).name for path in package_manager_txtcmds(persona)
),
"vulnerability": persona["vulnerability"],
}
)

View file

@ -21,6 +21,50 @@ def replace_all(path, old, new):
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,
""" if cmd in self.commands:
return self.commands[cmd]
if cmd[0] in (".", "/"):
path = self.fs.resolve_path(cmd, self.cwd)
if not self.fs.exists(path):
return None
else:
for i in [f"{self.fs.resolve_path(x, self.cwd)}/{cmd}" for x in paths]:
if self.fs.exists(i):
path = i
break
if path is None:
return None
""",
""" skip_python_commands = {
command.strip()
for command in CowrieConfig.get(
"shell", "skip_python_commands", fallback=""
).split(",")
if command.strip()
}
def skip_python_command(name):
return name in skip_python_commands or Path(name).name in skip_python_commands
if cmd in self.commands and not skip_python_command(cmd):
return self.commands[cmd]
if cmd[0] in (".", "/"):
path = self.fs.resolve_path(cmd, self.cwd)
if not self.fs.exists(path):
return None
else:
for i in [f"{self.fs.resolve_path(x, self.cwd)}/{cmd}" for x in paths]:
if self.fs.exists(i):
path = i
break
if path is None:
return None
""",
)
replace_once(
path,
""" try:
@ -42,6 +86,15 @@ def patch_protocol(cowrie_root):
pass
""",
)
replace_once(
path,
""" if path in self.commands:
return self.commands[path]
""",
""" if path in self.commands and not skip_python_command(path):
return self.commands[path]
""",
)
def patch_ssh(cowrie_root):