Skip to content

Commit f8e48b1

Browse files
committed
feat: Enhance camera reconnaissance toolkit with new features and scripts
- Updated mode-config.sh to include new configuration options for ONVIF probing, SSDP, HTTP metadata, and follow-up service scans based on selected modes. - Added onvif_device_info.py to parse ONVIF GetDeviceInformation SOAP responses and extract device details. - Introduced port-profiles.sh to define various port profiles for Nmap and Masscan, along with suggested RTSP brute-force thread counts. - Created profile_resolver.py to resolve camera/profile hints based on discovery artifacts, including catalog loading and matching logic. - Developed rtsp_stream_summary.py to summarize ffprobe output for RTSP streams. - Implemented ssdp_probe.py for performing multicast SSDP discovery sweeps and capturing responses. - Added ui-banner.sh for rendering a user interface banner with information about the toolkit and its usage.
1 parent ecb2ca3 commit f8e48b1

18 files changed

+1450
-278
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
build.log
2+
.venv/*
3+
24
dev/results/*
35
!dev/results/.gitkeep
6+
scripts/venv/*
47
scripts/paused.conf
58
debian/.debhelper/*
69
debian/debhelper-build-stamp

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.2.0
1+
2.2.1

debian/changelog

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
camsniff (2.2.1) unstable; urgency=medium
2+
* Move PORT_PROFILE_DATA and UI_HELPER to scripts/ for proper shellcheck sourcing.
3+
* implemented new scanning features and bug fixes.
4+
* Refactor script paths in analyze.sh and camsniff.sh for consistency
5+
* Moved Python scripts that resided in camsniff.sh to scripts/ for better organization.
6+
*
7+
* Add missing shellcheck disable directives to sourced UI scripts.
8+
* Fix UI banner
9+
10+
-- John Hauger Mitander <john@on1.no> Sat, 11 Oct 2025 01:00:00 +0000
11+
112
camsniff (2.2.0) unstable; urgency=medium
213

314
* Overhaul extras UX with animated spinners, clearer banners, and quiet mode.

scripts/analyze.sh

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
#!/usr/bin/env bash
22
#
3-
# CamSniff- Automated IP camera reconnaissance toolkit
4-
# By John Hauger Mitander <john@on1.no>
53
# Copyright 2025 John Hauger Mitander
4+
# Licensed under the MIT License
65
#
7-
# CamSniff is Licensed under the MIT License.
8-
# analyze.sh
96

107
set -euo pipefail
118

scripts/build-coap.sh

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
#!/usr/bin/env bash
22
#
3-
# CamSniff- Automated IP camera reconnaissance toolkit
4-
# By John Hauger Mitander <john@on1.no>
53
# Copyright 2025 John Hauger Mitander
4+
# Licensed under the MIT License
65
#
7-
# CamSniff is Licensed under the MIT License.
8-
# build-coap.sh
96

107
set -euo pipefail
118

scripts/camsniff.sh

Lines changed: 54 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
#!/usr/bin/env bash
22
#
3-
# CamSniff- Automated IP camera reconnaissance toolkit
4-
# By John Hauger Mitander <john@on1.no>
53
# Copyright 2025 John Hauger Mitander
4+
# Licensed under the MIT License
65
#
7-
# CamSniff is Licensed under the MIT License.
8-
# camsniff.sh
96

107
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
118
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -16,8 +13,8 @@ CREDENTIAL_PROBE="$SCRIPT_DIR/credential-probe.sh"
1613
NMAP_RTSP_SCRIPT="$ROOT_DIR/data/rtsp-url-brute.nse"
1714
NMAP_RTSP_THREADS=10
1815
RTSP_URL_DICT="$ROOT_DIR/data/rtsp-urls.txt"
19-
PORT_PROFILE_DATA="$ROOT_DIR/data/port-profiles.sh"
20-
UI_HELPER="$ROOT_DIR/data/ui-banner.sh"
16+
PORT_PROFILE_DATA="$SCRIPT_DIR/port-profiles.sh"
17+
UI_HELPER="$SCRIPT_DIR/ui-banner.sh"
2118

2219
MODE_DEFAULT="medium"
2320
MODE_REQUESTED=""
@@ -46,6 +43,7 @@ COAP_OUTPUT_FILE="$LOG_DIR/coap-discovery.txt"
4643
COAP_LOG_FILE="$LOG_DIR/coap-probe.log"
4744
COAP_PROBE_TIMEOUT="${COAP_PROBE_TIMEOUT:-5}"
4845
IVRE_LOG_FILE="$LOG_DIR/ivre-sync.log"
46+
CATALOG_JSON="$RUN_DIR/paths.json"
4947
GEOIP_DIR="$ROOT_DIR/share/geoip"
5048
GEOIP_CITY_DB="$GEOIP_DIR/dbip-city-lite.mmdb"
5149
GEOIP_ASN_DB="$GEOIP_DIR/dbip-asn-lite.mmdb"
@@ -80,6 +78,7 @@ tshark_output=""
8078
hosts_json_tmp=""
8179
discovery_enriched_tmp=""
8280
coap_output_tmp=""
81+
coap_build_log=""
8382

8483
print_usage() {
8584
cat <<'EOF'
@@ -175,6 +174,17 @@ parse_target_file() {
175174
return 0
176175
}
177176

177+
detect_capture_interface() {
178+
local iface=""
179+
if command -v ip >/dev/null 2>&1; then
180+
iface=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
181+
fi
182+
if [[ -z $iface && -r /proc/net/route ]]; then
183+
iface=$(awk '($2 == "00000000") {print $1; exit}' /proc/net/route)
184+
fi
185+
printf '%s' "$iface"
186+
}
187+
178188
record_protocol_hit() {
179189
local ip="$1"
180190
local proto="$2"
@@ -508,15 +518,15 @@ if [[ ! -f "$PORT_PROFILE_DATA" ]]; then
508518
exit 1
509519
fi
510520

511-
# shellcheck source=data/port-profiles.sh
521+
# shellcheck source=port-profiles.sh
512522
source "$PORT_PROFILE_DATA"
513523

514524
if [[ ! -f "$UI_HELPER" ]]; then
515525
echo "Missing UI helper: $UI_HELPER" >&2
516526
exit 1
517527
fi
518528

519-
# shellcheck source=data/ui-banner.sh
529+
# shellcheck source=ui-banner.sh
520530
source "$UI_HELPER"
521531

522532
cam_run_with_spinner() {
@@ -590,11 +600,19 @@ cam_run_packinst() {
590600
return $status
591601
}
592602

603+
# shellcheck disable=SC2329
604+
build_coap_with_log() {
605+
[[ -z ${coap_build_log:-} ]] && return 1
606+
"$SCRIPT_DIR/build-coap.sh" &>> "$coap_build_log"
607+
}
608+
609+
# shellcheck disable=SC2329
593610
do_coap_probe() {
594611
local tshark_output="$1"
595612
rm -f "$COAP_OUTPUT_FILE"
596613
: > "$COAP_LOG_FILE"
597-
local coap_output_tmp=$(mktemp /tmp/camsniff-coap.XXXXXX)
614+
local coap_output_tmp
615+
coap_output_tmp=$(mktemp /tmp/camsniff-coap.XXXXXX)
598616

599617
declare -a coap_targets=()
600618
declare -A coap_seen_targets=()
@@ -687,6 +705,17 @@ if [ "$EUID" -ne 0 ]; then
687705
exit 1
688706
fi
689707

708+
if [[ -z ${TSHARK_INTERFACE:-} ]]; then
709+
detected_iface=$(detect_capture_interface)
710+
if [[ -n $detected_iface ]]; then
711+
TSHARK_INTERFACE="$detected_iface"
712+
else
713+
TSHARK_INTERFACE="any"
714+
echo -e "${YELLOW}Warning: Unable to auto-detect default interface. Falling back to 'any'.${RESET}"
715+
fi
716+
fi
717+
export TSHARK_INTERFACE
718+
690719
mkdir -p "$RESULTS_ROOT" "$RUN_DIR" "$LOG_DIR" "$THUMB_DIR"
691720
if [[ $EXTRA_IVRE_ENABLED == true ]]; then
692721
: > "$IVRE_LOG_FILE"
@@ -913,10 +942,19 @@ case ${answer:0:1} in
913942
PYTHON_BIN="$ROOT_DIR/venv/bin/python3"
914943
fi
915944

945+
if [[ -f "$PATHS_FILE" ]]; then
946+
if command -v "$PYTHON_BIN" >/dev/null 2>&1 && "$PYTHON_BIN" "$SCRIPT_DIR/profile_resolver.py" catalog --paths "$PATHS_FILE" --output "$CATALOG_JSON" >/dev/null 2>&1; then
947+
echo -e "${CYAN}Exported catalog to:${RESET} ${GREEN}$CATALOG_JSON${RESET}"
948+
else
949+
echo -e "${YELLOW}Warning: Failed to export catalog JSON. Verify Python availability and $PATHS_FILE formatting.${RESET}"
950+
fi
951+
fi
952+
916953
# Build coap-client on demand using the shared spinner helper
917954
if ! command -v coap-client &> /dev/null; then
918955
coap_build_log="$LOG_DIR/coap-build.log"
919-
if cam_run_packinst "Building coap-client (libcoap)" env COAP_BUILD_LOG="$coap_build_log" CAM_COAP_HELPER="$SCRIPT_DIR/build-coap.sh" bash -c '"$CAM_COAP_HELPER" &>> "$COAP_BUILD_LOG"'; then
956+
: > "$coap_build_log"
957+
if cam_run_packinst "Building coap-client (libcoap)" build_coap_with_log; then
920958
echo -e "${CYAN}CoAP build log:${RESET} ${GREEN}$coap_build_log${RESET}"
921959
else
922960
echo -e "${RED}Failed to build coap-client; see ${coap_build_log}.${RESET}"
@@ -1212,10 +1250,11 @@ case ${answer:0:1} in
12121250

12131251
echo ""
12141252
echo -e "${BLUE}Capturing network traffic for camera protocols...${RESET}"
1253+
echo -e "${CYAN}Using interface:${RESET} ${GREEN}$TSHARK_INTERFACE${RESET}"
12151254

12161255
tshark_output=$(mktemp)
12171256
tshark_duration=${CAM_MODE_TSHARK_DURATION:-30}
1218-
timeout "${tshark_duration}s" tshark -n -i any \
1257+
timeout "${tshark_duration}s" tshark -n -i "$TSHARK_INTERFACE" \
12191258
-f "tcp port 80 or tcp port 554 or tcp port 8554 or udp portrange 5000-5010" \
12201259
-Y "rtsp || http.request || udp.port == 3702" \
12211260
-T fields -E header=n -E separator=, -E quote=d \
@@ -1431,135 +1470,7 @@ case ${answer:0:1} in
14311470

14321471
if [[ -f "$PATHS_FILE" && -s "$DISCOVERY_JSON" ]]; then
14331472
discovery_enriched_tmp=$(mktemp)
1434-
if ! "$PYTHON_BIN" - "$PATHS_FILE" "$DISCOVERY_JSON" "$discovery_enriched_tmp" <<'PY'
1435-
import csv
1436-
import json
1437-
import re
1438-
import sys
1439-
1440-
paths_file, discovery_file, output_file = sys.argv[1:4]
1441-
1442-
try:
1443-
with open(discovery_file, "r", encoding="utf-8") as fh:
1444-
data = json.load(fh)
1445-
except FileNotFoundError:
1446-
data = {"hosts": []}
1447-
1448-
try:
1449-
with open(paths_file, "r", encoding="utf-8") as handle:
1450-
catalog = list(csv.DictReader(handle))
1451-
except FileNotFoundError:
1452-
catalog = []
1453-
1454-
def parse_list(value):
1455-
if not value:
1456-
return []
1457-
parts = [item.strip() for item in value.split(';') if item.strip()]
1458-
return parts
1459-
1460-
def coerce_port(value):
1461-
if value is None:
1462-
return None
1463-
if isinstance(value, int):
1464-
return value
1465-
try:
1466-
return int(str(value).strip())
1467-
except (TypeError, ValueError):
1468-
return None
1469-
1470-
for host in data.get("hosts", []):
1471-
ports = set()
1472-
for entry in host.get("ports", []):
1473-
converted = coerce_port(entry)
1474-
if converted is not None:
1475-
ports.add(converted)
1476-
mac = (host.get("mac") or "").upper()
1477-
best = None
1478-
best_score = -1
1479-
for row in catalog:
1480-
matched_by = None
1481-
pattern = (row.get("oui_regex") or "").strip()
1482-
if mac and pattern:
1483-
try:
1484-
if re.search(pattern, mac, re.IGNORECASE):
1485-
matched_by = "oui"
1486-
except re.error:
1487-
pass
1488-
if not matched_by:
1489-
candidate_port = coerce_port(row.get("port"))
1490-
if candidate_port is not None and candidate_port in ports:
1491-
matched_by = "port"
1492-
if not matched_by:
1493-
continue
1494-
score = 2 if matched_by == "oui" else 1
1495-
if score <= best_score:
1496-
continue
1497-
best = (row, matched_by)
1498-
best_score = score
1499-
1500-
if not best:
1501-
continue
1502-
1503-
row, matched_by = best
1504-
1505-
def build_rtsp_candidates():
1506-
template = row.get("rtsp_url") or ""
1507-
if not template:
1508-
return []
1509-
port_val = coerce_port(row.get("port"))
1510-
streams = parse_list(row.get("streams")) or ["0"]
1511-
channels = parse_list(row.get("channels")) or ["1"]
1512-
candidates = []
1513-
for channel in channels[:3]:
1514-
for stream in streams[:3]:
1515-
candidates.append({
1516-
"template": template,
1517-
"port": port_val if port_val is not None else (row.get("port") or 554),
1518-
"channel": channel,
1519-
"stream": stream,
1520-
"transport": "tcp"
1521-
})
1522-
if len(candidates) >= 6:
1523-
return candidates
1524-
return candidates
1525-
1526-
def build_http_candidates():
1527-
template = row.get("http_snapshot_url") or ""
1528-
if not template:
1529-
return []
1530-
port_val = coerce_port(row.get("port"))
1531-
streams = parse_list(row.get("streams")) or ["0"]
1532-
channels = parse_list(row.get("channels")) or ["1"]
1533-
port_guess = port_val if port_val is not None else (443 if template.lower().startswith("https") else 80)
1534-
return [{
1535-
"template": template,
1536-
"port": port_guess,
1537-
"channel": channels[0],
1538-
"stream": streams[0]
1539-
}]
1540-
1541-
profile = {
1542-
"vendor": row.get("company") or "Unknown",
1543-
"model": row.get("model") or "Unknown",
1544-
"type": row.get("type") or "Unknown",
1545-
"matched_by": matched_by,
1546-
"default_username": row.get("username") or "",
1547-
"default_password": row.get("password") or "",
1548-
"digest_auth": str(row.get("is_digest_auth_supported") or "").lower() in {"true", "yes", "1"},
1549-
"video_encoding": row.get("video_encoding") or "",
1550-
"rtsp_candidates": build_rtsp_candidates(),
1551-
"http_snapshot_candidates": build_http_candidates(),
1552-
"onvif_profiles": parse_list(row.get("onvif_profile_path")),
1553-
"cve_ids": parse_list(row.get("cve_ids")),
1554-
"reference": row.get("user_manual_url") or ""
1555-
}
1556-
1557-
host["profile_match"] = profile
1558-
1559-
with open(output_file, "w", encoding="utf-8") as fh:
1560-
json.dump(data, fh, indent=2)
1561-
PY
1562-
then
1473+
if "$PYTHON_BIN" "$SCRIPT_DIR/profile_resolver.py" enrich --paths "$PATHS_FILE" --input "$DISCOVERY_JSON" --output "$discovery_enriched_tmp" --limit 3; then
15631474
mv "$discovery_enriched_tmp" "$DISCOVERY_JSON"
15641475
else
15651476
echo -e "${YELLOW}Warning: Unable to enrich device profiles; see above for details.${RESET}"
@@ -1613,6 +1524,9 @@ PY
16131524
if [[ -f "$COAP_OUTPUT_FILE" ]]; then
16141525
echo -e "${CYAN}CoAP discovery list:${RESET} ${GREEN}$COAP_OUTPUT_FILE${RESET}"
16151526
fi
1527+
if [[ -f "$CATALOG_JSON" ]]; then
1528+
echo -e "${CYAN}Catalog snapshot:${RESET} ${GREEN}$CATALOG_JSON${RESET}"
1529+
fi
16161530
if [[ $EXTRA_IVRE_ENABLED == true && -s "$IVRE_LOG_FILE" ]]; then
16171531
echo -e "${CYAN}IVRE sync log:${RESET} ${GREEN}$IVRE_LOG_FILE${RESET}"
16181532
fi

0 commit comments

Comments
 (0)