mirror of
https://github.com/telekom-security/tpotce.git
synced 2026-05-29 17:24:15 +00:00
bump cowrie to latest release v3.0.0 and alpine image, adjust config and add tests
This commit is contained in:
parent
c07121b930
commit
05e3148cf4
5 changed files with 990 additions and 8 deletions
|
|
@ -14,6 +14,7 @@ not touch production `data/` or `data_backup/` paths.
|
||||||
./docker/_tests/run.sh ciscoasa
|
./docker/_tests/run.sh ciscoasa
|
||||||
./docker/_tests/run.sh citrixhoneypot
|
./docker/_tests/run.sh citrixhoneypot
|
||||||
./docker/_tests/run.sh conpot
|
./docker/_tests/run.sh conpot
|
||||||
|
./docker/_tests/run.sh cowrie
|
||||||
```
|
```
|
||||||
|
|
||||||
Common options:
|
Common options:
|
||||||
|
|
@ -31,6 +32,8 @@ Individual tests can also be run directly:
|
||||||
./docker/_tests/tests/ciscoasa.sh --https-port 18443 --ike-port 15000
|
./docker/_tests/tests/ciscoasa.sh --https-port 18443 --ike-port 15000
|
||||||
./docker/_tests/tests/citrixhoneypot.sh --https-port 1443
|
./docker/_tests/tests/citrixhoneypot.sh --https-port 1443
|
||||||
./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 --ssh-port 2222 --telnet-port 2323
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
|
||||||
558
docker/_tests/tests/cowrie.sh
Executable file
558
docker/_tests/tests/cowrie.sh
Executable file
|
|
@ -0,0 +1,558 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
|
||||||
|
TEST_NAME="cowrie"
|
||||||
|
DEFAULT_IMAGE="ghcr.io/telekom-security/cowrie:24.04.1"
|
||||||
|
IMAGE=""
|
||||||
|
SSH_PORT=""
|
||||||
|
TELNET_PORT=""
|
||||||
|
LOG_DIR=""
|
||||||
|
TTY_LOG_DIR=""
|
||||||
|
DL_DIR=""
|
||||||
|
KEYS_DIR=""
|
||||||
|
JSON_LOG_FILE=""
|
||||||
|
MAPPED_SSH_PORT=""
|
||||||
|
MAPPED_TELNET_PORT=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 [options]
|
||||||
|
|
||||||
|
Run an isolated post-build smoke test for the Cowrie image.
|
||||||
|
|
||||||
|
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.
|
||||||
|
--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.
|
||||||
|
-h, --help Show this help message.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--image)
|
||||||
|
[[ $# -ge 2 ]] || test_die "--image requires an argument"
|
||||||
|
IMAGE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--image=*)
|
||||||
|
IMAGE="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--ssh-port)
|
||||||
|
[[ $# -ge 2 ]] || test_die "--ssh-port requires an argument"
|
||||||
|
SSH_PORT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ssh-port=*)
|
||||||
|
SSH_PORT="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--telnet-port)
|
||||||
|
[[ $# -ge 2 ]] || test_die "--telnet-port requires an argument"
|
||||||
|
TELNET_PORT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--telnet-port=*)
|
||||||
|
TELNET_PORT="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--timeout)
|
||||||
|
[[ $# -ge 2 ]] || test_die "--timeout requires an argument"
|
||||||
|
TEST_TIMEOUT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--timeout=*)
|
||||||
|
TEST_TIMEOUT="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--bind-ip)
|
||||||
|
[[ $# -ge 2 ]] || test_die "--bind-ip requires an argument"
|
||||||
|
TEST_BIND_IP="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bind-ip=*)
|
||||||
|
TEST_BIND_IP="${1#*=}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--keep-artifacts)
|
||||||
|
TEST_KEEP_ARTIFACTS="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
test_die "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_args() {
|
||||||
|
test_validate_timeout
|
||||||
|
if [[ -n "${SSH_PORT}" ]]; then
|
||||||
|
test_validate_port "${SSH_PORT}"
|
||||||
|
fi
|
||||||
|
if [[ -n "${TELNET_PORT}" ]]; then
|
||||||
|
test_validate_port "${TELNET_PORT}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_cowrie_harness() {
|
||||||
|
test_prepare_harness "${TEST_NAME}"
|
||||||
|
|
||||||
|
LOG_DIR="${TEST_TMP_ROOT}/log"
|
||||||
|
TTY_LOG_DIR="${LOG_DIR}/tty"
|
||||||
|
DL_DIR="${TEST_TMP_ROOT}/downloads"
|
||||||
|
KEYS_DIR="${TEST_TMP_ROOT}/keys"
|
||||||
|
JSON_LOG_FILE="${LOG_DIR}/cowrie.json"
|
||||||
|
TEST_ARTIFACT_LOG_DIR="${LOG_DIR}"
|
||||||
|
|
||||||
|
mkdir -p "${LOG_DIR}" "${TTY_LOG_DIR}" "${DL_DIR}" "${KEYS_DIR}"
|
||||||
|
chmod 0777 "${LOG_DIR}" "${TTY_LOG_DIR}" "${DL_DIR}" "${KEYS_DIR}"
|
||||||
|
|
||||||
|
cat > "${TEST_HARNESS_COMPOSE}" <<EOF
|
||||||
|
services:
|
||||||
|
cowrie:
|
||||||
|
image: "${IMAGE}"
|
||||||
|
container_name: "${TEST_CONTAINER_NAME}"
|
||||||
|
restart: "no"
|
||||||
|
read_only: true
|
||||||
|
user: "2000:2000"
|
||||||
|
tmpfs:
|
||||||
|
- /tmp/cowrie:uid=2000,gid=2000
|
||||||
|
- /tmp/cowrie/data:uid=2000,gid=2000
|
||||||
|
ports:
|
||||||
|
- "${TEST_BIND_IP}:${SSH_PORT}:22"
|
||||||
|
- "${TEST_BIND_IP}:${TELNET_PORT}:23"
|
||||||
|
volumes:
|
||||||
|
- "${DL_DIR}:/home/cowrie/cowrie/dl"
|
||||||
|
- "${KEYS_DIR}:/home/cowrie/cowrie/etc"
|
||||||
|
- "${LOG_DIR}:/home/cowrie/cowrie/log"
|
||||||
|
- "${TTY_LOG_DIR}:/home/cowrie/cowrie/log/tty"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: "${TEST_PROJECT_NAME}_net"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_json_log() {
|
||||||
|
local deadline=$((SECONDS + TEST_TIMEOUT))
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if [[ -f "${JSON_LOG_FILE}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_ssh_banner_probe() {
|
||||||
|
local token="$1"
|
||||||
|
|
||||||
|
python3 - "${TEST_BIND_IP}" "${MAPPED_SSH_PORT}" "${token}" "${TEST_TIMEOUT}" <<'PY'
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
host = sys.argv[1]
|
||||||
|
port = int(sys.argv[2])
|
||||||
|
token = sys.argv[3]
|
||||||
|
timeout = int(sys.argv[4])
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
client_banner = f"SSH-2.0-tpot-cowrie-smoke-{token}\r\n".encode("ascii")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||||
|
sock.settimeout(1)
|
||||||
|
chunks = []
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(256)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
if not chunk:
|
||||||
|
raise RuntimeError("Connection closed before SSH banner")
|
||||||
|
chunks.append(chunk)
|
||||||
|
if b"\n" in b"".join(chunks):
|
||||||
|
break
|
||||||
|
|
||||||
|
banner = b"".join(chunks).splitlines()[0]
|
||||||
|
if not banner.startswith(b"SSH-"):
|
||||||
|
raise RuntimeError(f"Expected SSH banner, got {banner!r}")
|
||||||
|
|
||||||
|
sock.sendall(client_banner)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
print(f"SSH banner: {banner.decode('ascii', errors='replace')}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"SSH probe failed: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_ssh_banner_probe_with_retries() {
|
||||||
|
local token="$1"
|
||||||
|
local deadline=$((SECONDS + TEST_TIMEOUT))
|
||||||
|
local output=""
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if output="$(run_ssh_banner_probe "${token}" 2>&1)"; then
|
||||||
|
printf '%s\n' "${output}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "${output}" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_telnet_login_probe() {
|
||||||
|
local token="$1"
|
||||||
|
|
||||||
|
python3 - "${TEST_BIND_IP}" "${MAPPED_TELNET_PORT}" "${token}" "${TEST_TIMEOUT}" <<'PY'
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
IAC = 255
|
||||||
|
DONT = 254
|
||||||
|
DO = 253
|
||||||
|
WONT = 252
|
||||||
|
WILL = 251
|
||||||
|
SB = 250
|
||||||
|
SE = 240
|
||||||
|
|
||||||
|
host = sys.argv[1]
|
||||||
|
port = int(sys.argv[2])
|
||||||
|
token = sys.argv[3]
|
||||||
|
timeout = int(sys.argv[4])
|
||||||
|
username = f"cowrie-user-{token}"
|
||||||
|
password = f"cowrie-pass-{token}"
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
|
||||||
|
|
||||||
|
def readable(data):
|
||||||
|
return data.decode("utf-8", errors="ignore").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def read_until(sock, needles):
|
||||||
|
data = bytearray()
|
||||||
|
in_subnegotiation = False
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(256)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
while index < len(chunk):
|
||||||
|
byte = chunk[index]
|
||||||
|
if byte == IAC and index + 1 < len(chunk):
|
||||||
|
command = chunk[index + 1]
|
||||||
|
if command in (DO, DONT, WILL, WONT) and index + 2 < len(chunk):
|
||||||
|
option = chunk[index + 2]
|
||||||
|
if command in (DO, DONT):
|
||||||
|
sock.sendall(bytes([IAC, WONT, option]))
|
||||||
|
else:
|
||||||
|
sock.sendall(bytes([IAC, DONT, option]))
|
||||||
|
index += 3
|
||||||
|
continue
|
||||||
|
if command == SB:
|
||||||
|
in_subnegotiation = True
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
if command == SE:
|
||||||
|
in_subnegotiation = False
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_subnegotiation:
|
||||||
|
data.append(byte)
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
text = readable(data)
|
||||||
|
if any(needle in text for needle in needles):
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Timed out waiting for one of {needles}; received {bytes(data)!r}")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||||
|
sock.settimeout(1)
|
||||||
|
login_prompt = read_until(sock, ("login:", "username:"))
|
||||||
|
sock.sendall((username + "\r\n").encode("ascii"))
|
||||||
|
password_prompt = read_until(sock, ("password:",))
|
||||||
|
sock.sendall((password + "\r\n").encode("ascii"))
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Telnet prompts: "
|
||||||
|
f"{login_prompt[-80:].decode('utf-8', errors='replace')!r}; "
|
||||||
|
f"{password_prompt[-80:].decode('utf-8', errors='replace')!r}"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Telnet probe failed: {exc}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_telnet_login_probe_with_retries() {
|
||||||
|
local token="$1"
|
||||||
|
local deadline=$((SECONDS + TEST_TIMEOUT))
|
||||||
|
local output=""
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if output="$(run_telnet_login_probe "${token}" 2>&1)"; then
|
||||||
|
printf '%s\n' "${output}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s\n' "${output}" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_json_event_containing() {
|
||||||
|
local token="$1"
|
||||||
|
local mode="$2"
|
||||||
|
|
||||||
|
python3 - "${JSON_LOG_FILE}" "${token}" "${mode}" "${TEST_TIMEOUT}" <<'PY'
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
token = sys.argv[2]
|
||||||
|
mode = sys.argv[3]
|
||||||
|
timeout = int(sys.argv[4])
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
|
||||||
|
def contains_token(value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
return token in value
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return any(contains_token(item) for item in value.values())
|
||||||
|
if isinstance(value, list):
|
||||||
|
return any(contains_token(item) for item in value)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def matches_mode(event):
|
||||||
|
eventid = str(event.get("eventid", ""))
|
||||||
|
if mode == "ssh":
|
||||||
|
return contains_token(event) and (
|
||||||
|
eventid.startswith("cowrie.client.")
|
||||||
|
or eventid.startswith("cowrie.session.")
|
||||||
|
or eventid.startswith("cowrie.login.")
|
||||||
|
)
|
||||||
|
if mode == "telnet":
|
||||||
|
return contains_token(event) and (
|
||||||
|
eventid.startswith("cowrie.login.")
|
||||||
|
or eventid.startswith("cowrie.session.")
|
||||||
|
or eventid.startswith("cowrie.client.")
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"Unknown mode: {mode}")
|
||||||
|
|
||||||
|
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if not path.exists():
|
||||||
|
last_error = f"{path} does not exist yet"
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||||
|
except OSError as exc:
|
||||||
|
last_error = f"Could not read {path}: {exc}"
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for line_number, line in enumerate(lines, 1):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
event = json.loads(line)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
last_error = f"Invalid JSON in {path}:{line_number}: {exc}"
|
||||||
|
continue
|
||||||
|
|
||||||
|
if matches_mode(event):
|
||||||
|
print(f"JSON event found in {path}:{line_number}: {event.get('eventid', '<missing eventid>')}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
last_error = f"No {mode} JSON event found in {path} for token {token}"
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if last_error:
|
||||||
|
print(last_error, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_custom_filesystem() {
|
||||||
|
docker exec -i "${TEST_CONTAINER_NAME}" python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
root = Path("/home/cowrie/cowrie")
|
||||||
|
pickle_path = root / "src" / "cowrie" / "data" / "fs.pickle"
|
||||||
|
honeyfs = root / "honeyfs"
|
||||||
|
offenders = []
|
||||||
|
|
||||||
|
|
||||||
|
def read_bytes(path):
|
||||||
|
try:
|
||||||
|
return path.read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Could not read {path}: {exc}", file=sys.stderr)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Cowrie filesystem profile validated: ubuntu@srv01 without phil")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_no_runtime_errors() {
|
||||||
|
if grep -R -E "Traceback|NameError|Unhandled Error|Exception" "${LOG_DIR}" >/dev/null 2>&1; then
|
||||||
|
test_die "Cowrie runtime error found in log files"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test_compose logs --no-color 2>/dev/null | grep -E "Traceback|NameError|Unhandled Error|Exception" >/dev/null 2>&1; then
|
||||||
|
test_die "Cowrie runtime error found in Docker logs"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
validate_args
|
||||||
|
test_check_dependencies
|
||||||
|
|
||||||
|
if [[ -z "${IMAGE}" ]]; then
|
||||||
|
IMAGE="$(test_read_compose_image "${TEST_NAME}" "${DEFAULT_IMAGE}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
test_info "Using image: ${IMAGE}"
|
||||||
|
test_require_image "${IMAGE}" "docker compose -f docker/${TEST_NAME}/docker-compose.yml build ${TEST_NAME}"
|
||||||
|
|
||||||
|
if [[ -n "${SSH_PORT}" ]]; then
|
||||||
|
test_ensure_port_free "${TEST_BIND_IP}" "${SSH_PORT}" || test_die "${TEST_BIND_IP}:${SSH_PORT} is already in use. Try --ssh-port <free-port>."
|
||||||
|
fi
|
||||||
|
if [[ -n "${TELNET_PORT}" ]]; then
|
||||||
|
test_ensure_port_free "${TEST_BIND_IP}" "${TELNET_PORT}" || test_die "${TEST_BIND_IP}:${TELNET_PORT} is already in use. Try --telnet-port <free-port>."
|
||||||
|
fi
|
||||||
|
|
||||||
|
prepare_cowrie_harness
|
||||||
|
test_enable_cleanup
|
||||||
|
|
||||||
|
test_info "Starting isolated Cowrie container"
|
||||||
|
test_compose up -d --no-build >/dev/null
|
||||||
|
|
||||||
|
test_wait_for_container || test_die "Cowrie container did not stay running"
|
||||||
|
test_ok "Container is running"
|
||||||
|
|
||||||
|
MAPPED_SSH_PORT="$(test_get_mapped_port "${TEST_NAME}" "22")" || test_die "Could not resolve mapped host port for 22/tcp"
|
||||||
|
test_ok "Port ${TEST_BIND_IP}:${MAPPED_SSH_PORT} maps to container port 22/tcp"
|
||||||
|
|
||||||
|
MAPPED_TELNET_PORT="$(test_get_mapped_port "${TEST_NAME}" "23")" || test_die "Could not resolve mapped host port for 23/tcp"
|
||||||
|
test_ok "Port ${TEST_BIND_IP}:${MAPPED_TELNET_PORT} maps to container port 23/tcp"
|
||||||
|
|
||||||
|
test_info "Validating custom Cowrie filesystem profile"
|
||||||
|
assert_custom_filesystem || test_die "Custom Cowrie filesystem validation failed"
|
||||||
|
test_ok "Custom Cowrie filesystem profile is present"
|
||||||
|
|
||||||
|
test_info "Waiting for cowrie.json"
|
||||||
|
wait_for_json_log || test_die "cowrie.json was not created"
|
||||||
|
test_ok "cowrie.json exists"
|
||||||
|
|
||||||
|
local ssh_token="ssh-test-$(date +%s)-$$"
|
||||||
|
test_info "Running SSH banner probe with token: ${ssh_token}"
|
||||||
|
run_ssh_banner_probe_with_retries "${ssh_token}" || test_die "SSH probe failed on ${TEST_BIND_IP}:${MAPPED_SSH_PORT}"
|
||||||
|
test_wait_for_container || test_die "Cowrie container stopped after SSH probe"
|
||||||
|
|
||||||
|
test_info "Waiting for SSH probe event in cowrie.json"
|
||||||
|
wait_for_json_event_containing "${ssh_token}" "ssh" || test_die "SSH probe token was not found in cowrie.json"
|
||||||
|
test_ok "SSH probe was written to cowrie.json"
|
||||||
|
|
||||||
|
local telnet_token="telnet-test-$(date +%s)-$$"
|
||||||
|
test_info "Running Telnet login probe with token: ${telnet_token}"
|
||||||
|
run_telnet_login_probe_with_retries "${telnet_token}" || test_die "Telnet probe failed on ${TEST_BIND_IP}:${MAPPED_TELNET_PORT}"
|
||||||
|
test_wait_for_container || test_die "Cowrie container stopped after Telnet probe"
|
||||||
|
|
||||||
|
test_info "Waiting for Telnet probe event in cowrie.json"
|
||||||
|
wait_for_json_event_containing "${telnet_token}" "telnet" || test_die "Telnet probe token was not found in cowrie.json"
|
||||||
|
test_ok "Telnet probe was written to cowrie.json"
|
||||||
|
|
||||||
|
assert_no_runtime_errors
|
||||||
|
test_ok "No Cowrie runtime errors found in logs"
|
||||||
|
|
||||||
|
test_ok "Cowrie post-build smoke test completed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM alpine:3.20
|
FROM alpine:3.23
|
||||||
#
|
#
|
||||||
# Include dist
|
# Include dist
|
||||||
COPY dist/ /root/dist/
|
COPY dist/ /root/dist/
|
||||||
|
|
@ -41,13 +41,14 @@ RUN apk --no-cache -U upgrade && \
|
||||||
# Install cowrie
|
# Install cowrie
|
||||||
mkdir -p /home/cowrie && \
|
mkdir -p /home/cowrie && \
|
||||||
cd /home/cowrie && \
|
cd /home/cowrie && \
|
||||||
git clone https://github.com/cowrie/cowrie && \
|
git clone https://github.com/cowrie/cowrie -b v3.0.0 && \
|
||||||
cd cowrie && \
|
cd cowrie && \
|
||||||
git checkout 7b18207485dbfc218082e82c615d948924429973 && \
|
|
||||||
mkdir -p log && \
|
mkdir -p log && \
|
||||||
# cp /root/dist/requirements.txt . && \
|
# cp /root/dist/requirements.txt . && \
|
||||||
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 . && \
|
||||||
|
python3 /root/dist/generate_cowrie_fs.py --cowrie-root /home/cowrie/cowrie && \
|
||||||
#
|
#
|
||||||
# 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)) && \
|
||||||
|
|
@ -78,7 +79,7 @@ RUN apk --no-cache -U upgrade && \
|
||||||
/home/cowrie/cowrie/.git
|
/home/cowrie/cowrie/.git
|
||||||
#
|
#
|
||||||
# Start cowrie
|
# Start cowrie
|
||||||
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/bin/twistd", "--nodaemon", "-y", "cowrie.tac", "--pidfile", "/tmp/cowrie/cowrie.pid", "cowrie"]
|
||||||
|
|
|
||||||
8
docker/cowrie/dist/cowrie.cfg
vendored
8
docker/cowrie/dist/cowrie.cfg
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
[honeypot]
|
[honeypot]
|
||||||
hostname = ubuntu
|
hostname = srv01
|
||||||
log_path = log
|
log_path = log
|
||||||
logtype = plain
|
logtype = plain
|
||||||
download_path = dl
|
download_path = dl
|
||||||
|
|
@ -21,8 +21,7 @@ data_path = src/cowrie/data
|
||||||
[shell]
|
[shell]
|
||||||
filesystem = src/cowrie/data/fs.pickle
|
filesystem = src/cowrie/data/fs.pickle
|
||||||
processes = src/cowrie/data/cmdoutput.json
|
processes = src/cowrie/data/cmdoutput.json
|
||||||
#arch = linux-x64-lsb
|
arch = linux-x64-lsb
|
||||||
arch = bsd-aarch64-lsb, bsd-aarch64-msb, bsd-bfin-msb, bsd-mips-lsb, bsd-mips-msb, bsd-mips64-lsb, bsd-mips64-msb, bsd-powepc-msb, bsd-powepc64-lsb, bsd-riscv64-lsb, bsd-sparc-msb, bsd-sparc64-msb, bsd-x32-lsb, bsd-x64-lsb, linux-aarch64-lsb, linux-aarch64-msb, linux-alpha-lsb, linux-am33-lsb, linux-arc-lsb, linux-arc-msb, linux-arm-lsb, linux-arm-msb, linux-avr32-lsb, linux-bfin-lsb, linux-c6x-lsb, linux-c6x-msb, linux-cris-lsb, linux-frv-msb, linux-h8300-msb, linux-hppa-msb, linux-hppa64-msb, linux-ia64-lsb, linux-m32r-msb, linux-m68k-msb, linux-microblaze-msb, linux-mips-lsb, linux-mips-msb, linux-mips64-lsb, linux-mips64-msb, linux-mn10300-lsb, linux-nios-lsb, linux-nios-msb, linux-powerpc-lsb, linux-powerpc-msb, linux-powerpc64-lsb, linux-powerpc64-msb, linux-riscv64-lsb, linux-s390x-msb, linux-sh-lsb, linux-sh-msb, linux-sparc-msb, linux-sparc64-msb, linux-tilegx-lsb, linux-tilegx-msb, linux-tilegx64-lsb, linux-tilegx64-msb, linux-x64-lsb, linux-x86-lsb, linux-xtensa-msb, osx-x32-lsb, osx-x64-lsb
|
|
||||||
kernel_version = 5.15.0-23-generic-amd64
|
kernel_version = 5.15.0-23-generic-amd64
|
||||||
kernel_build_string = #25~22.04-Ubuntu SMP
|
kernel_build_string = #25~22.04-Ubuntu SMP
|
||||||
hardware_platform = x86_64
|
hardware_platform = x86_64
|
||||||
|
|
@ -46,7 +45,7 @@ macs = hmac-sha2-512,hmac-sha2-384,hmac-sha2-56,hmac-sha1,hmac-md5
|
||||||
compression = zlib@openssh.com,zlib,none
|
compression = zlib@openssh.com,zlib,none
|
||||||
listen_endpoints = tcp:22:interface=0.0.0.0
|
listen_endpoints = tcp:22:interface=0.0.0.0
|
||||||
sftp_enabled = true
|
sftp_enabled = true
|
||||||
forwarding = true
|
forwarding = false
|
||||||
forward_redirect = false
|
forward_redirect = false
|
||||||
forward_tunnel = false
|
forward_tunnel = false
|
||||||
auth_none_enabled = false
|
auth_none_enabled = false
|
||||||
|
|
@ -57,6 +56,7 @@ auth_publickey_allow_any = true
|
||||||
enabled = true
|
enabled = true
|
||||||
listen_endpoints = tcp:23:interface=0.0.0.0
|
listen_endpoints = tcp:23:interface=0.0.0.0
|
||||||
reported_port = 23
|
reported_port = 23
|
||||||
|
cve_2026_24061_vulnerable = true
|
||||||
|
|
||||||
[output_jsonlog]
|
[output_jsonlog]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
|
||||||
420
docker/cowrie/dist/generate_cowrie_fs.py
vendored
Normal file
420
docker/cowrie/dist/generate_cowrie_fs.py
vendored
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
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
|
||||||
|
|
||||||
|
A_NAME = 0
|
||||||
|
A_TYPE = 1
|
||||||
|
A_UID = 2
|
||||||
|
A_GID = 3
|
||||||
|
A_SIZE = 4
|
||||||
|
A_MODE = 5
|
||||||
|
A_CTIME = 6
|
||||||
|
A_CONTENTS = 7
|
||||||
|
A_TARGET = 8
|
||||||
|
A_REALFILE = 9
|
||||||
|
|
||||||
|
T_LINK = 0
|
||||||
|
T_DIR = 1
|
||||||
|
T_FILE = 2
|
||||||
|
|
||||||
|
TEXT_FILES = {
|
||||||
|
"etc/passwd": """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
|
||||||
|
sync:x:4:65534:sync:/bin:/bin/sync
|
||||||
|
games:x:5:60:games:/usr/games:/usr/sbin/nologin
|
||||||
|
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
|
||||||
|
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
|
||||||
|
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
|
||||||
|
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
|
||||||
|
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
|
||||||
|
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
|
||||||
|
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:
|
||||||
|
daemon:x:1:
|
||||||
|
bin:x:2:
|
||||||
|
sys:x:3:
|
||||||
|
adm:x:4:syslog,ubuntu
|
||||||
|
tty:x:5:
|
||||||
|
disk:x:6:
|
||||||
|
lp:x:7:
|
||||||
|
mail:x:8:
|
||||||
|
news:x:9:
|
||||||
|
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
|
||||||
|
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
|
||||||
|
staff:x:50:
|
||||||
|
games:x:60:
|
||||||
|
users:x:100:
|
||||||
|
nogroup:x:65534:
|
||||||
|
systemd-network:x:998:
|
||||||
|
systemd-resolve:x:997:
|
||||||
|
messagebus:x:102:
|
||||||
|
ssh:x:103:
|
||||||
|
ubuntu:x:1000:
|
||||||
|
""",
|
||||||
|
"etc/shadow": """root:*:19276:0:99999:7:::
|
||||||
|
daemon:*:19276:0:99999:7:::
|
||||||
|
bin:*:19276:0:99999:7:::
|
||||||
|
sys:*:19276:0:99999:7:::
|
||||||
|
sync:*:19276:0:99999:7:::
|
||||||
|
games:*:19276:0:99999:7:::
|
||||||
|
man:*:19276:0:99999:7:::
|
||||||
|
lp:*:19276:0:99999:7:::
|
||||||
|
mail:*:19276:0:99999:7:::
|
||||||
|
news:*:19276:0:99999:7:::
|
||||||
|
uucp:*:19276:0:99999:7:::
|
||||||
|
proxy:*:19276:0:99999:7:::
|
||||||
|
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}
|
||||||
|
|
||||||
|
# The following lines are desirable for IPv6 capable hosts
|
||||||
|
::1 ip6-localhost ip6-loopback
|
||||||
|
fe00::0 ip6-localnet
|
||||||
|
ff00::0 ip6-mcastprefix
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
case $- in
|
||||||
|
*i*) ;;
|
||||||
|
*) return;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
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.
|
||||||
|
if [ "$BASH" ]; then
|
||||||
|
if [ -f ~/.bashrc ]; then
|
||||||
|
. ~/.bashrc
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mesg n 2> /dev/null || true
|
||||||
|
""",
|
||||||
|
".sudo_as_admin_successful": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Derive a custom Cowrie fs.pickle and honeyfs overlay from Cowrie defaults."
|
||||||
|
)
|
||||||
|
parser.add_argument("--cowrie-root", required=True, type=Path)
|
||||||
|
parser.add_argument("--work-dir", default=Path("/tmp/cowrie-custom-fs"), type=Path)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def node_get(node, index, default=None):
|
||||||
|
return node[index] if len(node) > index else default
|
||||||
|
|
||||||
|
|
||||||
|
def node_set(node, index, value):
|
||||||
|
while len(node) <= index:
|
||||||
|
node.append(None)
|
||||||
|
node[index] = value
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
current = root
|
||||||
|
for part in Path(relative_path).parts:
|
||||||
|
if part in ("", "/"):
|
||||||
|
continue
|
||||||
|
current = next(
|
||||||
|
(child for child in node_children(current) if node_get(child, A_NAME) == part),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if current is None:
|
||||||
|
return None
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dir(root, relative_path, uid=0, gid=0, mode=0o755):
|
||||||
|
current = root
|
||||||
|
now = int(time.time())
|
||||||
|
for part in Path(relative_path).parts:
|
||||||
|
if part in ("", "/"):
|
||||||
|
continue
|
||||||
|
match = next(
|
||||||
|
(child for child in node_children(current) if node_get(child, A_NAME) == part),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if match is None:
|
||||||
|
match = [part, T_DIR, uid, gid, 4096, stat.S_IFDIR | mode, now, [], None, None]
|
||||||
|
node_children(current).append(match)
|
||||||
|
current = match
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_file(root, relative_path, content, uid=0, gid=0, mode=0o644):
|
||||||
|
parent = ensure_dir(root, str(Path(relative_path).parent), uid=0, gid=0)
|
||||||
|
name = Path(relative_path).name
|
||||||
|
node = next(
|
||||||
|
(child for child in node_children(parent) if node_get(child, A_NAME) == name),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
now = int(time.time())
|
||||||
|
if node is None:
|
||||||
|
node = [name, T_FILE, uid, gid, len(content), stat.S_IFREG | mode, now, [], None, None]
|
||||||
|
node_children(parent).append(node)
|
||||||
|
|
||||||
|
node_set(node, A_NAME, name)
|
||||||
|
node_set(node, A_TYPE, T_FILE)
|
||||||
|
node_set(node, A_UID, uid)
|
||||||
|
node_set(node, A_GID, gid)
|
||||||
|
node_set(node, A_SIZE, len(content))
|
||||||
|
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_TARGET, None)
|
||||||
|
node_set(node, A_REALFILE, None)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def load_pickle(path):
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
try:
|
||||||
|
return pickle.load(handle)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
handle.seek(0)
|
||||||
|
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)
|
||||||
|
|
||||||
|
for relative_path, text in TEXT_FILES.items():
|
||||||
|
mode = 0o644
|
||||||
|
if relative_path == "etc/shadow":
|
||||||
|
mode = 0o640
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_no_phil(pickle_path, honeyfs):
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
copy_and_patch_honeyfs(source_honeyfs, generated_honeyfs)
|
||||||
|
|
||||||
|
validate_no_phil(generated_pickle_path, generated_honeyfs)
|
||||||
|
validate_expected_markers(generated_pickle_path, generated_honeyfs)
|
||||||
|
|
||||||
|
shutil.move(generated_pickle_path, pickle_path)
|
||||||
|
if source_honeyfs.exists():
|
||||||
|
shutil.rmtree(source_honeyfs)
|
||||||
|
shutil.copytree(generated_honeyfs, source_honeyfs, copy_function=shutil.copy2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue