mirror of
https://github.com/telekom-security/tpotce.git
synced 2026-05-29 17:24:15 +00:00
Enhance Cowrie persona support by adding package manager handling and skip command filtering in protocol.py
This commit is contained in:
parent
aefe3c7dac
commit
e1d6d376dc
3 changed files with 351 additions and 3 deletions
|
|
@ -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")
|
||||
|
|
|
|||
231
docker/cowrie/dist/generate_cowrie_fs.py
vendored
231
docker/cowrie/dist/generate_cowrie_fs.py
vendored
|
|
@ -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"],
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue