mirror of
https://github.com/telekom-security/tpotce.git
synced 2026-05-29 17:24:15 +00:00
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.
This commit is contained in:
parent
05e3148cf4
commit
aefe3c7dac
6 changed files with 1390 additions and 217 deletions
|
|
@ -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/conpot.sh --guardian-ast-port 11001 --ipmi-port 1623
|
||||||
./docker/_tests/tests/cowrie.sh
|
./docker/_tests/tests/cowrie.sh
|
||||||
./docker/_tests/tests/cowrie.sh --ssh-port 2222 --telnet-port 2323
|
./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
|
## Conventions
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ DEFAULT_IMAGE="ghcr.io/telekom-security/cowrie:24.04.1"
|
||||||
IMAGE=""
|
IMAGE=""
|
||||||
SSH_PORT=""
|
SSH_PORT=""
|
||||||
TELNET_PORT=""
|
TELNET_PORT=""
|
||||||
|
PERSONA=""
|
||||||
LOG_DIR=""
|
LOG_DIR=""
|
||||||
TTY_LOG_DIR=""
|
TTY_LOG_DIR=""
|
||||||
DL_DIR=""
|
DL_DIR=""
|
||||||
|
|
@ -29,6 +30,7 @@ Options:
|
||||||
--image IMAGE Image to test. Defaults to docker/cowrie/docker-compose.yml.
|
--image IMAGE Image to test. Defaults to docker/cowrie/docker-compose.yml.
|
||||||
--ssh-port PORT Host TCP port for SSH. Default: dynamic free port.
|
--ssh-port PORT Host TCP port for SSH. Default: dynamic free port.
|
||||||
--telnet-port PORT Host TCP port for Telnet. 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.
|
--timeout SEC Timeout for startup, protocol, and log checks. Default: 30.
|
||||||
--bind-ip IP Host IP to bind. Default: 127.0.0.1.
|
--bind-ip IP Host IP to bind. Default: 127.0.0.1.
|
||||||
--keep-artifacts Keep temporary compose file and logs for debugging.
|
--keep-artifacts Keep temporary compose file and logs for debugging.
|
||||||
|
|
@ -66,6 +68,15 @@ parse_args() {
|
||||||
TELNET_PORT="${1#*=}"
|
TELNET_PORT="${1#*=}"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--persona)
|
||||||
|
[[ $# -ge 2 ]] || test_die "--persona requires an argument"
|
||||||
|
PERSONA="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--persona=*)
|
||||||
|
PERSONA="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--timeout)
|
--timeout)
|
||||||
[[ $# -ge 2 ]] || test_die "--timeout requires an argument"
|
[[ $# -ge 2 ]] || test_die "--timeout requires an argument"
|
||||||
TEST_TIMEOUT="$2"
|
TEST_TIMEOUT="$2"
|
||||||
|
|
@ -107,6 +118,9 @@ validate_args() {
|
||||||
if [[ -n "${TELNET_PORT}" ]]; then
|
if [[ -n "${TELNET_PORT}" ]]; then
|
||||||
test_validate_port "${TELNET_PORT}"
|
test_validate_port "${TELNET_PORT}"
|
||||||
fi
|
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() {
|
prepare_cowrie_harness() {
|
||||||
|
|
@ -136,6 +150,8 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "${TEST_BIND_IP}:${SSH_PORT}:22"
|
- "${TEST_BIND_IP}:${SSH_PORT}:22"
|
||||||
- "${TEST_BIND_IP}:${TELNET_PORT}:23"
|
- "${TEST_BIND_IP}:${TELNET_PORT}:23"
|
||||||
|
environment:
|
||||||
|
COWRIE_PERSONA: "${PERSONA}"
|
||||||
volumes:
|
volumes:
|
||||||
- "${DL_DIR}:/home/cowrie/cowrie/dl"
|
- "${DL_DIR}:/home/cowrie/cowrie/dl"
|
||||||
- "${KEYS_DIR}:/home/cowrie/cowrie/etc"
|
- "${KEYS_DIR}:/home/cowrie/cowrie/etc"
|
||||||
|
|
@ -415,12 +431,16 @@ PY
|
||||||
|
|
||||||
assert_custom_filesystem() {
|
assert_custom_filesystem() {
|
||||||
docker exec -i "${TEST_CONTAINER_NAME}" python3 - <<'PY'
|
docker exec -i "${TEST_CONTAINER_NAME}" python3 - <<'PY'
|
||||||
|
import configparser
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
root = Path("/home/cowrie/cowrie")
|
root = Path("/home/cowrie/cowrie")
|
||||||
pickle_path = root / "src" / "cowrie" / "data" / "fs.pickle"
|
personas_root = root / "personas"
|
||||||
honeyfs = root / "honeyfs"
|
metadata_path = personas_root / "personas.json"
|
||||||
|
selected_path = Path("/tmp/cowrie/persona")
|
||||||
|
runtime_config_path = Path("/tmp/cowrie/runtime/cowrie.cfg")
|
||||||
offenders = []
|
offenders = []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -432,50 +452,128 @@ def read_bytes(path):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if not pickle_path.is_file():
|
def fail(message):
|
||||||
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)
|
|
||||||
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))
|
|
||||||
|
|
||||||
if offenders:
|
|
||||||
print("Cowrie filesystem still contains 'phil': " + ", ".join(offenders), file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
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)
|
print(message, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("Cowrie filesystem profile validated: ubuntu@srv01 without phil")
|
|
||||||
|
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:
|
||||||
|
fail("Cowrie persona filesystem contains forbidden markers: " + ", ".join(offenders))
|
||||||
|
|
||||||
|
print(f"Cowrie persona pool validated: {selected} selected, {len(personas)} personas, no phil")
|
||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 --upgrade --no-cache-dir pip && \
|
||||||
pip3 install --break-system-packages --no-cache-dir -r requirements.txt && \
|
pip3 install --break-system-packages --no-cache-dir -r requirements.txt && \
|
||||||
pip3 install --break-system-packages --no-cache-dir -e . && \
|
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 && \
|
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
|
# Setup configs
|
||||||
setcap cap_net_bind_service=+ep $(readlink -f $(type -P python3)) && \
|
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 && \
|
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
|
# 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 && \
|
su - cowrie -c "export PYTHONPATH=/home/cowrie/cowrie:/home/cowrie/cowrie/src && \
|
||||||
cd /home/cowrie/cowrie && \
|
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 && \
|
sleep 10 && \
|
||||||
rm -rf /home/cowrie/cowrie/etc && \
|
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
|
ENV PYTHONPATH=/home/cowrie/cowrie:/home/cowrie/cowrie/src
|
||||||
WORKDIR /home/cowrie/cowrie
|
WORKDIR /home/cowrie/cowrie
|
||||||
USER 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"]
|
||||||
|
|
|
||||||
1216
docker/cowrie/dist/generate_cowrie_fs.py
vendored
1216
docker/cowrie/dist/generate_cowrie_fs.py
vendored
File diff suppressed because it is too large
Load diff
114
docker/cowrie/dist/patch_cowrie_persona_support.py
vendored
Executable file
114
docker/cowrie/dist/patch_cowrie_persona_support.py
vendored
Executable file
|
|
@ -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()
|
||||||
79
docker/cowrie/dist/start-cowrie-persona.py
vendored
Executable file
79
docker/cowrie/dist/start-cowrie-persona.py
vendored
Executable file
|
|
@ -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())
|
||||||
Loading…
Reference in a new issue