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
107SCRIPT_DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd) "
118ROOT_DIR=" $( cd " $SCRIPT_DIR /.." && pwd) "
@@ -16,8 +13,8 @@ CREDENTIAL_PROBE="$SCRIPT_DIR/credential-probe.sh"
1613NMAP_RTSP_SCRIPT=" $ROOT_DIR /data/rtsp-url-brute.nse"
1714NMAP_RTSP_THREADS=10
1815RTSP_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
2219MODE_DEFAULT=" medium"
2320MODE_REQUESTED=" "
@@ -46,6 +43,7 @@ COAP_OUTPUT_FILE="$LOG_DIR/coap-discovery.txt"
4643COAP_LOG_FILE=" $LOG_DIR /coap-probe.log"
4744COAP_PROBE_TIMEOUT=" ${COAP_PROBE_TIMEOUT:- 5} "
4845IVRE_LOG_FILE=" $LOG_DIR /ivre-sync.log"
46+ CATALOG_JSON=" $RUN_DIR /paths.json"
4947GEOIP_DIR=" $ROOT_DIR /share/geoip"
5048GEOIP_CITY_DB=" $GEOIP_DIR /dbip-city-lite.mmdb"
5149GEOIP_ASN_DB=" $GEOIP_DIR /dbip-asn-lite.mmdb"
@@ -80,6 +78,7 @@ tshark_output=""
8078hosts_json_tmp=" "
8179discovery_enriched_tmp=" "
8280coap_output_tmp=" "
81+ coap_build_log=" "
8382
8483print_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+
178188record_protocol_hit () {
179189 local ip=" $1 "
180190 local proto=" $2 "
@@ -508,15 +518,15 @@ if [[ ! -f "$PORT_PROFILE_DATA" ]]; then
508518 exit 1
509519fi
510520
511- # shellcheck source=data/ port-profiles.sh
521+ # shellcheck source=port-profiles.sh
512522source " $PORT_PROFILE_DATA "
513523
514524if [[ ! -f " $UI_HELPER " ]]; then
515525 echo " Missing UI helper: $UI_HELPER " >&2
516526 exit 1
517527fi
518528
519- # shellcheck source=data/ ui-banner.sh
529+ # shellcheck source=ui-banner.sh
520530source " $UI_HELPER "
521531
522532cam_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
593610do_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
688706fi
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+
690719mkdir -p " $RESULTS_ROOT " " $RUN_DIR " " $LOG_DIR " " $THUMB_DIR "
691720if [[ $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} "
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